diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 282ece310..92b8dab1b 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -67,7 +67,7 @@ jobs: strategy: fail-fast: false matrix: - pgVersion: [11, 12, 13, 14, 15, 16] + pgVersion: [12, 13, 14, 15, 16] name: PG ${{ matrix.pgVersion }} runs-on: ubuntu-22.04 defaults: diff --git a/CHANGELOG.md b/CHANGELOG.md index 72da9e4e8..1df02ca29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). - #2052, Dropped support for PostgreSQL 9.6 - @wolfgangwalther - #2052, Dropped support for PostgreSQL 10 - @wolfgangwalther + - #2052, Dropped support for PostgreSQL 11 - @wolfgangwalther ## [12.2.0] - 2024-06-11 diff --git a/default.nix b/default.nix index 70900caf9..83e9f3daa 100644 --- a/default.nix +++ b/default.nix @@ -36,9 +36,7 @@ let allOverlays.checked-shell-script allOverlays.gitignore allOverlays.postgresql-libpq - allOverlays.postgresql-legacy allOverlays.postgresql-future - allOverlays.postgis (allOverlays.haskell-packages { inherit compiler; }) allOverlays.slocat ]; @@ -54,7 +52,6 @@ let { name = "postgresql-14"; postgresql = pkgs.postgresql_14.withPackages (p: [ p.postgis p.pg_safeupdate ]); } { name = "postgresql-13"; postgresql = pkgs.postgresql_13.withPackages (p: [ p.postgis p.pg_safeupdate ]); } { name = "postgresql-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 ]); } ]; # Dynamic derivation for PostgREST diff --git a/docs/explanations/install.rst b/docs/explanations/install.rst index e711a9371..894450ebf 100644 --- a/docs/explanations/install.rst +++ b/docs/explanations/install.rst @@ -16,16 +16,11 @@ Supported PostgreSQL versions ============================= =============== ================================= -**Supported** PostgreSQL >= 11 +**Supported** PostgreSQL >= 12 =============== ================================= -PostgREST works with all PostgreSQL versions starting from 11. +PostgREST works with all PostgreSQL versions still `officially supported `_. -.. note:: - - Support for PostgreSQL version 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/. Running PostgREST ================= diff --git a/nix/README.md b/nix/README.md index 424e18ddc..e88984b1c 100644 --- a/nix/README.md +++ b/nix/README.md @@ -75,12 +75,12 @@ The PostgREST utilities available in `nix-shell` all have names that begin with postgrest-build postgrest-test-spec postgrest-check postgrest-watch postgrest-clean postgrest-with-all -postgrest-coverage postgrest-with-postgresql-11 -postgrest-lint postgrest-with-postgresql-12 -postgrest-run postgrest-with-postgresql-13 -postgrest-style postgrest-with-postgresql-14 -postgrest-style-check postgrest-with-postgresql-15 -postgrest-test-io postgrest-with-postgresql-16 +postgrest-coverage postgrest-with-postgresql-12 +postgrest-lint postgrest-with-postgresql-13 +postgrest-run postgrest-with-postgresql-14 +postgrest-style postgrest-with-postgresql-15 +postgrest-style-check postgrest-with-postgresql-16 +postgrest-test-io ... [nix-shell]$ @@ -99,12 +99,12 @@ $ nix-shell --arg memory true postgrest-build postgrest-test-spec postgrest-check postgrest-watch postgrest-clean postgrest-with-all -postgrest-coverage postgrest-with-postgresql-11 -postgrest-lint postgrest-with-postgresql-12 -postgrest-run postgrest-with-postgresql-13 -postgrest-style postgrest-with-postgresql-14 -postgrest-style-check postgrest-with-postgresql-15 -postgrest-test-io postgrest-with-postgresql-16 +postgrest-coverage postgrest-with-postgresql-12 +postgrest-lint postgrest-with-postgresql-13 +postgrest-run postgrest-with-postgresql-14 +postgrest-style postgrest-with-postgresql-15 +postgrest-style-check postgrest-with-postgresql-16 +postgrest-test-io postgrest-test-memory ... diff --git a/nix/overlays/default.nix b/nix/overlays/default.nix index 434ccc72d..32e03c741 100644 --- a/nix/overlays/default.nix +++ b/nix/overlays/default.nix @@ -3,9 +3,7 @@ checked-shell-script = import ./checked-shell-script; gitignore = import ./gitignore.nix; haskell-packages = import ./haskell-packages.nix; - postgis = import ./postgis.nix; postgresql-libpq = import ./postgresql-libpq.nix; - postgresql-legacy = import ./postgresql-legacy.nix; postgresql-future = import ./postgresql-future.nix; slocat = import ./slocat.nix; } diff --git a/nix/overlays/postgis.nix b/nix/overlays/postgis.nix deleted file mode 100644 index 73040da28..000000000 --- a/nix/overlays/postgis.nix +++ /dev/null @@ -1,18 +0,0 @@ -final: prev: -let - postgis_3_2_3 = rec { - version = "3.2.3"; - src = final.fetchurl { - url = "https://download.osgeo.org/postgis/source/postgis-${version}.tar.gz"; - sha256 = "sha256-G02LXHVuWrpZ77wYM7Iu/k1lYneO7KVvpJf+susTZow="; - }; - meta.broken = false; - }; -in -{ - postgresql_11 = prev.postgresql_11.override { this = final.postgresql_11; } // { - pkgs = prev.postgresql_11.pkgs // { - postgis = prev.postgresql_11.pkgs.postgis.overrideAttrs (_: postgis_3_2_3); - }; - }; -} diff --git a/nix/overlays/postgresql-legacy.nix b/nix/overlays/postgresql-legacy.nix deleted file mode 100644 index 4b90d3d60..000000000 --- a/nix/overlays/postgresql-legacy.nix +++ /dev/null @@ -1,19 +0,0 @@ -_: _: -# Overlay that adds legacy versions of PostgreSQL that are supported by -# PostgREST. -{ - # PostgreSQL 11 was removed from Nixpkgs with - # https://github.com/NixOS/nixpkgs/commit/1220a4d4dd1a4590780a5e1c18d1333a121be366 - # We pin its parent commit to get the last version that was available. - postgresql_11 = - let - rev = "f5458516e42cc5cb4123cc2d93f45c240548aa18"; - tarballHash = "1h03621sxfhw4z6ya74k6c2lyx3z7pvf2jcg4vs7i01yz2m6w3cv"; - pinnedPkgs = - builtins.fetchTarball { - url = "https://github.com/nixos/nixpkgs/archive/${rev}.tar.gz"; - sha256 = tarballHash; - }; - in - (import pinnedPkgs { }).pkgs.postgresql_11; -} diff --git a/src/PostgREST/Config/PgVersion.hs b/src/PostgREST/Config/PgVersion.hs index 73b2f81bd..fd4952a87 100644 --- a/src/PostgREST/Config/PgVersion.hs +++ b/src/PostgREST/Config/PgVersion.hs @@ -3,9 +3,6 @@ module PostgREST.Config.PgVersion ( PgVersion(..) , minimumPgVersion - , pgVersion112 - , pgVersion114 - , pgVersion120 , pgVersion121 , pgVersion130 , pgVersion140 @@ -29,16 +26,7 @@ instance Ord PgVersion where -- | Tells the minimum PostgreSQL version required by this version of PostgREST minimumPgVersion :: PgVersion -minimumPgVersion = pgVersion110 - -pgVersion110 :: PgVersion -pgVersion110 = PgVersion 110000 "11.0" "11.0" - -pgVersion112 :: PgVersion -pgVersion112 = PgVersion 110002 "11.2" "11.2" - -pgVersion114 :: PgVersion -pgVersion114 = PgVersion 110004 "11.4" "11.4" +minimumPgVersion = pgVersion120 pgVersion120 :: PgVersion pgVersion120 = PgVersion 120000 "12.0" "12.0" diff --git a/src/PostgREST/SchemaCache.hs b/src/PostgREST/SchemaCache.hs index a4aeee10d..4dd231914 100644 --- a/src/PostgREST/SchemaCache.hs +++ b/src/PostgREST/SchemaCache.hs @@ -45,9 +45,7 @@ import Text.InterpolatedString.Perl6 (q) import PostgREST.Config (AppConfig (..)) import PostgREST.Config.Database (TimezoneNames, - pgVersionStatement, toIsolationLevel) -import PostgREST.Config.PgVersion (PgVersion, pgVersion120) import PostgREST.SchemaCache.Identifiers (AccessSet, FieldName, QualifiedIdentifier (..), RelIdentifier (..), @@ -144,8 +142,7 @@ type SqlQuery = ByteString querySchemaCache :: AppConfig -> SQL.Transaction SchemaCache querySchemaCache AppConfig{..} = do SQL.sql "set local schema ''" -- This voids the search path. The following queries need this for getting the fully qualified name(schema.name) of every db object - pgVer <- SQL.statement mempty $ pgVersionStatement prepared - tabs <- SQL.statement schemas $ allTables pgVer prepared + tabs <- SQL.statement schemas $ allTables prepared keyDeps <- SQL.statement (schemas, configDbExtraSearchPath) $ allViewsKeyDependencies prepared m2oRels <- SQL.statement mempty $ allM2OandO2ORels prepared funcs <- SQL.statement (schemas, configDbHoistedTxSettings) $ allFunctions prepared @@ -602,15 +599,13 @@ addViewPrimaryKeys tabs keyDeps = -- * We need to choose a single reference for each column, otherwise we'd output too many columns in location headers etc. takeFirstPK = mapMaybe (head . snd) -allTables :: PgVersion -> Bool -> SQL.Statement [Schema] TablesMap -allTables pgVer = - SQL.Statement sql (arrayParam HE.text) decodeTables - where - sql = tablesSqlQuery pgVer +allTables :: Bool -> SQL.Statement [Schema] TablesMap +allTables = + SQL.Statement tablesSqlQuery (arrayParam HE.text) decodeTables -- | Gets tables with their PK cols -tablesSqlQuery :: PgVersion -> SqlQuery -tablesSqlQuery pgVer = +tablesSqlQuery :: SqlQuery +tablesSqlQuery = -- the tbl_constraints/key_col_usage CTEs are based on the standard "information_schema.table_constraints"/"information_schema.key_column_usage" views, -- we cannot use those directly as they include the following privilege filter: -- (pg_has_role(ss.relowner, 'USAGE'::text) OR has_column_privilege(ss.roid, a.attnum, 'SELECT, INSERT, UPDATE, REFERENCES'::text)); @@ -624,7 +619,13 @@ tablesSqlQuery pgVer = c.relname::name AS table_name, a.attname::name AS column_name, d.description AS description, - |] <> columnDefault <> [q| AS column_default, + -- typbasetype and typdefaultbin handles `CREATE DOMAIN .. DEFAULT val`, attidentity/attgenerated handles generated columns, pg_get_expr gets the default of a column + 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)) + WHEN a.attgenerated = 's' THEN null + ELSE pg_get_expr(ad.adbin, ad.adrelid)::text + END AS column_default, not (a.attnotnull OR t.typtype = 'd' AND t.typnotnull) AS is_nullable, CASE WHEN t.typtype = 'd' THEN @@ -810,21 +811,6 @@ tablesSqlQuery pgVer = AND n.nspname NOT IN ('pg_catalog', 'information_schema') AND not c.relispartition ORDER BY table_schema, table_name|] - where - 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 - 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)) - WHEN a.attgenerated = 's' THEN null - ELSE pg_get_expr(ad.adbin, ad.adrelid)::text - END|] - | 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|] -- | Gets many-to-one relationships and one-to-one(O2O) relationships, which are a refinement of the many-to-one's allM2OandO2ORels :: Bool -> SQL.Statement () [Relationship] diff --git a/test/spec/Feature/Auth/AuthSpec.hs b/test/spec/Feature/Auth/AuthSpec.hs index fa0f17362..2b5f264ae 100644 --- a/test/spec/Feature/Auth/AuthSpec.hs +++ b/test/spec/Feature/Auth/AuthSpec.hs @@ -7,30 +7,20 @@ import Test.Hspec import Test.Hspec.Wai import Test.Hspec.Wai.JSON -import PostgREST.Config.PgVersion (PgVersion, pgVersion112) - import Protolude hiding (get) import SpecHelper -spec :: PgVersion -> SpecWith ((), Application) -spec actualPgVersion = describe "authorization" $ do +spec :: SpecWith ((), Application) +spec = describe "authorization" $ do let single = ("Accept","application/vnd.pgrst.object+json") it "denies access to tables that anonymous does not own" $ - get "/authors_only" `shouldRespondWith` ( - if actualPgVersion >= pgVersion112 then - [json| { - "hint":null, - "details":null, - "code":"42501", - "message":"permission denied for table authors_only"} |] - else - [json| { - "hint":null, - "details":null, - "code":"42501", - "message":"permission denied for relation authors_only"} |] - ) + get "/authors_only" `shouldRespondWith` + [json| { + "hint":null, + "details":null, + "code":"42501", + "message":"permission denied for table authors_only"} |] { matchStatus = 401 , matchHeaders = ["WWW-Authenticate" <:> "Bearer"] } @@ -38,20 +28,12 @@ spec actualPgVersion = describe "authorization" $ do it "denies access to tables that postgrest_test_author does not own" $ let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIn0.Xod-F15qsGL0WhdOCr2j3DdKuTw9QJERVgoFD3vGaWA" in request methodGet "/private_table" [auth] "" - `shouldRespondWith` ( - if actualPgVersion >= pgVersion112 then - [json| { - "hint":null, - "details":null, - "code":"42501", - "message":"permission denied for table private_table"} |] - else - [json| { - "hint":null, - "details":null, - "code":"42501", - "message":"permission denied for relation private_table"} |] - ) + `shouldRespondWith` + [json| { + "hint":null, + "details":null, + "code":"42501", + "message":"permission denied for table private_table"} |] { matchStatus = 403 , matchHeaders = [] } diff --git a/test/spec/Feature/Query/AndOrParamsSpec.hs b/test/spec/Feature/Query/AndOrParamsSpec.hs index aa6e50516..f3a8882f8 100644 --- a/test/spec/Feature/Query/AndOrParamsSpec.hs +++ b/test/spec/Feature/Query/AndOrParamsSpec.hs @@ -7,13 +7,11 @@ import Test.Hspec import Test.Hspec.Wai import Test.Hspec.Wai.JSON -import PostgREST.Config.PgVersion (PgVersion, pgVersion112) - import Protolude hiding (get) import SpecHelper -spec :: PgVersion -> SpecWith ((), Application) -spec actualPgVersion = +spec :: SpecWith ((), Application) +spec = describe "and/or params used for complex boolean logic" $ do context "used with GET" $ do context "or param" $ do @@ -96,17 +94,16 @@ spec actualPgVersion = get "/entities?and=(id.gte.2,arr.isdistinct.{1,2})&select=id" `shouldRespondWith` [json|[{ "id": 3 }, { "id": 4 }]|] { matchHeaders = [matchContentTypeJson] } - when (actualPgVersion >= pgVersion112) $ - it "can handle wfts (websearch_to_tsquery)" $ - get "/tsearch?or=(text_search_vector.plfts(german).Art,text_search_vector.plfts(french).amusant,text_search_vector.not.wfts(english).impossible)" - `shouldRespondWith` - [json|[ - {"text_search_vector": "'also':2 'fun':3 'possibl':8" }, - {"text_search_vector": "'ate':3 'cat':2 'fat':1 'rat':4" }, - {"text_search_vector": "'amus':5 'fair':7 'impossibl':9 'peu':4" }, - {"text_search_vector": "'art':4 'spass':5 'unmog':7" } - ]|] - { matchHeaders = [matchContentTypeJson] } + it "can handle wfts (websearch_to_tsquery)" $ + get "/tsearch?or=(text_search_vector.plfts(german).Art,text_search_vector.plfts(french).amusant,text_search_vector.not.wfts(english).impossible)" + `shouldRespondWith` + [json|[ + {"text_search_vector": "'also':2 'fun':3 'possibl':8" }, + {"text_search_vector": "'ate':3 'cat':2 'fat':1 'rat':4" }, + {"text_search_vector": "'amus':5 'fair':7 'impossibl':9 'peu':4" }, + {"text_search_vector": "'art':4 'spass':5 'unmog':7" } + ]|] + { matchHeaders = [matchContentTypeJson] } it "can handle cs and cd" $ get "/entities?or=(arr.cs.{1,2,3},arr.cd.{1})&select=id" `shouldRespondWith` diff --git a/test/spec/Feature/Query/InsertSpec.hs b/test/spec/Feature/Query/InsertSpec.hs index 475507db1..405ef62f9 100644 --- a/test/spec/Feature/Query/InsertSpec.hs +++ b/test/spec/Feature/Query/InsertSpec.hs @@ -11,8 +11,7 @@ import Test.Hspec.Wai import Test.Hspec.Wai.JSON import Text.Heredoc -import PostgREST.Config.PgVersion (PgVersion, pgVersion112, - pgVersion120, pgVersion130, +import PostgREST.Config.PgVersion (PgVersion, pgVersion130, pgVersion140) import Protolude hiding (get) @@ -542,28 +541,27 @@ spec actualPgVersion = do , matchHeaders = ["Preference-Applied" <:> "missing=default, return=representation"] } - when (actualPgVersion >= pgVersion120) $ - it "fails with a good error message on generated always columns" $ - request methodPost "/foo?columns=a,b" [("Prefer", "return=representation"), ("Prefer", "missing=default")] - [json| [ - {"a": "val"}, - {"a": "val", "b": "val"} - ]|] - `shouldRespondWith` - (if actualPgVersion < pgVersion140 - then [json| { - "code": "42601", - "details": "Column \"b\" is a generated column.", - "hint": null, - "message": "cannot insert into column \"b\"" - }|] - else [json| { - "code": "428C9", - "details": "Column \"b\" is a generated column.", - "hint": null, - "message": "cannot insert a non-DEFAULT value into column \"b\"" - }|]) - { matchStatus = 400 } + it "fails with a good error message on generated always columns" $ + request methodPost "/foo?columns=a,b" [("Prefer", "return=representation"), ("Prefer", "missing=default")] + [json| [ + {"a": "val"}, + {"a": "val", "b": "val"} + ]|] + `shouldRespondWith` + (if actualPgVersion < pgVersion140 + then [json| { + "code": "42601", + "details": "Column \"b\" is a generated column.", + "hint": null, + "message": "cannot insert into column \"b\"" + }|] + else [json| { + "code": "428C9", + "details": "Column \"b\" is a generated column.", + "hint": null, + "message": "cannot insert a non-DEFAULT value into column \"b\"" + }|]) + { matchStatus = 400 } it "inserts a default on a DOMAIN with default" $ request methodPost "/evil_friends?columns=id,name" [("Prefer", "return=representation"), ("Prefer", "missing=default")] @@ -710,24 +708,16 @@ spec actualPgVersion = do it "fails inserting if more columns are selected" $ request methodPost "/limited_article_stars?select=article_id,user_id,created_at" [("Prefer", "return=representation")] - [json| {"article_id": 2, "user_id": 2} |] `shouldRespondWith` ( - if actualPgVersion >= pgVersion112 then + [json| {"article_id": 2, "user_id": 2} |] `shouldRespondWith` [json|{"hint":null,"details":null,"code":"42501","message":"permission denied for view limited_article_stars"}|] - else - [json|{"hint":null,"details":null,"code":"42501","message":"permission denied for relation limited_article_stars"}|] - ) { matchStatus = 401 , matchHeaders = [] } it "fails inserting if select is not specified" $ request methodPost "/limited_article_stars" [("Prefer", "return=representation")] - [json| {"article_id": 3, "user_id": 1} |] `shouldRespondWith` ( - if actualPgVersion >= pgVersion112 then + [json| {"article_id": 3, "user_id": 1} |] `shouldRespondWith` [json|{"hint":null,"details":null,"code":"42501","message":"permission denied for view limited_article_stars"}|] - else - [json|{"hint":null,"details":null,"code":"42501","message":"permission denied for relation limited_article_stars"}|] - ) { matchStatus = 401 , matchHeaders = [] } diff --git a/test/spec/Feature/Query/JsonOperatorSpec.hs b/test/spec/Feature/Query/JsonOperatorSpec.hs index fea74093f..8dbdd8455 100644 --- a/test/spec/Feature/Query/JsonOperatorSpec.hs +++ b/test/spec/Feature/Query/JsonOperatorSpec.hs @@ -7,8 +7,7 @@ import Test.Hspec import Test.Hspec.Wai import Test.Hspec.Wai.JSON -import PostgREST.Config.PgVersion (PgVersion, pgVersion112, - pgVersion121) +import PostgREST.Config.PgVersion (PgVersion, pgVersion121) import Protolude hiding (get) import SpecHelper @@ -75,28 +74,16 @@ spec actualPgVersion = describe "json and jsonb operators" $ do -- this works fine for /rpc/unexistent requests, but for this case a 500 seems more appropriate it "fails when a double arrow ->> is followed with a single arrow ->" $ do get "/json_arr?select=data->>c->1" - `shouldRespondWith` ( - if actualPgVersion >= pgVersion112 then + `shouldRespondWith` [json| {"hint":"No operator matches the given name and argument types. You might need to add explicit type casts.", "details":null,"code":"42883","message":"operator does not exist: text -> integer"} |] - else - [json| - {"hint":"No operator matches the given name and argument type(s). You might need to add explicit type casts.", - "details":null,"code":"42883","message":"operator does not exist: text -> integer"} |] - ) { matchStatus = 404 , matchHeaders = [] } get "/json_arr?select=data->>c->b" - `shouldRespondWith` ( - if actualPgVersion >= pgVersion112 then + `shouldRespondWith` [json| {"hint":"No operator matches the given name and argument types. You might need to add explicit type casts.", "details":null,"code":"42883","message":"operator does not exist: text -> unknown"} |] - else - [json| - {"hint":"No operator matches the given name and argument type(s). You might need to add explicit type casts.", - "details":null,"code":"42883","message":"operator does not exist: text -> unknown"} |] - ) { matchStatus = 404 , matchHeaders = [] } context "with array index" $ do diff --git a/test/spec/Feature/Query/PlanSpec.hs b/test/spec/Feature/Query/PlanSpec.hs index d9e78d770..182ac99a5 100644 --- a/test/spec/Feature/Query/PlanSpec.hs +++ b/test/spec/Feature/Query/PlanSpec.hs @@ -15,8 +15,7 @@ import Test.Hspec hiding (pendingWith) import Test.Hspec.Wai import Test.Hspec.Wai.JSON -import PostgREST.Config.PgVersion (PgVersion, pgVersion120, - pgVersion130) +import PostgREST.Config.PgVersion (PgVersion, pgVersion130) import Protolude hiding (get) import SpecHelper @@ -34,10 +33,7 @@ spec actualPgVersion = do liftIO $ do resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; for=\"application/json\"; charset=utf-8") resStatus `shouldBe` Status { statusCode = 200, statusMessage="OK" } - totalCost `shouldBe` - if actualPgVersion > pgVersion120 - then 15.63 - else 15.69 + totalCost `shouldBe` 15.63 it "outputs the total cost for a single filter on a view" $ do r <- request methodGet "/projects_view?id=gt.2" @@ -50,10 +46,7 @@ spec actualPgVersion = do liftIO $ do resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; for=\"application/json\"; charset=utf-8") resStatus `shouldBe` Status { statusCode = 200, statusMessage="OK" } - totalCost `shouldBe` - if actualPgVersion > pgVersion120 - then 24.28 - else 32.27 + totalCost `shouldBe` 24.28 it "outputs blocks info when using the buffers option" $ if actualPgVersion >= pgVersion130 @@ -77,21 +70,20 @@ spec actualPgVersion = do resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; for=\"application/json\"; options=analyze|buffers; charset=utf-8") blocks `shouldBe` Just [aesonQQ| 1.0 |] - when (actualPgVersion >= pgVersion120) $ - it "outputs the search path when using the settings option" $ do - r <- request methodGet "/projects" (acceptHdrs "application/vnd.pgrst.plan+json; options=settings") "" + it "outputs the search path when using the settings option" $ do + r <- request methodGet "/projects" (acceptHdrs "application/vnd.pgrst.plan+json; options=settings") "" - let searchPath = simpleBody r ^? nth 0 . key "Settings" - resHeaders = simpleHeaders r + let searchPath = simpleBody r ^? nth 0 . key "Settings" + resHeaders = simpleHeaders r - liftIO $ do - resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; for=\"application/json\"; options=settings; charset=utf-8") - searchPath `shouldBe` - Just [aesonQQ| - { - "search_path": "\"test\"" - } - |] + liftIO $ do + resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; for=\"application/json\"; options=settings; charset=utf-8") + searchPath `shouldBe` + Just [aesonQQ| + { + "search_path": "\"test\"" + } + |] when (actualPgVersion >= pgVersion130) $ it "outputs WAL info when using the wal option" $ do @@ -123,9 +115,7 @@ spec actualPgVersion = do liftIO $ do resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; for=\"application/json\"; options=verbose; charset=utf-8") aggCol `shouldBe` - if actualPgVersion >= pgVersion120 - then Just [aesonQQ| "COALESCE(json_agg(ROW(projects.id, projects.name, projects.client_id)), '[]'::json)" |] - else Just [aesonQQ| "COALESCE(json_agg(ROW(pgrst_source.id, pgrst_source.name, pgrst_source.client_id)), '[]'::json)" |] + Just [aesonQQ| "COALESCE(json_agg(ROW(projects.id, projects.name, projects.client_id)), '[]'::json)" |] it "outputs the plan for application/vnd.pgrst.object " $ do r <- request methodGet "/projects_view" (acceptHdrs "application/vnd.pgrst.plan+json; for=\"application/vnd.pgrst.object\"; options=verbose") "" @@ -136,9 +126,7 @@ spec actualPgVersion = do liftIO $ do resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; for=\"application/vnd.pgrst.object+json\"; options=verbose; charset=utf-8") aggCol `shouldBe` - if actualPgVersion >= pgVersion120 - then Just [aesonQQ| "COALESCE((json_agg(ROW(projects.id, projects.name, projects.client_id)) -> 0), 'null'::json)" |] - else Just [aesonQQ| "COALESCE((json_agg(ROW(pgrst_source.id, pgrst_source.name, pgrst_source.client_id)) -> 0), 'null'::json)" |] + Just [aesonQQ| "COALESCE((json_agg(ROW(projects.id, projects.name, projects.client_id)) -> 0), 'null'::json)" |] describe "writes plans" $ do it "outputs the total cost for an insert" $ do @@ -452,11 +440,7 @@ spec actualPgVersion = do liftIO $ do resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; for=\"application/vnd.twkb\"; options=verbose; charset=utf-8") aggCol `shouldBe` - ( - if actualPgVersion >= pgVersion120 - then Just [aesonQQ| "twkb_agg(ROW(lines.id, lines.name, lines.geom)::lines)" |] - else Just [aesonQQ| "twkb_agg(ROW(pgrst_source.id, pgrst_source.name, pgrst_source.geom)::lines)" |] - ) + Just [aesonQQ| "twkb_agg(ROW(lines.id, lines.name, lines.geom)::lines)" |] disabledSpec :: SpecWith ((), Application) disabledSpec = diff --git a/test/spec/Feature/Query/QuerySpec.hs b/test/spec/Feature/Query/QuerySpec.hs index e738dc9d7..f181440f4 100644 --- a/test/spec/Feature/Query/QuerySpec.hs +++ b/test/spec/Feature/Query/QuerySpec.hs @@ -8,8 +8,7 @@ import Test.Hspec hiding (pendingWith) import Test.Hspec.Wai import Test.Hspec.Wai.JSON -import PostgREST.Config.PgVersion (PgVersion, pgVersion112, - pgVersion121) +import PostgREST.Config.PgVersion (PgVersion, pgVersion121) import Protolude hiding (get) import SpecHelper @@ -176,28 +175,27 @@ spec actualPgVersion = do [json| [ {"text_search_vector": "'ate':3 'cat':2 'fat':1 'rat':4" }] |] { matchHeaders = [matchContentTypeJson] } - when (actualPgVersion >= pgVersion112) $ do - it "finds matches with websearch_to_tsquery" $ - get "/tsearch?text_search_vector=wfts.The%20Fat%20Rats" `shouldRespondWith` - [json| [ {"text_search_vector": "'ate':3 'cat':2 'fat':1 'rat':4" }] |] - { matchHeaders = [matchContentTypeJson] } + it "finds matches with websearch_to_tsquery" $ + get "/tsearch?text_search_vector=wfts.The%20Fat%20Rats" `shouldRespondWith` + [json| [ {"text_search_vector": "'ate':3 'cat':2 'fat':1 'rat':4" }] |] + { matchHeaders = [matchContentTypeJson] } - it "can use boolean operators(and, or, -) in websearch_to_tsquery" $ do - get "/tsearch?text_search_vector=wfts.fun%20and%20possible" - `shouldRespondWith` - [json| [ {"text_search_vector": "'also':2 'fun':3 'possibl':8"}] |] - { matchHeaders = [matchContentTypeJson] } - get "/tsearch?text_search_vector=wfts.impossible%20or%20possible" - `shouldRespondWith` - [json| [ - {"text_search_vector": "'fun':5 'imposs':9 'kind':3"}, - {"text_search_vector": "'also':2 'fun':3 'possibl':8"}] - |] - { matchHeaders = [matchContentTypeJson] } - get "/tsearch?text_search_vector=wfts.fun%20and%20-possible" - `shouldRespondWith` - [json| [ {"text_search_vector": "'fun':5 'imposs':9 'kind':3"}] |] - { matchHeaders = [matchContentTypeJson] } + it "can use boolean operators(and, or, -) in websearch_to_tsquery" $ do + get "/tsearch?text_search_vector=wfts.fun%20and%20possible" + `shouldRespondWith` + [json| [ {"text_search_vector": "'also':2 'fun':3 'possibl':8"}] |] + { matchHeaders = [matchContentTypeJson] } + get "/tsearch?text_search_vector=wfts.impossible%20or%20possible" + `shouldRespondWith` + [json| [ + {"text_search_vector": "'fun':5 'imposs':9 'kind':3"}, + {"text_search_vector": "'also':2 'fun':3 'possibl':8"}] + |] + { matchHeaders = [matchContentTypeJson] } + get "/tsearch?text_search_vector=wfts.fun%20and%20-possible" + `shouldRespondWith` + [json| [ {"text_search_vector": "'fun':5 'imposs':9 'kind':3"}] |] + { matchHeaders = [matchContentTypeJson] } it "finds matches with different dictionaries" $ do get "/tsearch?text_search_vector=fts(french).amusant" `shouldRespondWith` @@ -207,11 +205,10 @@ spec actualPgVersion = do [json| [{"text_search_vector": "'amus':5 'fair':7 'impossibl':9 'peu':4" }] |] { matchHeaders = [matchContentTypeJson] } - when (actualPgVersion >= pgVersion112) $ - get "/tsearch?text_search_vector=wfts(french).amusant%20impossible" - `shouldRespondWith` - [json| [{"text_search_vector": "'amus':5 'fair':7 'impossibl':9 'peu':4" }] |] - { matchHeaders = [matchContentTypeJson] } + get "/tsearch?text_search_vector=wfts(french).amusant%20impossible" + `shouldRespondWith` + [json| [{"text_search_vector": "'amus':5 'fair':7 'impossibl':9 'peu':4" }] |] + { matchHeaders = [matchContentTypeJson] } it "can be negated with not operator" $ do get "/tsearch?text_search_vector=not.fts.impossible%7Cfat%7Cfun" `shouldRespondWith` @@ -231,13 +228,12 @@ spec actualPgVersion = do {"text_search_vector": "'amus':5 'fair':7 'impossibl':9 'peu':4"}, {"text_search_vector": "'art':4 'spass':5 'unmog':7"}]|] { matchHeaders = [matchContentTypeJson] } - when (actualPgVersion >= pgVersion112) $ - get "/tsearch?text_search_vector=not.wfts(english).impossible%20or%20fat%20or%20fun" - `shouldRespondWith` - [json| [ - {"text_search_vector": "'amus':5 'fair':7 'impossibl':9 'peu':4"}, - {"text_search_vector": "'art':4 'spass':5 'unmog':7"}]|] - { matchHeaders = [matchContentTypeJson] } + get "/tsearch?text_search_vector=not.wfts(english).impossible%20or%20fat%20or%20fun" + `shouldRespondWith` + [json| [ + {"text_search_vector": "'amus':5 'fair':7 'impossibl':9 'peu':4"}, + {"text_search_vector": "'art':4 'spass':5 'unmog':7"}]|] + { matchHeaders = [matchContentTypeJson] } context "Use of the phraseto_tsquery function" $ do it "finds matches" $ diff --git a/test/spec/Feature/Query/RpcSpec.hs b/test/spec/Feature/Query/RpcSpec.hs index abe522420..7f03db3ac 100644 --- a/test/spec/Feature/Query/RpcSpec.hs +++ b/test/spec/Feature/Query/RpcSpec.hs @@ -11,14 +11,11 @@ import Test.Hspec.Wai import Test.Hspec.Wai.JSON import Text.Heredoc -import PostgREST.Config.PgVersion (PgVersion, pgVersion112, - pgVersion114) - import Protolude hiding (get) import SpecHelper -spec :: PgVersion -> SpecWith ((), Application) -spec actualPgVersion = +spec :: SpecWith ((), Application) +spec = describe "remote procedure call" $ do context "a proc that returns a set" $ do context "returns paginated results" $ do @@ -599,13 +596,12 @@ spec actualPgVersion = [json|"object"|] { matchHeaders = [matchContentTypeJson] } - when (actualPgVersion >= pgVersion114) $ - it "parses quoted JSON arguments as JSON string (from Postgres 10.9, 11.4)" $ - post "/rpc/json_argument" - [json| { "arg": "{ \"key\": 3 }" } |] - `shouldRespondWith` - [json|"string"|] - { matchHeaders = [matchContentTypeJson] } + it "parses quoted JSON arguments as JSON string (from Postgres 10.9, 11.4)" $ + post "/rpc/json_argument" + [json| { "arg": "{ \"key\": 3 }" } |] + `shouldRespondWith` + [json|"string"|] + { matchHeaders = [matchContentTypeJson] } context "improper input" $ do it "rejects unknown content type even if payload is good" $ do @@ -1019,10 +1015,9 @@ spec actualPgVersion = get "/rpc/get_tsearch?text_search_vector=not.fts(english).fun%7Crat" `shouldRespondWith` [json|[{"text_search_vector":"'amus':5 'fair':7 'impossibl':9 'peu':4"},{"text_search_vector":"'art':4 'spass':5 'unmog':7"}]|] { matchHeaders = [matchContentTypeJson] } - when (actualPgVersion >= pgVersion112) $ - get "/rpc/get_tsearch?text_search_vector=wfts.impossible" `shouldRespondWith` - [json|[{"text_search_vector":"'fun':5 'imposs':9 'kind':3"}]|] - { matchHeaders = [matchContentTypeJson] } + get "/rpc/get_tsearch?text_search_vector=wfts.impossible" `shouldRespondWith` + [json|[{"text_search_vector":"'fun':5 'imposs':9 'kind':3"}]|] + { matchHeaders = [matchContentTypeJson] } it "should work with the phraseto_tsquery function" $ get "/rpc/get_tsearch?text_search_vector=phfts(english).impossible" `shouldRespondWith` diff --git a/test/spec/Main.hs b/test/spec/Main.hs index fde51a4c1..332d0542c 100644 --- a/test/spec/Main.hs +++ b/test/spec/Main.hs @@ -137,14 +137,14 @@ main = do analyzeTable "child_entities" specs = uncurry describe <$> [ - ("Feature.Auth.AuthSpec" , Feature.Auth.AuthSpec.spec actualPgVersion) + ("Feature.Auth.AuthSpec" , Feature.Auth.AuthSpec.spec) , ("Feature.ConcurrentSpec" , Feature.ConcurrentSpec.spec) , ("Feature.CorsSpec" , Feature.CorsSpec.spec) , ("Feature.CustomMediaSpec" , Feature.Query.CustomMediaSpec.spec) , ("Feature.NoSuperuserSpec" , Feature.NoSuperuserSpec.spec) , ("Feature.OpenApi.OpenApiSpec" , Feature.OpenApi.OpenApiSpec.spec) , ("Feature.OptionsSpec" , Feature.OptionsSpec.spec) - , ("Feature.Query.AndOrParamsSpec" , Feature.Query.AndOrParamsSpec.spec actualPgVersion) + , ("Feature.Query.AndOrParamsSpec" , Feature.Query.AndOrParamsSpec.spec) , ("Feature.Query.ComputedRelsSpec" , Feature.Query.ComputedRelsSpec.spec) , ("Feature.Query.DeleteSpec" , Feature.Query.DeleteSpec.spec) , ("Feature.Query.EmbedDisambiguationSpec" , Feature.Query.EmbedDisambiguationSpec.spec) @@ -159,7 +159,7 @@ main = do , ("Feature.Query.QuerySpec" , Feature.Query.QuerySpec.spec actualPgVersion) , ("Feature.Query.RawOutputTypesSpec" , Feature.Query.RawOutputTypesSpec.spec) , ("Feature.Query.RelatedQueriesSpec" , Feature.Query.RelatedQueriesSpec.spec) - , ("Feature.Query.RpcSpec" , Feature.Query.RpcSpec.spec actualPgVersion) + , ("Feature.Query.RpcSpec" , Feature.Query.RpcSpec.spec) , ("Feature.Query.SingularSpec" , Feature.Query.SingularSpec.spec) , ("Feature.Query.SpreadQueriesSpec" , Feature.Query.SpreadQueriesSpec.spec) , ("Feature.Query.UpdateSpec" , Feature.Query.UpdateSpec.spec) diff --git a/test/spec/fixtures/data.sql b/test/spec/fixtures/data.sql index aef27d1f9..3a5af3862 100644 --- a/test/spec/fixtures/data.sql +++ b/test/spec/fixtures/data.sql @@ -694,23 +694,19 @@ UPDATE test.car_models SET car_brand_name = 'Ferrari' WHERE name = 'F310-B'; UPDATE test.car_models SET car_brand_name = 'Lamborghini' WHERE name = 'Veneno'; UPDATE test.car_models SET car_brand_name = 'Lamborghini' WHERE name = 'Murcielago'; -DO $do$BEGIN - IF (SELECT current_setting('server_version_num')::INT >= 120000) THEN - INSERT INTO test.car_model_sales(date, quantity, car_model_name, car_model_year) VALUES ('2021-01-14',7,'DeLorean',1981); - INSERT INTO test.car_model_sales(date, quantity, car_model_name, car_model_year) VALUES ('2021-01-15',9,'DeLorean',1981); - INSERT INTO test.car_model_sales(date, quantity, car_model_name, car_model_year) VALUES ('2021-02-11',1,'Murcielago',2001); - INSERT INTO test.car_model_sales(date, quantity, car_model_name, car_model_year) VALUES ('2021-02-12',3,'Murcielago',2001); +INSERT INTO test.car_model_sales(date, quantity, car_model_name, car_model_year) VALUES ('2021-01-14',7,'DeLorean',1981); +INSERT INTO test.car_model_sales(date, quantity, car_model_name, car_model_year) VALUES ('2021-01-15',9,'DeLorean',1981); +INSERT INTO test.car_model_sales(date, quantity, car_model_name, car_model_year) VALUES ('2021-02-11',1,'Murcielago',2001); +INSERT INTO test.car_model_sales(date, quantity, car_model_name, car_model_year) VALUES ('2021-02-12',3,'Murcielago',2001); - INSERT INTO test.car_racers(name) VALUES ('Alain Prost'); - INSERT INTO test.car_racers(name, car_model_name, car_model_year) VALUES ('Michael Schumacher', 'F310-B', 1997); +INSERT INTO test.car_racers(name) VALUES ('Alain Prost'); +INSERT INTO test.car_racers(name, car_model_name, car_model_year) VALUES ('Michael Schumacher', 'F310-B', 1997); - INSERT INTO test.car_dealers(name,city) VALUES ('Springfield Cars S.A.','Springfield'); - INSERT INTO test.car_dealers(name,city) VALUES ('The Best Deals S.A.','Franklin'); +INSERT INTO test.car_dealers(name,city) VALUES ('Springfield Cars S.A.','Springfield'); +INSERT INTO test.car_dealers(name,city) VALUES ('The Best Deals S.A.','Franklin'); - INSERT INTO test.car_models_car_dealers(car_model_name, car_model_year, car_dealer_name, car_dealer_city, quantity) VALUES ('DeLorean',1981,'Springfield Cars S.A.','Springfield',15); - INSERT INTO test.car_models_car_dealers(car_model_name, car_model_year, car_dealer_name, car_dealer_city, quantity) VALUES ('Murcielago',2001,'The Best Deals S.A.','Franklin',2); - END IF; -END$do$; +INSERT INTO test.car_models_car_dealers(car_model_name, car_model_year, car_dealer_name, car_dealer_city, quantity) VALUES ('DeLorean',1981,'Springfield Cars S.A.','Springfield',15); +INSERT INTO test.car_models_car_dealers(car_model_name, car_model_year, car_dealer_name, car_dealer_city, quantity) VALUES ('Murcielago',2001,'The Best Deals S.A.','Franklin',2); TRUNCATE TABLE test.products CASCADE; INSERT INTO test.products (id, name) VALUES (1,'product-1'), (2,'product-2'), (3,'product-3'); diff --git a/test/spec/fixtures/schema.sql b/test/spec/fixtures/schema.sql index 3ad0505d3..d20c36889 100644 --- a/test/spec/fixtures/schema.sql +++ b/test/spec/fixtures/schema.sql @@ -2301,61 +2301,56 @@ create table test.car_brands ( alter table test.car_models add primary key (name, year); alter table test.car_models add column car_brand_name varchar(64) references test.car_brands(name); -do $do$begin - -- foreign keys referencing partitioned tables are supported from pg v12 - if (select current_setting('server_version_num')::int >= 120000) then - create table test.car_model_sales( - date varchar(64) not null, - quantity int not null, - car_model_name varchar(64), - car_model_year int, - primary key (date, car_model_name, car_model_year), - foreign key (car_model_name, car_model_year) references test.car_models (name, year) - ) partition by range (date); +create table test.car_model_sales( + date varchar(64) not null, + quantity int not null, + car_model_name varchar(64), + car_model_year int, + primary key (date, car_model_name, car_model_year), + foreign key (car_model_name, car_model_year) references test.car_models (name, year) +) partition by range (date); - create table test.car_model_sales_202101 partition of test.car_model_sales - for values from ('2021-01-01') to ('2021-01-31'); +create table test.car_model_sales_202101 partition of test.car_model_sales + for values from ('2021-01-01') to ('2021-01-31'); - create table test.car_model_sales_default partition of test.car_model_sales - default; +create table test.car_model_sales_default partition of test.car_model_sales + default; - create table test.car_racers ( - name varchar(64) not null primary key, - car_model_name varchar(64), - car_model_year int, - foreign key (car_model_name, car_model_year) references test.car_models (name, year) - ); +create table test.car_racers ( + name varchar(64) not null primary key, + car_model_name varchar(64), + car_model_year int, + foreign key (car_model_name, car_model_year) references test.car_models (name, year) +); - create table test.car_dealers ( - name varchar(64) not null, - city varchar(64) not null, - primary key (name, city) - ) partition by list (city); +create table test.car_dealers ( + name varchar(64) not null, + city varchar(64) not null, + primary key (name, city) +) partition by list (city); - create table test.car_dealers_springfield partition of test.car_dealers - for values in ('Springfield'); +create table test.car_dealers_springfield partition of test.car_dealers + for values in ('Springfield'); - create table test.car_dealers_default partition of test.car_dealers - default; +create table test.car_dealers_default partition of test.car_dealers + default; - create table test.car_models_car_dealers ( - car_model_name varchar(64) not null, - car_model_year int not null, - car_dealer_name varchar(64) not null, - car_dealer_city varchar(64) not null, - quantity int not null, - foreign key (car_model_name, car_model_year) references test.car_models (name, year), - foreign key (car_dealer_name, car_dealer_city) references test.car_dealers (name, city), - primary key (car_model_name, car_model_year, car_dealer_name, car_dealer_city, quantity) - ) partition by range (quantity); +create table test.car_models_car_dealers ( + car_model_name varchar(64) not null, + car_model_year int not null, + car_dealer_name varchar(64) not null, + car_dealer_city varchar(64) not null, + quantity int not null, + foreign key (car_model_name, car_model_year) references test.car_models (name, year), + foreign key (car_dealer_name, car_dealer_city) references test.car_dealers (name, city), + primary key (car_model_name, car_model_year, car_dealer_name, car_dealer_city, quantity) +) partition by range (quantity); - create table test.car_models_car_dealers_10to20 partition of test.car_models_car_dealers - for values from (10) to (20); +create table test.car_models_car_dealers_10to20 partition of test.car_models_car_dealers + for values from (10) to (20); - create table test.car_models_car_dealers_default partition of test.car_models_car_dealers - default; - end if; -end$do$; +create table test.car_models_car_dealers_default partition of test.car_models_car_dealers + default; create or replace function test.unnamed_json_param(json) returns json as $$ select $1; @@ -3261,21 +3256,15 @@ AS $$ select current_setting('is_superuser')::boolean; $$; -DO $do$ -BEGIN - IF current_setting('server_version_num')::INT >= 120000 THEN - CREATE TABLE test.foo ( - a text, - b text GENERATED ALWAYS AS ( - case WHEN a = 'telegram' THEN 'im' - WHEN a = 'proton' THEN 'email' - WHEN a = 'infinity' THEN 'idea' - ELSE 'bad idea' - end) stored - ); - END IF; -END -$do$; +CREATE TABLE test.foo ( + a text, + b text GENERATED ALWAYS AS ( + case WHEN a = 'telegram' THEN 'im' + WHEN a = 'proton' THEN 'email' + WHEN a = 'infinity' THEN 'idea' + ELSE 'bad idea' + end) stored +); create domain devil_int as int default 666;