From daa77d17aad02cd8e99cc71de9ece47aa35be0bc Mon Sep 17 00:00:00 2001 From: Wolfgang Walther Date: Sat, 27 Nov 2021 23:15:34 +0100 Subject: [PATCH] feat: Drop support for pg 9.6 --- .github/workflows/test.yaml | 2 +- CHANGELOG.md | 4 + default.nix | 1 - docs/explanations/install.rst | 6 +- nix/README.md | 4 +- nix/overlays/postgis.nix | 5 - nix/overlays/postgresql-legacy.nix | 15 -- src/PostgREST/Config/PgVersion.hs | 7 +- src/PostgREST/Query.hs | 2 +- src/PostgREST/SchemaCache.hs | 28 +-- test/spec/Feature/OpenApi/OpenApiSpec.hs | 36 ++- test/spec/Feature/OptionsSpec.hs | 26 +-- test/spec/Feature/Query/InsertSpec.hs | 262 +++++++++++---------- test/spec/Feature/Query/PostGISSpec.hs | 97 +++----- test/spec/Feature/Query/RpcSpec.hs | 191 ++++++---------- test/spec/Feature/Query/UpdateSpec.hs | 280 +++++++---------------- test/spec/Main.hs | 4 +- test/spec/fixtures/data.sql | 12 +- test/spec/fixtures/privileges.sql | 8 +- test/spec/fixtures/schema.sql | 67 ++---- 20 files changed, 401 insertions(+), 656 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 5840b1737..96674e231 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -67,7 +67,7 @@ jobs: strategy: fail-fast: false matrix: - pgVersion: ["9_6", 10, 11, 12, 13, 14, 15, 16] + pgVersion: [10, 11, 12, 13, 14, 15, 16] name: PG ${{ matrix.pgVersion }} runs-on: ubuntu-22.04 defaults: diff --git a/CHANGELOG.md b/CHANGELOG.md index ceb7caca9..cdf29ac04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ This project adheres to [Semantic Versioning](http://semver.org/). ## Unreleased +### Changed + + - #2052, Dropped support for PostgreSQL 9.6 - @wolfgangwalther + ## [12.2.0] - 2024-06-11 ### Added diff --git a/default.nix b/default.nix index 3cf63c83d..a432b2f9d 100644 --- a/default.nix +++ b/default.nix @@ -56,7 +56,6 @@ let { name = "postgresql-12"; postgresql = pkgs.postgresql_12.withPackages (p: [ p.postgis p.pg_safeupdate ]); } { name = "postgresql-11"; postgresql = pkgs.postgresql_11.withPackages (p: [ p.postgis p.pg_safeupdate ]); } { name = "postgresql-10"; postgresql = pkgs.postgresql_10.withPackages (p: [ p.postgis p.pg_safeupdate ]); } - { name = "postgresql-9_6"; postgresql = pkgs.postgresql_9_6.withPackages (p: [ p.postgis p.pg_safeupdate ]); } ]; # Dynamic derivation for PostgREST diff --git a/docs/explanations/install.rst b/docs/explanations/install.rst index e6374dac4..e9378dfed 100644 --- a/docs/explanations/install.rst +++ b/docs/explanations/install.rst @@ -16,14 +16,14 @@ Supported PostgreSQL versions ============================= =============== ================================= -**Supported** PostgreSQL >= 9.6 +**Supported** PostgreSQL >= 10 =============== ================================= -PostgREST works with all PostgreSQL versions starting from 9.6. +PostgREST works with all PostgreSQL versions starting from 10. .. note:: - 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. + Support for PostgreSQL versions 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/. diff --git a/nix/README.md b/nix/README.md index 94804b1fd..8d3087b95 100644 --- a/nix/README.md +++ b/nix/README.md @@ -79,7 +79,7 @@ postgrest-coverage postgrest-with-postgresql-10 postgrest-lint postgrest-with-postgresql-11 postgrest-run postgrest-with-postgresql-12 postgrest-style postgrest-with-postgresql-13 -postgrest-style-check postgrest-with-postgresql-9.6 +postgrest-style-check postgrest-with-postgresql-14 postgrest-test-io ... @@ -103,7 +103,7 @@ postgrest-coverage postgrest-with-postgresql-10 postgrest-lint postgrest-with-postgresql-11 postgrest-run postgrest-with-postgresql-12 postgrest-style postgrest-with-postgresql-13 -postgrest-style-check postgrest-with-postgresql-9.6 +postgrest-style-check postgrest-with-postgresql-14 postgrest-test-io postgrest-test-memory ... diff --git a/nix/overlays/postgis.nix b/nix/overlays/postgis.nix index 26b5e226e..49bf26aef 100644 --- a/nix/overlays/postgis.nix +++ b/nix/overlays/postgis.nix @@ -20,9 +20,4 @@ in postgis = prev.postgresql_10.pkgs.postgis.overrideAttrs (_: postgis_3_2_3); }; }; - postgresql_9_6 = prev.postgresql_9_6.override { this = final.postgresql_11; } // { - pkgs = prev.postgresql_9_6.pkgs // { - postgis = prev.postgresql_9_6.pkgs.postgis.overrideAttrs (_: postgis_3_2_3); - }; - }; } diff --git a/nix/overlays/postgresql-legacy.nix b/nix/overlays/postgresql-legacy.nix index 506d8eb34..aeef1a6df 100644 --- a/nix/overlays/postgresql-legacy.nix +++ b/nix/overlays/postgresql-legacy.nix @@ -2,21 +2,6 @@ _: _: # Overlay that adds legacy versions of PostgreSQL that are supported by # PostgREST. { - # PostgreSQL 9.6 was removed from Nixpkgs with - # https://github.com/NixOS/nixpkgs/commit/757dd008b2f2926fc0f7688fa8189f930ea47521 - # We pin its parent commit to get the last version that was available. - postgresql_9_6 = - let - rev = "571cbf3d1db477058303cef8754fb85a14e90eb7"; - tarballHash = "0q74wn418i1bn5sssacmw8ykpmqvzr0s93sj6pbs3rf6bf134fkz"; - pinnedPkgs = - builtins.fetchTarball { - url = "https://github.com/nixos/nixpkgs/archive/${rev}.tar.gz"; - sha256 = tarballHash; - }; - in - (import pinnedPkgs { }).pkgs.postgresql_9_6; - # PostgreSQL 10 was removed from Nixpkgs with # https://github.com/NixOS/nixpkgs/commit/aa1483114bb329fee7e1266100b8d8921ed4723f # We pin its parent commit to get the last version that was available. diff --git a/src/PostgREST/Config/PgVersion.hs b/src/PostgREST/Config/PgVersion.hs index 921824453..855352f3b 100644 --- a/src/PostgREST/Config/PgVersion.hs +++ b/src/PostgREST/Config/PgVersion.hs @@ -3,8 +3,6 @@ module PostgREST.Config.PgVersion ( PgVersion(..) , minimumPgVersion - , pgVersion96 - , pgVersion100 , pgVersion109 , pgVersion110 , pgVersion112 @@ -33,10 +31,7 @@ instance Ord PgVersion where -- | Tells the minimum PostgreSQL version required by this version of PostgREST minimumPgVersion :: PgVersion -minimumPgVersion = pgVersion96 - -pgVersion96 :: PgVersion -pgVersion96 = PgVersion 90600 "9.6" "9.6" +minimumPgVersion = pgVersion100 pgVersion100 :: PgVersion pgVersion100 = PgVersion 100000 "10" "10" diff --git a/src/PostgREST/Query.hs b/src/PostgREST/Query.hs index ab6ddf8cd..59f5ecf61 100644 --- a/src/PostgREST/Query.hs +++ b/src/PostgREST/Query.hs @@ -178,7 +178,7 @@ actionQuery (DbCall plan@CallReadPlan{..}) conf@AppConfig{..} apiReq@ApiRequest{ actionQuery (MaybeDb plan@InspectPlan{ipSchema=tSchema}) AppConfig{..} _ pgVer sCache = lift $ case configOpenApiMode of OAFollowPriv -> do - tableAccess <- SQL.statement [tSchema] (SchemaCache.accessibleTables pgVer configDbPreparedStatements) + tableAccess <- SQL.statement [tSchema] (SchemaCache.accessibleTables configDbPreparedStatements) MaybeDbResult plan . Just <$> ((,,) (HM.filterWithKey (\qi _ -> S.member qi tableAccess) $ SchemaCache.dbTables sCache) <$> SQL.statement (tSchema, configDbHoistedTxSettings) (SchemaCache.accessibleFuncs pgVer configDbPreparedStatements) diff --git a/src/PostgREST/SchemaCache.hs b/src/PostgREST/SchemaCache.hs index b6dca5ba1..a597edda6 100644 --- a/src/PostgREST/SchemaCache.hs +++ b/src/PostgREST/SchemaCache.hs @@ -47,8 +47,7 @@ import PostgREST.Config (AppConfig (..)) import PostgREST.Config.Database (TimezoneNames, pgVersionStatement, toIsolationLevel) -import PostgREST.Config.PgVersion (PgVersion, pgVersion100, - pgVersion110, +import PostgREST.Config.PgVersion (PgVersion, pgVersion110, pgVersion120) import PostgREST.SchemaCache.Identifiers (AccessSet, FieldName, QualifiedIdentifier (..), @@ -477,8 +476,8 @@ schemaDescription = where n.nspname = $1 |] -accessibleTables :: PgVersion -> Bool -> SQL.Statement [Schema] AccessSet -accessibleTables pgVer = +accessibleTables :: Bool -> SQL.Statement [Schema] AccessSet +accessibleTables = SQL.Statement sql (arrayParam HE.text) decodeAccessibleIdentifiers where sql = [q| @@ -494,10 +493,9 @@ accessibleTables pgVer = pg_has_role(c.relowner, 'USAGE') or has_table_privilege(c.oid, 'SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER') or has_any_column_privilege(c.oid, 'SELECT, INSERT, UPDATE, REFERENCES') - ) |] <> - relIsPartition <> - "ORDER BY table_schema, table_name" - relIsPartition = if pgVer >= pgVersion100 then " AND not c.relispartition " else mempty + ) + AND not c.relispartition + ORDER BY table_schema, table_name|] {- Adds 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. @@ -810,11 +808,10 @@ tablesSqlQuery pgVer = LEFT JOIN tbl_pk_cols tpks ON n.nspname = tpks.table_schema AND c.relname = tpks.table_name LEFT JOIN columns_agg cols_agg ON n.nspname = cols_agg.table_schema AND c.relname = cols_agg.table_name WHERE c.relkind IN ('v','r','m','f','p') - AND n.nspname NOT IN ('pg_catalog', 'information_schema') |] <> - relIsPartition <> - "ORDER BY table_schema, table_name" + AND n.nspname NOT IN ('pg_catalog', 'information_schema') + AND not c.relispartition + ORDER BY table_schema, table_name|] where - relIsPartition = if pgVer >= pgVersion100 then " AND not c.relispartition " else mempty columnDefault -- typbasetype and typdefaultbin handles `CREATE DOMAIN .. DEFAULT val`, attidentity/attgenerated handles generated columns, pg_get_expr gets the default of a column | pgVer >= pgVersion120 = [q| CASE @@ -823,17 +820,12 @@ tablesSqlQuery pgVer = WHEN a.attgenerated = 's' THEN null ELSE pg_get_expr(ad.adbin, ad.adrelid)::text END|] - | pgVer >= pgVersion100 = [q| + | otherwise = [q| CASE WHEN t.typbasetype != 0 THEN pg_get_expr(t.typdefaultbin, 0) WHEN a.attidentity = 'd' THEN format('nextval(%s)', quote_literal(seqsch.nspname || '.' || seqclass.relname)) ELSE pg_get_expr(ad.adbin, ad.adrelid)::text END|] - | otherwise = [q| - CASE - WHEN t.typbasetype != 0 THEN pg_get_expr(t.typdefaultbin, 0) - ELSE pg_get_expr(ad.adbin, ad.adrelid)::text - END|] -- | Gets many-to-one relationships and one-to-one(O2O) relationships, which are a refinement of the many-to-one's allM2OandO2ORels :: PgVersion -> Bool -> SQL.Statement () [Relationship] diff --git a/test/spec/Feature/OpenApi/OpenApiSpec.hs b/test/spec/Feature/OpenApi/OpenApiSpec.hs index 0fc29508b..72275a1f1 100644 --- a/test/spec/Feature/OpenApi/OpenApiSpec.hs +++ b/test/spec/Feature/OpenApi/OpenApiSpec.hs @@ -11,8 +11,7 @@ import Network.HTTP.Types import Test.Hspec hiding (pendingWith) import Test.Hspec.Wai -import PostgREST.Config.PgVersion (PgVersion, pgVersion100, - pgVersion110) +import PostgREST.Config.PgVersion (PgVersion, pgVersion110) import PostgREST.Version (docsVersion) import Protolude hiding (get) @@ -269,31 +268,30 @@ spec actualPgVersion = describe "OpenAPI" $ do ] |] - when (actualPgVersion >= pgVersion100) $ do - describe "Partitioned table" $ + describe "Partitioned table" $ - it "includes partitioned table properties" $ do - r <- simpleBody <$> get "/" + it "includes partitioned table properties" $ do + r <- simpleBody <$> get "/" - let method s = key "paths" . key "/car_models" . key s - getSummary = r ^? method "get" . key "summary" - getDescription = r ^? method "get" . key "description" - getParameterName = r ^? method "get" . key "parameters" . nth 0 . key "$ref" - getParameterYear = r ^? method "get" . key "parameters" . nth 1 . key "$ref" - getParameterRef = r ^? method "get" . key "parameters" . nth 2 . key "$ref" + let method s = key "paths" . key "/car_models" . key s + getSummary = r ^? method "get" . key "summary" + getDescription = r ^? method "get" . key "description" + getParameterName = r ^? method "get" . key "parameters" . nth 0 . key "$ref" + getParameterYear = r ^? method "get" . key "parameters" . nth 1 . key "$ref" + getParameterRef = r ^? method "get" . key "parameters" . nth 2 . key "$ref" - liftIO $ do + liftIO $ do - getSummary `shouldBe` Just "A partitioned table" + getSummary `shouldBe` Just "A partitioned table" - getDescription `shouldBe` Just "A test for partitioned tables" + getDescription `shouldBe` Just "A test for partitioned tables" - getParameterName `shouldBe` Just "#/parameters/rowFilter.car_models.name" + getParameterName `shouldBe` Just "#/parameters/rowFilter.car_models.name" - getParameterYear `shouldBe` Just "#/parameters/rowFilter.car_models.year" + getParameterYear `shouldBe` Just "#/parameters/rowFilter.car_models.year" - when (actualPgVersion >= pgVersion110) $ - getParameterRef `shouldBe` Just "#/parameters/rowFilter.car_models.car_brand_name" + when (actualPgVersion >= pgVersion110) $ + getParameterRef `shouldBe` Just "#/parameters/rowFilter.car_models.car_brand_name" describe "Materialized view" $ diff --git a/test/spec/Feature/OptionsSpec.hs b/test/spec/Feature/OptionsSpec.hs index 78e5e5d79..ebe1a412e 100644 --- a/test/spec/Feature/OptionsSpec.hs +++ b/test/spec/Feature/OptionsSpec.hs @@ -7,8 +7,7 @@ import Network.HTTP.Types import Test.Hspec import Test.Hspec.Wai -import PostgREST.Config.PgVersion (PgVersion, pgVersion100, - pgVersion110) +import PostgREST.Config.PgVersion (PgVersion, pgVersion110) import Protolude import SpecHelper @@ -25,18 +24,17 @@ spec actualPgVersion = describe "Allow header" $ do it "fails with 404 for an unknown table" $ request methodOptions "/unknown" [] "" `shouldRespondWith` 404 - when (actualPgVersion >= pgVersion100) $ - context "a partitioned table" $ do - it "includes read/write methods for writeable partitioned tables" $ do - r <- request methodOptions "/car_models" [] "" - liftIO $ - simpleHeaders r `shouldSatisfy` - matchHeader "Allow" ( - if actualPgVersion >= pgVersion110 then - "OPTIONS,GET,HEAD,POST,PUT,PATCH,DELETE" - else - "OPTIONS,GET,HEAD,POST,PATCH,DELETE" - ) + context "a partitioned table" $ do + it "includes read/write methods for writeable partitioned tables" $ do + r <- request methodOptions "/car_models" [] "" + liftIO $ + simpleHeaders r `shouldSatisfy` + matchHeader "Allow" ( + if actualPgVersion >= pgVersion110 then + "OPTIONS,GET,HEAD,POST,PUT,PATCH,DELETE" + else + "OPTIONS,GET,HEAD,POST,PATCH,DELETE" + ) context "a view" $ do context "auto updatable" $ do diff --git a/test/spec/Feature/Query/InsertSpec.hs b/test/spec/Feature/Query/InsertSpec.hs index 4bf15d898..b6281b4c1 100644 --- a/test/spec/Feature/Query/InsertSpec.hs +++ b/test/spec/Feature/Query/InsertSpec.hs @@ -11,10 +11,9 @@ import Test.Hspec.Wai import Test.Hspec.Wai.JSON import Text.Heredoc -import PostgREST.Config.PgVersion (PgVersion, pgVersion100, - pgVersion110, pgVersion112, - pgVersion120, pgVersion130, - pgVersion140) +import PostgREST.Config.PgVersion (PgVersion, pgVersion110, + pgVersion112, pgVersion120, + pgVersion130, pgVersion140) import Protolude hiding (get) import SpecHelper @@ -494,24 +493,22 @@ spec actualPgVersion = do {"id": 205, "body": "zzz"}]|] `shouldRespondWith` 400 context "apply defaults on missing values" $ do - -- inserting the array fails on pg 9.6, but the feature should work normally - when (actualPgVersion >= pgVersion100) $ - it "inserts table default values(field-with_sep) when json keys are undefined" $ - request methodPost "/complex_items?columns=id,name,field-with_sep,arr_data" [("Prefer", "return=representation"), ("Prefer", "missing=default")] - [json|[ - {"id": 4, "name": "Vier"}, - {"id": 5, "name": "Funf", "arr_data": null}, - {"id": 6, "name": "Sechs", "field-with_sep": 6, "arr_data": "{1,2,3}"} - ]|] - `shouldRespondWith` - [json|[ - {"id": 4, "name": "Vier", "field-with_sep": 1, "settings":null,"arr_data":null}, - {"id": 5, "name": "Funf", "field-with_sep": 1, "settings":null,"arr_data":null}, - {"id": 6, "name": "Sechs", "field-with_sep": 6, "settings":null,"arr_data":[1,2,3]} - ]|] - { matchStatus = 201 - , matchHeaders = ["Preference-Applied" <:> "missing=default, return=representation"] - } + it "inserts table default values(field-with_sep) when json keys are undefined" $ + request methodPost "/complex_items?columns=id,name,field-with_sep,arr_data" [("Prefer", "return=representation"), ("Prefer", "missing=default")] + [json|[ + {"id": 4, "name": "Vier"}, + {"id": 5, "name": "Funf", "arr_data": null}, + {"id": 6, "name": "Sechs", "field-with_sep": 6, "arr_data": "{1,2,3}"} + ]|] + `shouldRespondWith` + [json|[ + {"id": 4, "name": "Vier", "field-with_sep": 1, "settings":null,"arr_data":null}, + {"id": 5, "name": "Funf", "field-with_sep": 1, "settings":null,"arr_data":null}, + {"id": 6, "name": "Sechs", "field-with_sep": 6, "settings":null,"arr_data":[1,2,3]} + ]|] + { matchStatus = 201 + , matchHeaders = ["Preference-Applied" <:> "missing=default, return=representation"] + } it "inserts view default values(field-with_sep) when json keys are undefined" $ request methodPost "/complex_items_view?columns=id,name" [("Prefer", "return=representation"), ("Prefer", "missing=default")] @@ -537,15 +534,14 @@ spec actualPgVersion = do , matchHeaders = ["Preference-Applied" <:> "missing=default, return=representation"] } - when (actualPgVersion >= pgVersion100) $ - it "inserts a default on a generated by default as identity column" $ - request methodPost "/channels?columns=id,data,slug&select=data,slug" [("Prefer", "return=representation"), ("Prefer", "missing=default")] - [json| { "slug": "foo" } |] - `shouldRespondWith` - [json| [{"data":{"foo": "bar"},"slug":"foo"}] |] -- id 1 was inserted here, we don't get it for idempotence in the tests - { matchStatus = 201 - , matchHeaders = ["Preference-Applied" <:> "missing=default, return=representation"] - } + it "inserts a default on a generated by default as identity column" $ + request methodPost "/channels?columns=id,data,slug&select=data,slug" [("Prefer", "return=representation"), ("Prefer", "missing=default")] + [json| { "slug": "foo" } |] + `shouldRespondWith` + [json| [{"data":{"foo": "bar"},"slug":"foo"}] |] -- id 1 was inserted here, we don't get it for idempotence in the tests + { matchStatus = 201 + , matchHeaders = ["Preference-Applied" <:> "missing=default, return=representation"] + } when (actualPgVersion >= pgVersion120) $ it "fails with a good error message on generated always columns" $ @@ -798,115 +794,113 @@ spec actualPgVersion = do } - -- Data representations for payload parsing requires Postgres 10 or above. - when (actualPgVersion >= pgVersion100) $ do - describe "Data representations" $ do - context "on regular table" $ do - it "parses values in POST body" $ - -- we don't check that the parsing is correct here, just that it's happening. If it doesn't happen we'll get a - -- an "invalid input syntax for type integer:" error. - request methodPost "/datarep_todos" [("Prefer", "return=headers-only")] - [json| {"id":5, "name": "party", "label_color": "#001100", "due_at": "2018-01-03T11:00:00+00"} |] - `shouldRespondWith` - "" - { matchStatus = 201 - , matchHeaders = [ matchHeaderAbsent hContentType - , "Location" <:> "/datarep_todos?id=eq.5" - , "Content-Range" <:> "*/*" - , "Preference-Applied" <:> "return=headers-only"] - } + describe "Data representations" $ do + context "on regular table" $ do + it "parses values in POST body" $ + -- we don't check that the parsing is correct here, just that it's happening. If it doesn't happen we'll get a + -- an "invalid input syntax for type integer:" error. + request methodPost "/datarep_todos" [("Prefer", "return=headers-only")] + [json| {"id":5, "name": "party", "label_color": "#001100", "due_at": "2018-01-03T11:00:00+00"} |] + `shouldRespondWith` + "" + { matchStatus = 201 + , matchHeaders = [ matchHeaderAbsent hContentType + , "Location" <:> "/datarep_todos?id=eq.5" + , "Content-Range" <:> "*/*" + , "Preference-Applied" <:> "return=headers-only"] + } - it "parses values in POST body and formats individually selected values in return=representation" $ - request methodPost "/datarep_todos?select=id,label_color" [("Prefer", "return=representation")] - [json| {"id":5, "name": "party", "label_color": "#001100", "due_at": "2018-01-03T11:00:00+00"} |] - `shouldRespondWith` - [json| [{"id":5, "label_color": "#001100"}] |] - { matchStatus = 201 - , matchHeaders = ["Content-Type" <:> "application/json; charset=utf-8", - "Content-Range" <:> "*/*"] - } + it "parses values in POST body and formats individually selected values in return=representation" $ + request methodPost "/datarep_todos?select=id,label_color" [("Prefer", "return=representation")] + [json| {"id":5, "name": "party", "label_color": "#001100", "due_at": "2018-01-03T11:00:00+00"} |] + `shouldRespondWith` + [json| [{"id":5, "label_color": "#001100"}] |] + { matchStatus = 201 + , matchHeaders = ["Content-Type" <:> "application/json; charset=utf-8", + "Content-Range" <:> "*/*"] + } - it "parses values in POST body and formats values in return=representation" $ - request methodPost "/datarep_todos" [("Prefer", "return=representation")] - [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"} |] - `shouldRespondWith` - [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"}] |] - { matchStatus = 201 - , matchHeaders = ["Content-Type" <:> "application/json; charset=utf-8", - "Content-Range" <:> "*/*"] - } + it "parses values in POST body and formats values in return=representation" $ + request methodPost "/datarep_todos" [("Prefer", "return=representation")] + [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"} |] + `shouldRespondWith` + [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"}] |] + { matchStatus = 201 + , matchHeaders = ["Content-Type" <:> "application/json; charset=utf-8", + "Content-Range" <:> "*/*"] + } - context "with ?columns parameter" $ do - it "ignores json keys not included in ?columns; parses only the ones specified" $ - request methodPost "/datarep_todos?columns=id,label_color&select=id,name,label_color,due_at" [("Prefer", "return=representation")] - [json| {"id":5, "name": "party", "label_color": "#001100", "due_at": "invalid but should be ignored"} |] - `shouldRespondWith` - [json| [{"id":5, "name":null, "label_color": "#001100", "due_at": "2018-01-01T00:00:00Z"}] |] - { matchStatus = 201 - , matchHeaders = ["Content-Type" <:> "application/json; charset=utf-8", - "Content-Range" <:> "*/*"] - } + context "with ?columns parameter" $ do + it "ignores json keys not included in ?columns; parses only the ones specified" $ + request methodPost "/datarep_todos?columns=id,label_color&select=id,name,label_color,due_at" [("Prefer", "return=representation")] + [json| {"id":5, "name": "party", "label_color": "#001100", "due_at": "invalid but should be ignored"} |] + `shouldRespondWith` + [json| [{"id":5, "name":null, "label_color": "#001100", "due_at": "2018-01-01T00:00:00Z"}] |] + { matchStatus = 201 + , matchHeaders = ["Content-Type" <:> "application/json; charset=utf-8", + "Content-Range" <:> "*/*"] + } - it "fails without parsing anything if at least one specified column doesn't exist" $ - request methodPost "/datarep_todos?columns=id,label_color,helicopters&select=id,name,label_color,due_at" [("Prefer", "return=representation")] - [json| {"due_at": "2019-01-03T11:00:00+00", "smth": "here", "label_color": "invalid", "fake_id": 13} |] - `shouldRespondWith` - [json| {"code":"PGRST204","details":null,"hint":null,"message":"Could not find the 'helicopters' column of 'datarep_todos' in the schema cache"} |] - { matchStatus = 400 - , matchHeaders = ["Content-Type" <:> "application/json; charset=utf-8"] - } + it "fails without parsing anything if at least one specified column doesn't exist" $ + request methodPost "/datarep_todos?columns=id,label_color,helicopters&select=id,name,label_color,due_at" [("Prefer", "return=representation")] + [json| {"due_at": "2019-01-03T11:00:00+00", "smth": "here", "label_color": "invalid", "fake_id": 13} |] + `shouldRespondWith` + [json| {"code":"PGRST204","details":null,"hint":null,"message":"Could not find the 'helicopters' column of 'datarep_todos' in the schema cache"} |] + { matchStatus = 400 + , matchHeaders = ["Content-Type" <:> "application/json; charset=utf-8"] + } - context "on updatable view" $ do - it "parses values in POST body" $ - -- we don't check that the parsing is correct here, just that it's happening. If it doesn't happen we'll get a - -- an "invalid input syntax for type integer:" error. - request methodPost "/datarep_todos_computed" [("Prefer", "return=headers-only")] - [json| {"id":5, "name": "party", "label_color": "#001100", "due_at": "2018-01-03T11:00:00+00"} |] - `shouldRespondWith` - "" - { matchStatus = 201 - , matchHeaders = [ matchHeaderAbsent hContentType - , "Location" <:> "/datarep_todos_computed?id=eq.5" - , "Content-Range" <:> "*/*" - , "Preference-Applied" <:> "return=headers-only"] - } + context "on updatable view" $ do + it "parses values in POST body" $ + -- we don't check that the parsing is correct here, just that it's happening. If it doesn't happen we'll get a + -- an "invalid input syntax for type integer:" error. + request methodPost "/datarep_todos_computed" [("Prefer", "return=headers-only")] + [json| {"id":5, "name": "party", "label_color": "#001100", "due_at": "2018-01-03T11:00:00+00"} |] + `shouldRespondWith` + "" + { matchStatus = 201 + , matchHeaders = [ matchHeaderAbsent hContentType + , "Location" <:> "/datarep_todos_computed?id=eq.5" + , "Content-Range" <:> "*/*" + , "Preference-Applied" <:> "return=headers-only"] + } - it "parses values in POST body and formats individually selected values in return=representation" $ - request methodPost "/datarep_todos_computed?select=id,label_color" [("Prefer", "return=representation")] - [json| {"id":5, "name": "party", "label_color": "#001100", "due_at": "2018-01-03T11:00:00+00"} |] - `shouldRespondWith` - [json| [{"id":5, "label_color": "#001100"}] |] - { matchStatus = 201 - , matchHeaders = ["Content-Type" <:> "application/json; charset=utf-8", - "Content-Range" <:> "*/*"] - } + it "parses values in POST body and formats individually selected values in return=representation" $ + request methodPost "/datarep_todos_computed?select=id,label_color" [("Prefer", "return=representation")] + [json| {"id":5, "name": "party", "label_color": "#001100", "due_at": "2018-01-03T11:00:00+00"} |] + `shouldRespondWith` + [json| [{"id":5, "label_color": "#001100"}] |] + { matchStatus = 201 + , matchHeaders = ["Content-Type" <:> "application/json; charset=utf-8", + "Content-Range" <:> "*/*"] + } - it "parses values in POST body and formats values in return=representation" $ - request methodPost "/datarep_todos_computed" [("Prefer", "return=representation")] - [json| {"id":5, "name": "party", "label_color": "#001100", "due_at": "2018-01-03T11:00:00+00"} |] - `shouldRespondWith` - [json| [{"id":5,"name": "party", "label_color": "#001100", "due_at":"2018-01-03T11:00:00Z", "dark_color":"#000880"}] |] - { matchStatus = 201 - , matchHeaders = ["Content-Type" <:> "application/json; charset=utf-8", - "Content-Range" <:> "*/*"] - } + it "parses values in POST body and formats values in return=representation" $ + request methodPost "/datarep_todos_computed" [("Prefer", "return=representation")] + [json| {"id":5, "name": "party", "label_color": "#001100", "due_at": "2018-01-03T11:00:00+00"} |] + `shouldRespondWith` + [json| [{"id":5,"name": "party", "label_color": "#001100", "due_at":"2018-01-03T11:00:00Z", "dark_color":"#000880"}] |] + { matchStatus = 201 + , matchHeaders = ["Content-Type" <:> "application/json; charset=utf-8", + "Content-Range" <:> "*/*"] + } - context "on updatable views with ?columns parameter" $ do - it "ignores json keys not included in ?columns; parses only the ones specified" $ - request methodPost "/datarep_todos_computed?columns=id,label_color&select=id,name,label_color,due_at" [("Prefer", "return=representation")] - [json| {"id":5, "name": "party", "label_color": "#001100", "due_at": "invalid but should be ignored"} |] - `shouldRespondWith` - [json| [{"id":5, "name":null, "label_color": "#001100", "due_at": "2018-01-01T00:00:00Z"}] |] - { matchStatus = 201 - , matchHeaders = ["Content-Type" <:> "application/json; charset=utf-8", - "Content-Range" <:> "*/*"] - } + context "on updatable views with ?columns parameter" $ do + it "ignores json keys not included in ?columns; parses only the ones specified" $ + request methodPost "/datarep_todos_computed?columns=id,label_color&select=id,name,label_color,due_at" [("Prefer", "return=representation")] + [json| {"id":5, "name": "party", "label_color": "#001100", "due_at": "invalid but should be ignored"} |] + `shouldRespondWith` + [json| [{"id":5, "name":null, "label_color": "#001100", "due_at": "2018-01-01T00:00:00Z"}] |] + { matchStatus = 201 + , matchHeaders = ["Content-Type" <:> "application/json; charset=utf-8", + "Content-Range" <:> "*/*"] + } - it "fails without parsing anything if at least one specified column doesn't exist" $ - request methodPost "/datarep_todos_computed?columns=id,label_color,helicopters&select=id,name,label_color,due_at" [("Prefer", "return=representation")] - [json| {"due_at": "2019-01-03T11:00:00+00", "smth": "here", "label_color": "invalid", "fake_id": 13} |] - `shouldRespondWith` - [json| {"code":"PGRST204","details":null,"hint":null,"message":"Could not find the 'helicopters' column of 'datarep_todos_computed' in the schema cache"} |] - { matchStatus = 400 - , matchHeaders = ["Content-Type" <:> "application/json; charset=utf-8"] - } + it "fails without parsing anything if at least one specified column doesn't exist" $ + request methodPost "/datarep_todos_computed?columns=id,label_color,helicopters&select=id,name,label_color,due_at" [("Prefer", "return=representation")] + [json| {"due_at": "2019-01-03T11:00:00+00", "smth": "here", "label_color": "invalid", "fake_id": 13} |] + `shouldRespondWith` + [json| {"code":"PGRST204","details":null,"hint":null,"message":"Could not find the 'helicopters' column of 'datarep_todos_computed' in the schema cache"} |] + { matchStatus = 400 + , matchHeaders = ["Content-Type" <:> "application/json; charset=utf-8"] + } diff --git a/test/spec/Feature/Query/PostGISSpec.hs b/test/spec/Feature/Query/PostGISSpec.hs index a3e30dbb8..c6c431a79 100644 --- a/test/spec/Feature/Query/PostGISSpec.hs +++ b/test/spec/Feature/Query/PostGISSpec.hs @@ -7,13 +7,11 @@ import Test.Hspec import Test.Hspec.Wai import Test.Hspec.Wai.JSON -import PostgREST.Config.PgVersion (PgVersion, pgVersion100) - import Protolude hiding (get) import SpecHelper -spec :: PgVersion -> SpecWith ((), Application) -spec actualPgVersion = describe "PostGIS features" $ +spec :: SpecWith ((), Application) +spec = describe "PostGIS features" $ context "GeoJSON output" $ do it "works for a table that has a geometry column" $ request methodGet "/shops" @@ -56,59 +54,32 @@ spec actualPgVersion = describe "PostGIS features" $ it "works with resource embedding" $ request methodGet "/shops?select=*,shop_bles(*)&id=eq.1" [("Accept", "application/geo+json")] "" `shouldRespondWith` - (if actualPgVersion >= pgVersion100 - then [json| { - "type": "FeatureCollection", - "features": [ - { - "type": "Feature", - "geometry": { "coordinates": [ -71.10044, 42.373695 ], "type": "Point" }, - "properties": { - "address": "1369 Cambridge St", "id": 1, - "shop_bles": [ - { "id": 1, "name": "Beacon-1", "shop_id": 1 , - "coords": { "coordinates": [ -71.10044, 42.373695 ], "crs": { "properties": { "name": "EPSG:4326" }, "type": "name" }, "type": "Point" }, - "range_area": { - "coordinates": [ [ [ -71.10045254230499, 42.37387083326593 ], [ -71.10048070549963, 42.37377126199953 ], [ -71.10039688646793, 42.37375838212269 ], [ -71.10037006437777, 42.37385844878863 ], [ -71.10045254230499, 42.37387083326593 ] ] ], - "crs": { "properties": { "name": "EPSG:4326" }, "type": "name" }, "type": "Polygon" } - }, - { "coords": { "coordinates": [ -71.10044, 42.373695 ], "crs": { "properties": { "name": "EPSG:4326" }, "type": "name" }, "type": "Point" }, - "id": 2, "name": "Beacon-2", "shop_id": 1, - "range_area": { - "coordinates": [ [ [ -71.10034391283989, 42.37385299961788 ], [ -71.10036939382553, 42.373756895982865 ], [ -71.1002916097641, 42.373745997623224 ], [ -71.1002641171217, 42.37384408279195 ], [ -71.10034391283989, 42.37385299961788 ] ] ], - "crs": { "properties": { "name": "EPSG:4326" }, "type": "name" }, "type": "Polygon" } - } - ] - } + [json| { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { "coordinates": [ -71.10044, 42.373695 ], "type": "Point" }, + "properties": { + "address": "1369 Cambridge St", "id": 1, + "shop_bles": [ + { "id": 1, "name": "Beacon-1", "shop_id": 1 , + "coords": { "coordinates": [ -71.10044, 42.373695 ], "crs": { "properties": { "name": "EPSG:4326" }, "type": "name" }, "type": "Point" }, + "range_area": { + "coordinates": [ [ [ -71.10045254230499, 42.37387083326593 ], [ -71.10048070549963, 42.37377126199953 ], [ -71.10039688646793, 42.37375838212269 ], [ -71.10037006437777, 42.37385844878863 ], [ -71.10045254230499, 42.37387083326593 ] ] ], + "crs": { "properties": { "name": "EPSG:4326" }, "type": "name" }, "type": "Polygon" } + }, + { "coords": { "coordinates": [ -71.10044, 42.373695 ], "crs": { "properties": { "name": "EPSG:4326" }, "type": "name" }, "type": "Point" }, + "id": 2, "name": "Beacon-2", "shop_id": 1, + "range_area": { + "coordinates": [ [ [ -71.10034391283989, 42.37385299961788 ], [ -71.10036939382553, 42.373756895982865 ], [ -71.1002916097641, 42.373745997623224 ], [ -71.1002641171217, 42.37384408279195 ], [ -71.10034391283989, 42.37385299961788 ] ] ], + "crs": { "properties": { "name": "EPSG:4326" }, "type": "name" }, "type": "Polygon" } + } + ] } - ] - }|] - else [json| { - "type": "FeatureCollection", - "features": [ - { - "type": "Feature", - "geometry": { "coordinates": [ -71.10044, 42.373695 ], "type": "Point" }, - "properties": { - "address": "1369 Cambridge St", "id": 1, - "shop_bles": [ - { "id": 1, "name": "Beacon-1", "shop_id": 1 , - "coords": { "coordinates": [ -71.10044, 42.373695 ], "type": "Point" }, - "range_area": { - "coordinates": [ [ [ -71.10045254230499, 42.37387083326593 ], [ -71.10048070549963, 42.37377126199953 ], [ -71.10039688646793, 42.37375838212269 ], [ -71.10037006437777, 42.37385844878863 ], [ -71.10045254230499, 42.37387083326593 ] ] ], - "type": "Polygon" } - }, - { "coords": { "coordinates": [ -71.10044, 42.373695 ], "type": "Point" }, - "id": 2, "name": "Beacon-2", "shop_id": 1, - "range_area": { - "coordinates": [ [ [ -71.10034391283989, 42.37385299961788 ], [ -71.10036939382553, 42.373756895982865 ], [ -71.1002916097641, 42.373745997623224 ], [ -71.1002641171217, 42.37384408279195 ], [ -71.10034391283989, 42.37385299961788 ] ] ], - "type": "Polygon" } - } - ] - } - } - ] - }|]) + } + ] + }|] { matchHeaders = ["Content-Type" <:> "application/geo+json; charset=utf-8"] } it "works with RPC" $ @@ -219,14 +190,8 @@ spec actualPgVersion = describe "PostGIS features" $ it "gets the geojson geometry object with the regular application/json output" $ request methodGet "/shops?id=eq.1" [] "" `shouldRespondWith` - (if actualPgVersion >= pgVersion100 - then [json|[{ - "id":1,"address":"1369 Cambridge St", - "shop_geom":{"type":"Point","crs":{"type":"name","properties":{"name":"EPSG:4326"}},"coordinates":[-71.10044,42.373695]} - }]|] - else [json|[{ - "address": "1369 Cambridge St", - "id": 1, - "shop_geom": { "coordinates": [ -71.10044, 42.373695 ], "type": "Point" } - }]|]) + [json|[{ + "id":1,"address":"1369 Cambridge St", + "shop_geom":{"type":"Point","crs":{"type":"name","properties":{"name":"EPSG:4326"}},"coordinates":[-71.10044,42.373695]} + }]|] { matchHeaders = [matchContentTypeJson] } diff --git a/test/spec/Feature/Query/RpcSpec.hs b/test/spec/Feature/Query/RpcSpec.hs index 0e4af838e..c01150bfb 100644 --- a/test/spec/Feature/Query/RpcSpec.hs +++ b/test/spec/Feature/Query/RpcSpec.hs @@ -11,9 +11,9 @@ import Test.Hspec.Wai import Test.Hspec.Wai.JSON import Text.Heredoc -import PostgREST.Config.PgVersion (PgVersion, pgVersion100, - pgVersion109, pgVersion110, - pgVersion112, pgVersion114) +import PostgREST.Config.PgVersion (PgVersion, pgVersion109, + pgVersion110, pgVersion112, + pgVersion114) import Protolude hiding (get) import SpecHelper @@ -530,66 +530,34 @@ spec actualPgVersion = { matchHeaders = [matchContentTypeJson] } context "proc argument types" $ do - -- different syntax for array needed for pg<10 - when (actualPgVersion < pgVersion100) $ - it "accepts a variety of arguments (Postgres < 10)" $ - post "/rpc/varied_arguments" - [json| { - "double": 3.1, - "varchar": "hello", - "boolean": true, - "date": "20190101", - "money": 0, - "enum": "foo", - "arr": "{a,b,c}", - "integer": 43, - "json": {"some key": "some value"}, - "jsonb": {"another key": [1, 2, "3"]} - } |] - `shouldRespondWith` - [json| { - "double": 3.1, - "varchar": "hello", - "boolean": true, - "date": "2019-01-01", - "money": "$0.00", - "enum": "foo", - "arr": ["a", "b", "c"], - "integer": 43, - "json": {"some key": "some value"}, - "jsonb": {"another key": [1, 2, "3"]} - } |] - { matchHeaders = [matchContentTypeJson] } - - when (actualPgVersion >= pgVersion100) $ - it "accepts a variety of arguments (Postgres >= 10)" $ - post "/rpc/varied_arguments" - [json| { - "double": 3.1, - "varchar": "hello", - "boolean": true, - "date": "20190101", - "money": 0, - "enum": "foo", - "arr": ["a", "b", "c"], - "integer": 43, - "json": {"some key": "some value"}, - "jsonb": {"another key": [1, 2, "3"]} - } |] - `shouldRespondWith` - [json| { - "double": 3.1, - "varchar": "hello", - "boolean": true, - "date": "2019-01-01", - "money": "$0.00", - "enum": "foo", - "arr": ["a", "b", "c"], - "integer": 43, - "json": {"some key": "some value"}, - "jsonb": {"another key": [1, 2, "3"]} - } |] - { matchHeaders = [matchContentTypeJson] } + it "accepts a variety of arguments (Postgres >= 10)" $ + post "/rpc/varied_arguments" + [json| { + "double": 3.1, + "varchar": "hello", + "boolean": true, + "date": "20190101", + "money": 0, + "enum": "foo", + "arr": ["a", "b", "c"], + "integer": 43, + "json": {"some key": "some value"}, + "jsonb": {"another key": [1, 2, "3"]} + } |] + `shouldRespondWith` + [json| { + "double": 3.1, + "varchar": "hello", + "boolean": true, + "date": "2019-01-01", + "money": "$0.00", + "enum": "foo", + "arr": ["a", "b", "c"], + "integer": 43, + "json": {"some key": "some value"}, + "jsonb": {"another key": [1, 2, "3"]} + } |] + { matchHeaders = [matchContentTypeJson] } it "accepts a variety of arguments with GET" $ -- without JSON / JSONB here, because passing those via query string is useless - they just become a "json string" all the time @@ -635,14 +603,6 @@ spec actualPgVersion = [json|"object"|] { matchHeaders = [matchContentTypeJson] } - when (actualPgVersion < pgVersion100) $ - it "parses quoted JSON arguments as JSON (Postgres < 10)" $ - post "/rpc/json_argument" - [json| { "arg": "{ \"key\": 3 }" } |] - `shouldRespondWith` - [json|"object"|] - { matchHeaders = [matchContentTypeJson] } - when ((actualPgVersion >= pgVersion109 && actualPgVersion < pgVersion110) || actualPgVersion >= pgVersion114) $ it "parses quoted JSON arguments as JSON string (from Postgres 10.9, 11.4)" $ @@ -765,68 +725,59 @@ spec actualPgVersion = [json|[{"a": "A", "b": "B"}]|] context "procs with VARIADIC params" $ do - when (actualPgVersion < pgVersion100) $ - it "works with POST (Postgres < 10)" $ - post "/rpc/variadic_param" - [json| { "v": "{hi,hello,there}" } |] + it "works with POST (Postgres >= 10)" $ + post "/rpc/variadic_param" + [json| { "v": ["hi", "hello", "there"] } |] + `shouldRespondWith` + [json|["hi", "hello", "there"]|] + + context "works with GET and repeated params" $ do + it "n=0 (through DEFAULT)" $ + get "/rpc/variadic_param" `shouldRespondWith` - [json|["hi", "hello", "there"]|] + [json|[]|] - when (actualPgVersion >= pgVersion100) $ do - it "works with POST (Postgres >= 10)" $ - post "/rpc/variadic_param" - [json| { "v": ["hi", "hello", "there"] } |] + it "n=1" $ + get "/rpc/variadic_param?v=hi" `shouldRespondWith` - [json|["hi", "hello", "there"]|] + [json|["hi"]|] - context "works with GET and repeated params" $ do - it "n=0 (through DEFAULT)" $ - get "/rpc/variadic_param" - `shouldRespondWith` - [json|[]|] + it "n>1" $ + get "/rpc/variadic_param?v=hi&v=there" + `shouldRespondWith` + [json|["hi", "there"]|] - it "n=1" $ - get "/rpc/variadic_param?v=hi" - `shouldRespondWith` - [json|["hi"]|] + context "works with POST and repeated params from html form" $ do + it "n=0 (through DEFAULT)" $ + request methodPost "/rpc/variadic_param" + [("Content-Type", "application/x-www-form-urlencoded")] + "" + `shouldRespondWith` + [json|[]|] - it "n>1" $ - get "/rpc/variadic_param?v=hi&v=there" - `shouldRespondWith` - [json|["hi", "there"]|] + it "n=1" $ + request methodPost "/rpc/variadic_param" + [("Content-Type", "application/x-www-form-urlencoded")] + "v=hi" + `shouldRespondWith` + [json|["hi"]|] - context "works with POST and repeated params from html form" $ do - it "n=0 (through DEFAULT)" $ - request methodPost "/rpc/variadic_param" - [("Content-Type", "application/x-www-form-urlencoded")] - "" - `shouldRespondWith` - [json|[]|] - - it "n=1" $ - request methodPost "/rpc/variadic_param" - [("Content-Type", "application/x-www-form-urlencoded")] - "v=hi" - `shouldRespondWith` - [json|["hi"]|] - - it "n>1" $ - request methodPost "/rpc/variadic_param" - [("Content-Type", "application/x-www-form-urlencoded")] - "v=hi&v=there" - `shouldRespondWith` - [json|["hi", "there"]|] + it "n>1" $ + request methodPost "/rpc/variadic_param" + [("Content-Type", "application/x-www-form-urlencoded")] + "v=hi&v=there" + `shouldRespondWith` + [json|["hi", "there"]|] it "returns last value for repeated params without VARIADIC" $ get "/rpc/sayhello?name=ignored&name=world" `shouldRespondWith` [json|"Hello, world"|] - when (actualPgVersion >= pgVersion100) $ - it "returns last value for repeated non-variadic params in function with other VARIADIC arguments" $ - get "/rpc/sayhello_variadic?name=ignored&name=world&v=unused" - `shouldRespondWith` - [json|"Hello, world"|] + it "returns last value for repeated non-variadic params in function with other VARIADIC arguments" $ + get "/rpc/sayhello_variadic?name=ignored&name=world&v=unused" + `shouldRespondWith` + [json|"Hello, world"|] it "can handle procs with args that have a DEFAULT value" $ do get "/rpc/many_inout_params?num=1&str=two" diff --git a/test/spec/Feature/Query/UpdateSpec.hs b/test/spec/Feature/Query/UpdateSpec.hs index 68c09ebdb..3faaab451 100644 --- a/test/spec/Feature/Query/UpdateSpec.hs +++ b/test/spec/Feature/Query/UpdateSpec.hs @@ -7,14 +7,11 @@ import Network.HTTP.Types import Test.Hspec.Wai import Test.Hspec.Wai.JSON -import PostgREST.Config.PgVersion (PgVersion, pgVersion100) - - import Protolude hiding (get) import SpecHelper -spec :: PgVersion -> SpecWith ((), Application) -spec actualPgVersion = do +spec :: SpecWith ((), Application) +spec = do describe "Patching record" $ do context "to unknown uri" $ it "indicates no table found by returning 404" $ @@ -623,196 +620,93 @@ spec actualPgVersion = do } -- Data representations for payload parsing requires Postgres 10 or above. - when (actualPgVersion >= pgVersion100) $ do - describe "Data representations" $ do - context "for a single row" $ do - it "parses values in payload" $ - request methodPatch "/datarep_todos?id=eq.2" [("Prefer", "return=headers-only")] - [json| {"label_color": "#221100", "due_at": "2019-01-03T11:00:00Z"} |] - `shouldRespondWith` - "" - { matchStatus = 204 - , matchHeaders = [ matchHeaderAbsent hContentType - , "Content-Range" <:> "0-0/*"] - } + describe "Data representations" $ do + context "for a single row" $ do + it "parses values in payload" $ + request methodPatch "/datarep_todos_computed?id=eq.2" [("Prefer", "return=headers-only")] + [json| {"label_color": "#221100", "due_at": "2019-01-03T11:00:00Z"} |] + `shouldRespondWith` + "" + { matchStatus = 204 + , matchHeaders = [ matchHeaderAbsent hContentType + , "Content-Range" <:> "0-0/*" ] + } - it "parses values in payload and formats individually selected values in return=representation" $ - request methodPatch "/datarep_todos?id=eq.2&select=id,label_color" [("Prefer", "return=representation")] - [json| {"label_color": "#221100", "due_at": "2019-01-03T11:00:00Z"} |] - `shouldRespondWith` - [json| [{"id":2, "label_color": "#221100"}] |] - { matchStatus = 200 - , matchHeaders = ["Content-Type" <:> "application/json; charset=utf-8" - , "Content-Range" <:> "0-0/*" - , "Preference-Applied" <:> "return=representation"] - } + it "parses values in payload and formats individually selected values in return=representation" $ + request methodPatch "/datarep_todos_computed?id=eq.2&select=id,label_color" [("Prefer", "return=representation")] + [json| {"label_color": "#221100", "due_at": "2019-01-03T11:00:00Z"} |] + `shouldRespondWith` + [json| [{"id":2, "label_color": "#221100"}] |] + { matchStatus = 200 + , matchHeaders = ["Content-Type" <:> "application/json; charset=utf-8" + , "Content-Range" <:> "0-0/*" + , "Preference-Applied" <:> "return=representation"] + } - it "parses values in payload and formats values in return=representation" $ - request methodPatch "/datarep_todos?id=eq.2" [("Prefer", "return=representation")] - [json| {"label_color": "#221100", "due_at": "2019-01-03T11:00:20Z", "icon_image": "3q2+7w"} |] - `shouldRespondWith` - [json| [{"id":2,"name":"Essay","label_color":"#221100","due_at":"2019-01-03T11:00:20Z","icon_image":"3q2+7w==","created_at":1513213350,"budget":"100000000000000.13"}] |] - { matchStatus = 200 - , matchHeaders = ["Content-Type" <:> "application/json; charset=utf-8" - , "Content-Range" <:> "0-0/*" - , "Preference-Applied" <:> "return=representation"] - } + it "parses values in payload and formats values in return=representation" $ + request methodPatch "/datarep_todos_computed?id=eq.2" [("Prefer", "return=representation")] + [json| {"label_color": "#221100", "due_at": "2019-01-03T11:00:20Z"} |] + `shouldRespondWith` + [json| [{"id":2, "name": "Essay", "label_color": "#221100", "dark_color":"#110880", "due_at":"2019-01-03T11:00:20Z"}] |] + { matchStatus = 200 + , matchHeaders = ["Content-Type" <:> "application/json; charset=utf-8" + , "Content-Range" <:> "0-0/*" + , "Preference-Applied" <:> "return=representation"] + } + context "for multiple rows" $ do + it "parses values in payload and formats individually selected values in return=representation" $ + request methodPatch "/datarep_todos_computed?id=lt.4&select=id,name,label_color,dark_color" [("Prefer", "return=representation")] + [json| {"label_color": "#221100", "due_at": "2019-01-03T11:00:00Z"} |] + `shouldRespondWith` + [json| [ + {"id":1, "name": "Report", "label_color": "#221100", "dark_color":"#110880"}, + {"id":2, "name": "Essay", "label_color": "#221100", "dark_color":"#110880"}, + {"id":3, "name": "Algebra", "label_color": "#221100", "dark_color":"#110880"} + ] |] + { matchStatus = 200 + , matchHeaders = ["Content-Type" <:> "application/json; charset=utf-8" + , "Content-Range" <:> "0-2/*" + , "Preference-Applied" <:> "return=representation"] + } - it "parses values in payload and formats star mixed selected values in return=representation" $ - request methodPatch "/datarep_todos?id=eq.2&select=due_at,*" [("Prefer", "return=representation")] - [json| {"label_color": "#221100", "due_at": "2019-01-03T11:00:00Z", "created_at": 0} |] - `shouldRespondWith` - -- end up with due_at twice here but that's unrelated to data reps - [json| [{"due_at":"2019-01-03T11:00:00Z","id":2,"name":"Essay","label_color":"#221100","due_at":"2019-01-03T11:00:00Z","icon_image":null,"created_at":0,"budget":"100000000000000.13"}] |] - { matchStatus = 200 - , matchHeaders = ["Content-Type" <:> "application/json; charset=utf-8" - , "Content-Range" <:> "0-0/*" - , "Preference-Applied" <:> "return=representation"] - } - context "for multiple rows" $ do - it "parses values in payload and formats individually selected values in return=representation" $ - request methodPatch "/datarep_todos?id=lt.4&select=id,name,label_color" [("Prefer", "return=representation")] - [json| {"label_color": "#221100", "due_at": "2019-01-03T11:00:00Z"} |] - `shouldRespondWith` - [json| [ - {"id":1, "name": "Report", "label_color": "#221100"}, - {"id":2, "name": "Essay", "label_color": "#221100"}, - {"id":3, "name": "Algebra", "label_color": "#221100"} - ] |] - { matchStatus = 200 - , matchHeaders = ["Content-Type" <:> "application/json; charset=utf-8" - , "Content-Range" <:> "0-2/*" - , "Preference-Applied" <:> "return=representation"] - } + it "parses values in payload and formats values in return=representation" $ + request methodPatch "/datarep_todos_computed?id=lt.4" [("Prefer", "return=representation")] + [json| {"label_color": "#221100", "due_at": "2019-01-03T11:00:00Z"} |] + `shouldRespondWith` + [json| [ + {"id":1, "name": "Report", "label_color": "#221100", "dark_color":"#110880", "due_at":"2019-01-03T11:00:00Z"}, + {"id":2, "name": "Essay", "label_color": "#221100", "dark_color":"#110880", "due_at":"2019-01-03T11:00:00Z"}, + {"id":3, "name": "Algebra", "label_color": "#221100", "dark_color":"#110880", "due_at":"2019-01-03T11:00:00Z"} + ] |] + { matchStatus = 200 + , matchHeaders = ["Content-Type" <:> "application/json; charset=utf-8" + , "Content-Range" <:> "0-2/*" + , "Preference-Applied" <:> "return=representation"] + } + context "with ?columns parameter" $ do + it "ignores json keys not included in ?columns; parses only the ones specified" $ + request methodPatch "/datarep_todos_computed?id=eq.2&columns=due_at" [("Prefer", "return=representation")] + [json| {"due_at": "2019-01-03T11:00:00Z", "smth": "here", "label_color": "invalid", "fake_id": 13} |] + `shouldRespondWith` + [json| [ + {"id":2, "name": "Essay", "label_color": "#000100", "dark_color": "#000080", "due_at":"2019-01-03T11:00:00Z"} + ] |] + { matchStatus = 200 + , matchHeaders = ["Content-Type" <:> "application/json; charset=utf-8" + , "Content-Range" <:> "0-0/*" + , "Preference-Applied" <:> "return=representation"] + } - it "parses values in payload and formats values in return=representation" $ - request methodPatch "/datarep_todos?id=lt.4" [("Prefer", "return=representation")] - [json| {"label_color": "#221100", "due_at": "2019-01-03T11:00:00Z", "icon_image": "3q2+7w="} |] - `shouldRespondWith` - [json| [ - {"id":1,"name":"Report","label_color":"#221100","due_at":"2019-01-03T11:00:00Z","icon_image":"3q2+7w==","created_at":1513213350,"budget":"12.50"}, - {"id":2,"name":"Essay","label_color":"#221100","due_at":"2019-01-03T11:00:00Z","icon_image":"3q2+7w==","created_at":1513213350,"budget":"100000000000000.13"}, - {"id":3,"name":"Algebra","label_color":"#221100","due_at":"2019-01-03T11:00:00Z","icon_image":"3q2+7w==","created_at":1513213350,"budget":"0.00"} - ] |] - { matchStatus = 200 - , matchHeaders = ["Content-Type" <:> "application/json; charset=utf-8" - , "Content-Range" <:> "0-2/*" - , "Preference-Applied" <:> "return=representation"] - } - context "with ?columns parameter" $ do - it "ignores json keys not included in ?columns; parses only the ones specified" $ - request methodPatch "/datarep_todos?id=eq.2&columns=due_at" [("Prefer", "return=representation")] - [json| {"due_at": "2019-01-03T11:00:00Z", "smth": "here", "label_color": "invalid", "fake_id": 13} |] - `shouldRespondWith` - [json| [ - {"id":2,"name":"Essay","label_color":"#000100","due_at":"2019-01-03T11:00:00Z","icon_image":null,"created_at":1513213350,"budget":"100000000000000.13"} - ] |] - { matchStatus = 200 - , matchHeaders = ["Content-Type" <:> "application/json; charset=utf-8" - , "Content-Range" <:> "0-0/*" - , "Preference-Applied" <:> "return=representation"] - } + it "fails if at least one specified column doesn't exist" $ + request methodPatch "/datarep_todos_computed?id=eq.2&columns=label_color,helicopters" [("Prefer", "return=representation")] + [json| {"due_at": "2019-01-03T11:00:00Z", "smth": "here", "label_color": "invalid", "fake_id": 13} |] + `shouldRespondWith` + [json| {"code":"PGRST204","details":null,"hint":null,"message":"Could not find the 'helicopters' column of 'datarep_todos_computed' in the schema cache"} |] + { matchStatus = 400 + , matchHeaders = ["Content-Type" <:> "application/json; charset=utf-8"] + } - it "fails if at least one specified column doesn't exist" $ - request methodPatch "/datarep_todos?id=eq.2&columns=label_color,helicopters" [("Prefer", "return=representation")] - [json| {"due_at": "2019-01-03T11:00:00Z", "smth": "here", "label_color": "invalid", "fake_id": 13} |] - `shouldRespondWith` - [json| {"code":"PGRST204","details":null,"hint":null,"message":"Could not find the 'helicopters' column of 'datarep_todos' in the schema cache"} |] - { matchStatus = 400 - , matchHeaders = ["Content-Type" <:> "application/json; charset=utf-8"] - } - - it "ignores json keys and gives 200 if no record updated" $ - request methodPatch "/datarep_todos?id=eq.2001&columns=label_color" [("Prefer", "return=representation")] - [json| {"due_at": "2019-01-03T11:00:00Z", "smth": "here", "label_color": "invalid", "fake_id": 13} |] - `shouldRespondWith` 200 - context "on a view" $ do - context "for a single row" $ do - it "parses values in payload" $ - request methodPatch "/datarep_todos_computed?id=eq.2" [("Prefer", "return=headers-only")] - [json| {"label_color": "#221100", "due_at": "2019-01-03T11:00:00Z"} |] - `shouldRespondWith` - "" - { matchStatus = 204 - , matchHeaders = [ matchHeaderAbsent hContentType - , "Content-Range" <:> "0-0/*" ] - } - - it "parses values in payload and formats individually selected values in return=representation" $ - request methodPatch "/datarep_todos_computed?id=eq.2&select=id,label_color" [("Prefer", "return=representation")] - [json| {"label_color": "#221100", "due_at": "2019-01-03T11:00:00Z"} |] - `shouldRespondWith` - [json| [{"id":2, "label_color": "#221100"}] |] - { matchStatus = 200 - , matchHeaders = ["Content-Type" <:> "application/json; charset=utf-8" - , "Content-Range" <:> "0-0/*" - , "Preference-Applied" <:> "return=representation"] - } - - it "parses values in payload and formats values in return=representation" $ - request methodPatch "/datarep_todos_computed?id=eq.2" [("Prefer", "return=representation")] - [json| {"label_color": "#221100", "due_at": "2019-01-03T11:00:20Z"} |] - `shouldRespondWith` - [json| [{"id":2, "name": "Essay", "label_color": "#221100", "dark_color":"#110880", "due_at":"2019-01-03T11:00:20Z"}] |] - { matchStatus = 200 - , matchHeaders = ["Content-Type" <:> "application/json; charset=utf-8" - , "Content-Range" <:> "0-0/*" - , "Preference-Applied" <:> "return=representation"] - } - context "for multiple rows" $ do - it "parses values in payload and formats individually selected values in return=representation" $ - request methodPatch "/datarep_todos_computed?id=lt.4&select=id,name,label_color,dark_color" [("Prefer", "return=representation")] - [json| {"label_color": "#221100", "due_at": "2019-01-03T11:00:00Z"} |] - `shouldRespondWith` - [json| [ - {"id":1, "name": "Report", "label_color": "#221100", "dark_color":"#110880"}, - {"id":2, "name": "Essay", "label_color": "#221100", "dark_color":"#110880"}, - {"id":3, "name": "Algebra", "label_color": "#221100", "dark_color":"#110880"} - ] |] - { matchStatus = 200 - , matchHeaders = ["Content-Type" <:> "application/json; charset=utf-8" - , "Content-Range" <:> "0-2/*" - , "Preference-Applied" <:> "return=representation"] - } - - it "parses values in payload and formats values in return=representation" $ - request methodPatch "/datarep_todos_computed?id=lt.4" [("Prefer", "return=representation")] - [json| {"label_color": "#221100", "due_at": "2019-01-03T11:00:00Z"} |] - `shouldRespondWith` - [json| [ - {"id":1, "name": "Report", "label_color": "#221100", "dark_color":"#110880", "due_at":"2019-01-03T11:00:00Z"}, - {"id":2, "name": "Essay", "label_color": "#221100", "dark_color":"#110880", "due_at":"2019-01-03T11:00:00Z"}, - {"id":3, "name": "Algebra", "label_color": "#221100", "dark_color":"#110880", "due_at":"2019-01-03T11:00:00Z"} - ] |] - { matchStatus = 200 - , matchHeaders = ["Content-Type" <:> "application/json; charset=utf-8" - , "Content-Range" <:> "0-2/*" - , "Preference-Applied" <:> "return=representation"] - } - context "with ?columns parameter" $ do - it "ignores json keys not included in ?columns; parses only the ones specified" $ - request methodPatch "/datarep_todos_computed?id=eq.2&columns=due_at" [("Prefer", "return=representation")] - [json| {"due_at": "2019-01-03T11:00:00Z", "smth": "here", "label_color": "invalid", "fake_id": 13} |] - `shouldRespondWith` - [json| [ - {"id":2, "name": "Essay", "label_color": "#000100", "dark_color": "#000080", "due_at":"2019-01-03T11:00:00Z"} - ] |] - { matchStatus = 200 - , matchHeaders = ["Content-Type" <:> "application/json; charset=utf-8" - , "Content-Range" <:> "0-0/*" - , "Preference-Applied" <:> "return=representation"] - } - - it "fails if at least one specified column doesn't exist" $ - request methodPatch "/datarep_todos_computed?id=eq.2&columns=label_color,helicopters" [("Prefer", "return=representation")] - [json| {"due_at": "2019-01-03T11:00:00Z", "smth": "here", "label_color": "invalid", "fake_id": 13} |] - `shouldRespondWith` - [json| {"code":"PGRST204","details":null,"hint":null,"message":"Could not find the 'helicopters' column of 'datarep_todos_computed' in the schema cache"} |] - { matchStatus = 400 - , matchHeaders = ["Content-Type" <:> "application/json; charset=utf-8"] - } - - it "ignores json keys and gives 200 if no record updated" $ - request methodPatch "/datarep_todos_computed?id=eq.2001&columns=label_color" [("Prefer", "return=representation")] - [json| {"due_at": "2019-01-03T11:00:00Z", "smth": "here", "label_color": "invalid", "fake_id": 13} |] - `shouldRespondWith` 200 + it "ignores json keys and gives 200 if no record updated" $ + request methodPatch "/datarep_todos_computed?id=eq.2001&columns=label_color" [("Prefer", "return=representation")] + [json| {"due_at": "2019-01-03T11:00:00Z", "smth": "here", "label_color": "invalid", "fake_id": 13} |] + `shouldRespondWith` 200 diff --git a/test/spec/Main.hs b/test/spec/Main.hs index 407264335..be982d93a 100644 --- a/test/spec/Main.hs +++ b/test/spec/Main.hs @@ -162,7 +162,7 @@ main = do , ("Feature.Query.RpcSpec" , Feature.Query.RpcSpec.spec actualPgVersion) , ("Feature.Query.SingularSpec" , Feature.Query.SingularSpec.spec) , ("Feature.Query.SpreadQueriesSpec" , Feature.Query.SpreadQueriesSpec.spec) - , ("Feature.Query.UpdateSpec" , Feature.Query.UpdateSpec.spec actualPgVersion) + , ("Feature.Query.UpdateSpec" , Feature.Query.UpdateSpec.spec) , ("Feature.Query.UpsertSpec" , Feature.Query.UpsertSpec.spec actualPgVersion) ] @@ -228,7 +228,7 @@ main = do -- this test runs with an extra search path parallel $ before extraSearchPathApp $ do describe "Feature.ExtraSearchPathSpec" Feature.ExtraSearchPathSpec.spec - describe "Feature.Query.PostGISSpec" $ Feature.Query.PostGISSpec.spec actualPgVersion + describe "Feature.Query.PostGISSpec" Feature.Query.PostGISSpec.spec -- this test runs with a root spec function override parallel $ before rootSpecApp $ diff --git a/test/spec/fixtures/data.sql b/test/spec/fixtures/data.sql index 68a83a230..b02a05bc4 100644 --- a/test/spec/fixtures/data.sql +++ b/test/spec/fixtures/data.sql @@ -680,14 +680,12 @@ INSERT INTO private.films (id, title) VALUES (12,'douze commandements'), (2001,' TRUNCATE TABLE private.personnages CASCADE; INSERT INTO private.personnages (film_id, role_id, character) VALUES (12,1,'méchant'), (2001,2,'astronaute'); -DO $do$BEGIN - IF (SELECT current_setting('server_version_num')::INT >= 100000) THEN - INSERT INTO test.car_models(name, year) VALUES ('DeLorean',1981); - INSERT INTO test.car_models(name, year) VALUES ('F310-B',1997); - INSERT INTO test.car_models(name, year) VALUES ('Veneno',2013); - INSERT INTO test.car_models(name, year) VALUES ('Murcielago',2001); - END IF; +INSERT INTO test.car_models(name, year) VALUES ('DeLorean',1981); +INSERT INTO test.car_models(name, year) VALUES ('F310-B',1997); +INSERT INTO test.car_models(name, year) VALUES ('Veneno',2013); +INSERT INTO test.car_models(name, year) VALUES ('Murcielago',2001); +DO $do$BEGIN IF (SELECT current_setting('server_version_num')::INT >= 110000) THEN INSERT INTO test.car_brands(name) VALUES ('DMC'); INSERT INTO test.car_brands(name) VALUES ('Ferrari'); diff --git a/test/spec/fixtures/privileges.sql b/test/spec/fixtures/privileges.sql index e5fc79e9f..c4df1b02d 100644 --- a/test/spec/fixtures/privileges.sql +++ b/test/spec/fixtures/privileges.sql @@ -41,13 +41,7 @@ GRANT USAGE ON SEQUENCE , leak_id_seq TO postgrest_test_anonymous; -DO $do$ -BEGIN - IF current_setting('server_version_num')::INT >= 100000 THEN - GRANT USAGE ON SEQUENCE channels_id_seq TO postgrest_test_anonymous; - END IF; -END -$do$; +GRANT USAGE ON SEQUENCE channels_id_seq TO postgrest_test_anonymous; -- Privileges for non anonymous users GRANT USAGE ON SCHEMA test TO postgrest_test_author; diff --git a/test/spec/fixtures/schema.sql b/test/spec/fixtures/schema.sql index fdb472bbd..2fcb81b56 100644 --- a/test/spec/fixtures/schema.sql +++ b/test/spec/fixtures/schema.sql @@ -2289,26 +2289,22 @@ create table private.rollen ( ); -- Tables used for testing embedding between partitioned tables +create table test.car_models( + name varchar(64) not null, + year int not null +) partition by list (year); -do $do$begin - -- partitioned tables using the PARTITION syntax are supported from pg v10 - if (select current_setting('server_version_num')::int >= 100000) then - create table test.car_models( - name varchar(64) not null, - year int not null - ) partition by list (year); - - comment on table test.car_models is - $$A partitioned table +comment on table test.car_models is +$$A partitioned table A test for partitioned tables$$; - create table test.car_models_2021 partition of test.car_models - for values in (2021); - create table test.car_models_default partition of test.car_models - for values in (1981,1997,2001,2013); - end if; +create table test.car_models_2021 partition of test.car_models + for values in (2021); +create table test.car_models_default partition of test.car_models + for values in (1981,1997,2001,2013); +do $do$begin -- primary keys for partitioned tables are supported from pg v11 if (select current_setting('server_version_num')::int >= 110000) then create table test.car_brands ( @@ -2492,22 +2488,15 @@ BEGIN END$$; -- This view is not used in any requests but just parsed by the pfkSourceColumns query. --- XMLTABLE is only supported from PG 10 on -DO $do$ -BEGIN - IF current_setting('server_version_num')::INT >= 100000 THEN - CREATE VIEW test.xml AS - SELECT * - FROM (SELECT ''::xml AS data) _, - XMLTABLE( - '' - PASSING data - COLUMNS id int PATH '@id', - premier_name text PATH 'PREMIER_NAME' DEFAULT 'not specified' - ); - END IF; -END -$do$; +CREATE VIEW test.xml AS +SELECT * + FROM (SELECT ''::xml AS data) _, + XMLTABLE( + '' + PASSING data + COLUMNS id int PATH '@id', + premier_name text PATH 'PREMIER_NAME' DEFAULT 'not specified' + ); -- https://github.com/PostgREST/postgrest/issues/1543 CREATE TYPE complex AS ( @@ -3277,17 +3266,11 @@ create table test.tbl_w_json( data json ); -DO $do$ -BEGIN - IF current_setting('server_version_num')::INT >= 100000 THEN - CREATE TABLE test.channels ( - id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, - data jsonb DEFAULT '{"foo": "bar"}', - slug text - ); - END IF; -END -$do$; +CREATE TABLE test.channels ( + id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + data jsonb DEFAULT '{"foo": "bar"}', + slug text +); CREATE FUNCTION test.is_superuser() RETURNS boolean LANGUAGE sql