diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 5840b17372..92b8dab1bc 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: [12, 13, 14, 15, 16] name: PG ${{ matrix.pgVersion }} runs-on: ubuntu-22.04 defaults: diff --git a/CHANGELOG.md b/CHANGELOG.md index ceb7caca96..1df02ca29e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ This project adheres to [Semantic Versioning](http://semver.org/). ## Unreleased +### Changed + + - #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 ### Added diff --git a/default.nix b/default.nix index a55011e2aa..551e0bf486 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,9 +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 ]); } - { 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 e6374dac48..8415a6ea9c 100644 --- a/docs/explanations/install.rst +++ b/docs/explanations/install.rst @@ -3,7 +3,7 @@ Installation ############ -The release page has `pre-compiled binaries for macOS, Windows, Linux and FreeBSD `_ . +The release page has `pre-compiled binaries for macOS, Windows, Linux and FreeBSD `_. The Linux binary is a static executable that can be run on any Linux distribution. You can also use your OS package manager. @@ -16,16 +16,11 @@ Supported PostgreSQL versions ============================= =============== ================================= -**Supported** PostgreSQL >= 9.6 +**Supported** PostgreSQL >= 12 =============== ================================= -PostgREST works with all PostgreSQL versions starting from 9.6. +PostgREST works with all PostgreSQL versions still `officially supported `_. -.. 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. - - See https://www.postgresql.org/support/versioning/. Running PostgREST ================= diff --git a/nix/README.md b/nix/README.md index 94804b1fd5..e88984b1cf 100644 --- a/nix/README.md +++ b/nix/README.md @@ -75,11 +75,11 @@ 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-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-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 ... @@ -99,11 +99,11 @@ $ nix-shell --arg memory true postgrest-build postgrest-test-spec postgrest-check postgrest-watch postgrest-clean postgrest-with-all -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-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 434ccc72d8..32e03c7415 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 26b5e226e4..0000000000 --- a/nix/overlays/postgis.nix +++ /dev/null @@ -1,28 +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); - }; - }; - postgresql_10 = prev.postgresql_10.override { this = final.postgresql_11; } // { - pkgs = prev.postgresql_10.pkgs // { - 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 deleted file mode 100644 index 506d8eb342..0000000000 --- a/nix/overlays/postgresql-legacy.nix +++ /dev/null @@ -1,49 +0,0 @@ -_: _: -# 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. - postgresql_10 = - let - rev = "79661ba7e2fb96ebefbb537458a5bbae9dc5bd1a"; - tarballHash = "0rn796pfn4sg90ai9fdnwmr10a2s835p1arazzgz46h6s5cxvq97"; - pinnedPkgs = - builtins.fetchTarball { - url = "https://github.com/nixos/nixpkgs/archive/${rev}.tar.gz"; - sha256 = tarballHash; - }; - in - (import pinnedPkgs { }).pkgs.postgresql_10; - - # 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 921824453d..e1387b8f31 100644 --- a/src/PostgREST/Config/PgVersion.hs +++ b/src/PostgREST/Config/PgVersion.hs @@ -3,14 +3,6 @@ module PostgREST.Config.PgVersion ( PgVersion(..) , minimumPgVersion - , pgVersion96 - , pgVersion100 - , pgVersion109 - , pgVersion110 - , pgVersion112 - , pgVersion114 - , pgVersion120 - , pgVersion121 , pgVersion130 , pgVersion140 , pgVersion150 @@ -33,28 +25,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" - -pgVersion100 :: PgVersion -pgVersion100 = PgVersion 100000 "10" "10" - -pgVersion109 :: PgVersion -pgVersion109 = PgVersion 100009 "10.9" "10.9" - -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" - -pgVersion120 :: PgVersion -pgVersion120 = PgVersion 120000 "12.0" "12.0" +minimumPgVersion = pgVersion121 pgVersion121 :: PgVersion pgVersion121 = PgVersion 120001 "12.1" "12.1" diff --git a/src/PostgREST/Query.hs b/src/PostgREST/Query.hs index ab6ddf8cd5..cf9833e16c 100644 --- a/src/PostgREST/Query.hs +++ b/src/PostgREST/Query.hs @@ -175,13 +175,13 @@ actionQuery (DbCall plan@CallReadPlan{..}) conf@AppConfig{..} apiReq@ApiRequest{ failExceedsMaxAffectedPref (preferMaxAffected,preferHandling) resultSet pure $ DbCallResult plan resultSet -actionQuery (MaybeDb plan@InspectPlan{ipSchema=tSchema}) AppConfig{..} _ pgVer sCache = +actionQuery (MaybeDb plan@InspectPlan{ipSchema=tSchema}) AppConfig{..} _ _ 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) + <$> SQL.statement (tSchema, configDbHoistedTxSettings) (SchemaCache.accessibleFuncs configDbPreparedStatements) <*> SQL.statement tSchema (SchemaCache.schemaDescription configDbPreparedStatements)) OAIgnorePriv -> MaybeDbResult plan . Just <$> ((,,) diff --git a/src/PostgREST/Query/QueryBuilder.hs b/src/PostgREST/Query/QueryBuilder.hs index 0d51c55484..1729f0dce4 100644 --- a/src/PostgREST/Query/QueryBuilder.hs +++ b/src/PostgREST/Query/QueryBuilder.hs @@ -23,8 +23,7 @@ import Data.Maybe (fromJust) import Data.Tree (Tree (..)) import PostgREST.ApiRequest.Preferences (PreferResolution (..)) -import PostgREST.Config.PgVersion (PgVersion, pgVersion110, - pgVersion130) +import PostgREST.Config.PgVersion (PgVersion, pgVersion130) import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..)) import PostgREST.SchemaCache.Relationship (Cardinality (..), Junction (..), @@ -202,9 +201,9 @@ callPlanToQuery (FunctionCall qi params args returnsScalar returnsSetOfScalar re "LATERAL " <> callIt (fmtParams prms) callIt :: SQL.Snippet -> SQL.Snippet - callIt argument | pgVer < pgVersion130 && pgVer >= pgVersion110 && returnsCompositeAlias = "(SELECT (" <> fromQi qi <> "(" <> argument <> ")).*) pgrst_call" - | returnsScalar || returnsSetOfScalar = "(SELECT " <> fromQi qi <> "(" <> argument <> ") pgrst_scalar) pgrst_call" - | otherwise = fromQi qi <> "(" <> argument <> ") pgrst_call" + callIt argument | pgVer < pgVersion130 && returnsCompositeAlias = "(SELECT (" <> fromQi qi <> "(" <> argument <> ")).*) pgrst_call" + | returnsScalar || returnsSetOfScalar = "(SELECT " <> fromQi qi <> "(" <> argument <> ") pgrst_scalar) pgrst_call" + | otherwise = fromQi qi <> "(" <> argument <> ") pgrst_call" fmtParams :: [RoutineParam] -> SQL.Snippet fmtParams prms = intercalateSnippet ", " diff --git a/src/PostgREST/SchemaCache.hs b/src/PostgREST/SchemaCache.hs index b6dca5ba14..4dd231914f 100644 --- a/src/PostgREST/SchemaCache.hs +++ b/src/PostgREST/SchemaCache.hs @@ -45,11 +45,7 @@ import Text.InterpolatedString.Perl6 (q) import PostgREST.Config (AppConfig (..)) import PostgREST.Config.Database (TimezoneNames, - pgVersionStatement, toIsolationLevel) -import PostgREST.Config.PgVersion (PgVersion, pgVersion100, - pgVersion110, - pgVersion120) import PostgREST.SchemaCache.Identifiers (AccessSet, FieldName, QualifiedIdentifier (..), RelIdentifier (..), @@ -146,14 +142,13 @@ 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 pgVer prepared - funcs <- SQL.statement (schemas, configDbHoistedTxSettings) $ allFunctions pgVer prepared + m2oRels <- SQL.statement mempty $ allM2OandO2ORels prepared + funcs <- SQL.statement (schemas, configDbHoistedTxSettings) $ allFunctions prepared cRels <- SQL.statement mempty $ allComputedRels prepared reps <- SQL.statement schemas $ dataRepresentations prepared - mHdlers <- SQL.statement schemas $ mediaHandlers pgVer prepared + mHdlers <- SQL.statement schemas $ mediaHandlers prepared tzones <- SQL.statement mempty $ timezones prepared _ <- let sleepCall = SQL.Statement "select pg_sleep($1 / 1000.0)" (param HE.int4) HD.noResult prepared in @@ -363,18 +358,18 @@ dataRepresentations = SQL.Statement sql (arrayParam HE.text) decodeRepresentatio OR (dst_t.typtype = 'd' AND c.castsource IN ('json'::regtype::oid , 'text'::regtype::oid))) |] -allFunctions :: PgVersion -> Bool -> SQL.Statement ([Schema], [Text]) RoutineMap -allFunctions pgVer = SQL.Statement sql (contrazip2 (arrayParam HE.text) (arrayParam HE.text)) decodeFuncs +allFunctions :: Bool -> SQL.Statement ([Schema], [Text]) RoutineMap +allFunctions = SQL.Statement sql (contrazip2 (arrayParam HE.text) (arrayParam HE.text)) decodeFuncs where - sql = funcsSqlQuery pgVer <> " AND pn.nspname = ANY($1)" + sql = funcsSqlQuery <> " AND pn.nspname = ANY($1)" -accessibleFuncs :: PgVersion -> Bool -> SQL.Statement (Schema, [Text]) RoutineMap -accessibleFuncs pgVer = SQL.Statement sql (contrazip2 (param HE.text) (arrayParam HE.text)) decodeFuncs +accessibleFuncs :: Bool -> SQL.Statement (Schema, [Text]) RoutineMap +accessibleFuncs = SQL.Statement sql (contrazip2 (param HE.text) (arrayParam HE.text)) decodeFuncs where - sql = funcsSqlQuery pgVer <> " AND pn.nspname = $1 AND has_function_privilege(p.oid, 'execute')" + sql = funcsSqlQuery <> " AND pn.nspname = $1 AND has_function_privilege(p.oid, 'execute')" -funcsSqlQuery :: PgVersion -> SqlQuery -funcsSqlQuery pgVer = [q| +funcsSqlQuery :: SqlQuery +funcsSqlQuery = [q| -- Recursively get the base types of domains WITH base_types AS ( @@ -462,7 +457,7 @@ funcsSqlQuery pgVer = [q| WHERE setting ~ ANY($2) ) func_settings ON TRUE WHERE t.oid <> 'trigger'::regtype AND COALESCE(a.callable, true) -|] <> (if pgVer >= pgVersion110 then "AND prokind = 'f'" else "AND NOT (proisagg OR proiswindow)") + AND prokind = 'f'|] schemaDescription :: Bool -> SQL.Statement Schema (Maybe Text) schemaDescription = @@ -477,8 +472,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 +489,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. @@ -605,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)); @@ -627,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,34 +808,13 @@ 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" - 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 - 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|] - | pgVer >= pgVersion100 = [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|] + AND n.nspname NOT IN ('pg_catalog', 'information_schema') + AND not c.relispartition + ORDER BY table_schema, table_name|] -- | 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] -allM2OandO2ORels pgVer = +allM2OandO2ORels :: Bool -> SQL.Statement () [Relationship] +allM2OandO2ORels = SQL.Statement sql HE.noParams decodeRels where -- We use jsonb_agg for comparing the uniques/pks instead of array_agg to avoid the ERROR: cannot accumulate arrays of different dimensionality @@ -883,11 +860,8 @@ allM2OandO2ORels pgVer = JOIN pg_namespace ns2 ON ns2.oid = other.relnamespace LEFT JOIN pks_uniques_cols pks_uqs ON pks_uqs.connamespace = traint.connamespace AND pks_uqs.conrelid = traint.conrelid WHERE traint.contype = 'f' - |] <> - (if pgVer >= pgVersion110 - then " and traint.conparentid = 0 " - else mempty) <> - "ORDER BY traint.conrelid, traint.conname" + AND traint.conparentid = 0 + ORDER BY traint.conrelid, traint.conname|] allComputedRels :: Bool -> SQL.Statement () [Relationship] allComputedRels = @@ -1140,8 +1114,8 @@ initialMediaHandlers = HM.insert (RelAnyElement, MediaType.MTGeoJSON ) (BuiltinOvAggGeoJson, MediaType.MTGeoJSON) HM.empty -mediaHandlers :: PgVersion -> Bool -> SQL.Statement [Schema] MediaHandlerMap -mediaHandlers pgVer = +mediaHandlers :: Bool -> SQL.Statement [Schema] MediaHandlerMap +mediaHandlers = SQL.Statement sql (arrayParam HE.text) decodeMediaHandlers where sql = [q| @@ -1203,7 +1177,7 @@ mediaHandlers pgVer = join pg_namespace typ_sch on typ_sch.oid = mtype.typnamespace where pro_sch.nspname = ANY($1) and NOT proretset - |] <> (if pgVer >= pgVersion110 then " AND prokind = 'f'" else " AND NOT (proisagg OR proiswindow)") + and prokind = 'f'|] decodeMediaHandlers :: HD.Result MediaHandlerMap decodeMediaHandlers = diff --git a/test/spec/Feature/Auth/AuthSpec.hs b/test/spec/Feature/Auth/AuthSpec.hs index fa0f173626..2b5f264ae7 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/OpenApi/OpenApiSpec.hs b/test/spec/Feature/OpenApi/OpenApiSpec.hs index 0fc29508b5..215c4b5ac9 100644 --- a/test/spec/Feature/OpenApi/OpenApiSpec.hs +++ b/test/spec/Feature/OpenApi/OpenApiSpec.hs @@ -11,15 +11,12 @@ import Network.HTTP.Types import Test.Hspec hiding (pendingWith) import Test.Hspec.Wai -import PostgREST.Config.PgVersion (PgVersion, pgVersion100, - pgVersion110) - import PostgREST.Version (docsVersion) import Protolude hiding (get) import SpecHelper -spec :: PgVersion -> SpecWith ((), Application) -spec actualPgVersion = describe "OpenAPI" $ do +spec :: SpecWith ((), Application) +spec = describe "OpenAPI" $ do it "root path returns a valid openapi spec" $ do validateOpenApiResponse [("Accept", "application/openapi+json")] request methodHead "/" @@ -269,31 +266,29 @@ 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" + 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 78e5e5d792..e5aa54ab9e 100644 --- a/test/spec/Feature/OptionsSpec.hs +++ b/test/spec/Feature/OptionsSpec.hs @@ -7,14 +7,11 @@ import Network.HTTP.Types import Test.Hspec import Test.Hspec.Wai -import PostgREST.Config.PgVersion (PgVersion, pgVersion100, - pgVersion110) - import Protolude import SpecHelper -spec :: PgVersion -> SpecWith ((), Application) -spec actualPgVersion = describe "Allow header" $ do +spec :: SpecWith ((), Application) +spec = describe "Allow header" $ do context "a table" $ do it "includes read/write methods for writeable table" $ do r <- request methodOptions "/items" [] "" @@ -25,18 +22,12 @@ 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" "OPTIONS,GET,HEAD,POST,PUT,PATCH,DELETE" context "a view" $ do context "auto updatable" $ do diff --git a/test/spec/Feature/Query/AndOrParamsSpec.hs b/test/spec/Feature/Query/AndOrParamsSpec.hs index aa6e505167..f3a8882f8e 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 4bf15d8985..405ef62f9f 100644 --- a/test/spec/Feature/Query/InsertSpec.hs +++ b/test/spec/Feature/Query/InsertSpec.hs @@ -11,9 +11,7 @@ import Test.Hspec.Wai import Test.Hspec.Wai.JSON import Text.Heredoc -import PostgREST.Config.PgVersion (PgVersion, pgVersion100, - pgVersion110, pgVersion112, - pgVersion120, pgVersion130, +import PostgREST.Config.PgVersion (PgVersion, pgVersion130, pgVersion140) import Protolude hiding (get) @@ -142,19 +140,18 @@ spec actualPgVersion = do , "Preference-Applied" <:> "return=headers-only"] } - when (actualPgVersion >= pgVersion110) $ - it "should not throw and return location header for partitioned tables when selecting without PK" $ - request methodPost "/car_models" - [("Prefer", "return=headers-only")] - [json|{"name":"Enzo","year":2021}|] - `shouldRespondWith` - "" - { matchStatus = 201 - , matchHeaders = [ matchHeaderAbsent hContentType - , "Location" <:> "/car_models?name=eq.Enzo&year=eq.2021" - , "Content-Range" <:> "*/*" - , "Preference-Applied" <:> "return=headers-only"] - } + it "should not throw and return location header for partitioned tables when selecting without PK" $ + request methodPost "/car_models" + [("Prefer", "return=headers-only")] + [json|{"name":"Enzo","year":2021}|] + `shouldRespondWith` + "" + { matchStatus = 201 + , matchHeaders = [ matchHeaderAbsent hContentType + , "Location" <:> "/car_models?name=eq.Enzo&year=eq.2021" + , "Content-Range" <:> "*/*" + , "Preference-Applied" <:> "return=headers-only"] + } context "requesting no representation" $ it "should not throw and return no location header when selecting without PK" $ @@ -494,24 +491,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,38 +532,36 @@ 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"] - } - - 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 "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 "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")] @@ -715,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 = [] } @@ -798,115 +783,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/JsonOperatorSpec.hs b/test/spec/Feature/Query/JsonOperatorSpec.hs index fea74093f5..2443976def 100644 --- a/test/spec/Feature/Query/JsonOperatorSpec.hs +++ b/test/spec/Feature/Query/JsonOperatorSpec.hs @@ -7,14 +7,11 @@ import Test.Hspec import Test.Hspec.Wai import Test.Hspec.Wai.JSON -import PostgREST.Config.PgVersion (PgVersion, pgVersion112, - pgVersion121) - import Protolude hiding (get) import SpecHelper -spec :: PgVersion -> SpecWith ((), Application) -spec actualPgVersion = describe "json and jsonb operators" $ do +spec :: SpecWith ((), Application) +spec = describe "json and jsonb operators" $ do context "Shaping response with select parameter" $ do it "obtains a json subfield one level with casting" $ get "/complex_items?id=eq.1&select=settings->>foo::json" `shouldRespondWith` @@ -28,12 +25,8 @@ spec actualPgVersion = describe "json and jsonb operators" $ do it "fails on bad casting (data of the wrong format)" $ get "/complex_items?select=settings->foo->>bar::integer" - `shouldRespondWith` ( - if actualPgVersion >= pgVersion121 then + `shouldRespondWith` [json| {"hint":null,"details":null,"code":"22P02","message":"invalid input syntax for type integer: \"baz\""} |] - else - [json| {"hint":null,"details":null,"code":"22P02","message":"invalid input syntax for integer: \"baz\""} |] - ) { matchStatus = 400 , matchHeaders = [] } it "obtains a json subfield two levels (string)" $ @@ -75,28 +68,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 d9e78d7709..182ac99a55 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/PostGISSpec.hs b/test/spec/Feature/Query/PostGISSpec.hs index a3e30dbb89..c6c431a790 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" } - } - ] - } - } - ] - }|] - 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" } - } - ] - } + [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" } + } + ] } - ] - }|]) + } + ] + }|] { 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/QuerySpec.hs b/test/spec/Feature/Query/QuerySpec.hs index 3b1da167b6..11ee50e266 100644 --- a/test/spec/Feature/Query/QuerySpec.hs +++ b/test/spec/Feature/Query/QuerySpec.hs @@ -8,13 +8,11 @@ import Test.Hspec hiding (pendingWith) import Test.Hspec.Wai import Test.Hspec.Wai.JSON -import PostgREST.Config.PgVersion (PgVersion, pgVersion110, - pgVersion112, pgVersion121) -import Protolude hiding (get) +import Protolude hiding (get) import SpecHelper -spec :: PgVersion -> SpecWith ((), Application) -spec actualPgVersion = do +spec :: SpecWith ((), Application) +spec = do describe "Querying a table with a column called count" $ it "should not confuse count column with pg_catalog.count aggregate" $ @@ -176,29 +174,28 @@ 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 "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"}] |] + 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 "finds matches with different dictionaries" $ do get "/tsearch?text_search_vector=fts(french).amusant" `shouldRespondWith` [json| [{"text_search_vector": "'amus':5 'fair':7 'impossibl':9 'peu':4" }] |] @@ -207,11 +204,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 +227,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" $ @@ -539,107 +534,105 @@ spec actualPgVersion = do [json|[{"id":1,"computed_overload":true}]|] { matchHeaders = [matchContentTypeJson] } - when (actualPgVersion >= pgVersion110) $ do - describe "partitioned tables embedding" $ do - it "can request a table as parent from a partitioned table" $ - get "/car_models?name=in.(DeLorean,Murcielago)&select=name,year,car_brands(name)&order=name.asc" `shouldRespondWith` - [json| - [{"name":"DeLorean","year":1981,"car_brands":{"name":"DMC"}}, - {"name":"Murcielago","year":2001,"car_brands":{"name":"Lamborghini"}}] |] - { matchHeaders = [matchContentTypeJson] } + describe "partitioned tables embedding" $ do + it "can request a table as parent from a partitioned table" $ + get "/car_models?name=in.(DeLorean,Murcielago)&select=name,year,car_brands(name)&order=name.asc" `shouldRespondWith` + [json| + [{"name":"DeLorean","year":1981,"car_brands":{"name":"DMC"}}, + {"name":"Murcielago","year":2001,"car_brands":{"name":"Lamborghini"}}] |] + { matchHeaders = [matchContentTypeJson] } - it "can request partitioned tables as children from a table" $ - get "/car_brands?select=name,car_models(name,year)&order=name.asc&car_models.order=name.asc" `shouldRespondWith` - [json| - [{"name":"DMC","car_models":[{"name":"DeLorean","year":1981}]}, - {"name":"Ferrari","car_models":[{"name":"F310-B","year":1997}]}, - {"name":"Lamborghini","car_models":[{"name":"Murcielago","year":2001},{"name":"Veneno","year":2013}]}] |] - { matchHeaders = [matchContentTypeJson] } + it "can request partitioned tables as children from a table" $ + get "/car_brands?select=name,car_models(name,year)&order=name.asc&car_models.order=name.asc" `shouldRespondWith` + [json| + [{"name":"DMC","car_models":[{"name":"DeLorean","year":1981}]}, + {"name":"Ferrari","car_models":[{"name":"F310-B","year":1997}]}, + {"name":"Lamborghini","car_models":[{"name":"Murcielago","year":2001},{"name":"Veneno","year":2013}]}] |] + { matchHeaders = [matchContentTypeJson] } - when (actualPgVersion >= pgVersion121) $ do - it "can request tables as children from a partitioned table" $ - get "/car_models?name=in.(DeLorean,F310-B)&select=name,year,car_racers(name)&order=name.asc" `shouldRespondWith` - [json| - [{"name":"DeLorean","year":1981,"car_racers":[]}, - {"name":"F310-B","year":1997,"car_racers":[{"name":"Michael Schumacher"}]}] |] - { matchHeaders = [matchContentTypeJson] } + it "can request tables as children from a partitioned table" $ + get "/car_models?name=in.(DeLorean,F310-B)&select=name,year,car_racers(name)&order=name.asc" `shouldRespondWith` + [json| + [{"name":"DeLorean","year":1981,"car_racers":[]}, + {"name":"F310-B","year":1997,"car_racers":[{"name":"Michael Schumacher"}]}] |] + { matchHeaders = [matchContentTypeJson] } - it "can request a partitioned table as parent from a table" $ - get "/car_racers?select=name,car_models(name,year)&order=name.asc" `shouldRespondWith` - [json| - [{"name":"Alain Prost","car_models":null}, - {"name":"Michael Schumacher","car_models":{"name":"F310-B","year":1997}}] |] - { matchHeaders = [matchContentTypeJson] } + it "can request a partitioned table as parent from a table" $ + get "/car_racers?select=name,car_models(name,year)&order=name.asc" `shouldRespondWith` + [json| + [{"name":"Alain Prost","car_models":null}, + {"name":"Michael Schumacher","car_models":{"name":"F310-B","year":1997}}] |] + { matchHeaders = [matchContentTypeJson] } - it "can request partitioned tables as children from a partitioned table" $ - get "/car_models?name=in.(DeLorean,Murcielago,Veneno)&select=name,year,car_model_sales(date,quantity)&order=name.asc" `shouldRespondWith` - [json| - [{"name":"DeLorean","year":1981,"car_model_sales":[{"date":"2021-01-14","quantity":7},{"date":"2021-01-15","quantity":9}]}, - {"name":"Murcielago","year":2001,"car_model_sales":[{"date":"2021-02-11","quantity":1},{"date":"2021-02-12","quantity":3}]}, - {"name":"Veneno","year":2013,"car_model_sales":[]}] |] - { matchHeaders = [matchContentTypeJson] } + it "can request partitioned tables as children from a partitioned table" $ + get "/car_models?name=in.(DeLorean,Murcielago,Veneno)&select=name,year,car_model_sales(date,quantity)&order=name.asc" `shouldRespondWith` + [json| + [{"name":"DeLorean","year":1981,"car_model_sales":[{"date":"2021-01-14","quantity":7},{"date":"2021-01-15","quantity":9}]}, + {"name":"Murcielago","year":2001,"car_model_sales":[{"date":"2021-02-11","quantity":1},{"date":"2021-02-12","quantity":3}]}, + {"name":"Veneno","year":2013,"car_model_sales":[]}] |] + { matchHeaders = [matchContentTypeJson] } - it "can request a partitioned table as parent from a partitioned table" $ do - get "/car_model_sales?date=in.(2021-01-15,2021-02-11)&select=date,quantity,car_models(name,year)&order=date.asc" `shouldRespondWith` - [json| - [{"date":"2021-01-15","quantity":9,"car_models":{"name":"DeLorean","year":1981}}, - {"date":"2021-02-11","quantity":1,"car_models":{"name":"Murcielago","year":2001}}] |] - { matchHeaders = [matchContentTypeJson] } + it "can request a partitioned table as parent from a partitioned table" $ do + get "/car_model_sales?date=in.(2021-01-15,2021-02-11)&select=date,quantity,car_models(name,year)&order=date.asc" `shouldRespondWith` + [json| + [{"date":"2021-01-15","quantity":9,"car_models":{"name":"DeLorean","year":1981}}, + {"date":"2021-02-11","quantity":1,"car_models":{"name":"Murcielago","year":2001}}] |] + { matchHeaders = [matchContentTypeJson] } - it "can request many to many relationships between partitioned tables ignoring the intermediate table partitions" $ - get "/car_models?select=name,year,car_dealers(name,city)&order=name.asc&limit=4" `shouldRespondWith` - [json| - [{"name":"DeLorean","year":1981,"car_dealers":[{"name":"Springfield Cars S.A.","city":"Springfield"}]}, - {"name":"F310-B","year":1997,"car_dealers":[]}, - {"name":"Murcielago","year":2001,"car_dealers":[{"name":"The Best Deals S.A.","city":"Franklin"}]}, - {"name":"Veneno","year":2013,"car_dealers":[]}] |] - { matchStatus = 200 - , matchHeaders = [matchContentTypeJson] - } - - it "cannot request partitions as children from a partitioned table" $ - get "/car_models?id=in.(1,2,4)&select=id,name,car_model_sales_202101(id)&order=id.asc" `shouldRespondWith` - [json| - {"hint":"Perhaps you meant 'car_model_sales' instead of 'car_model_sales_202101'.", - "details":"Searched for a foreign key relationship between 'car_models' and 'car_model_sales_202101' in the schema 'test', but no matches were found.", - "code":"PGRST200", - "message":"Could not find a relationship between 'car_models' and 'car_model_sales_202101' in the schema cache"} |] - { matchStatus = 400 - , matchHeaders = [matchContentTypeJson] - } - - it "cannot request a partitioned table as parent from a partition" $ - get "/car_model_sales_202101?select=id,name,car_models(id,name)&order=id.asc" `shouldRespondWith` - [json| - {"hint":"Perhaps you meant 'car_model_sales' instead of 'car_model_sales_202101'.", - "details":"Searched for a foreign key relationship between 'car_model_sales_202101' and 'car_models' in the schema 'test', but no matches were found.", - "code":"PGRST200", - "message":"Could not find a relationship between 'car_model_sales_202101' and 'car_models' in the schema cache"} |] - { matchStatus = 400 - , matchHeaders = [matchContentTypeJson] - } - - it "cannot request a partition as parent from a partitioned table" $ - get "/car_model_sales?id=in.(1,3,4)&select=id,name,car_models_default(id,name)&order=id.asc" `shouldRespondWith` - [json| - {"hint":"Perhaps you meant 'car_models' instead of 'car_models_default'.", - "details":"Searched for a foreign key relationship between 'car_model_sales' and 'car_models_default' in the schema 'test', but no matches were found.", - "code":"PGRST200", - "message":"Could not find a relationship between 'car_model_sales' and 'car_models_default' in the schema cache"} |] - { matchStatus = 400 - , matchHeaders = [matchContentTypeJson] - } - - it "cannot request partitioned tables as children from a partition" $ - get "/car_models_default?select=id,name,car_model_sales(id,name)&order=id.asc" `shouldRespondWith` - [json| - {"hint":"Perhaps you meant 'car_model_sales' instead of 'car_models_default'.", - "details":"Searched for a foreign key relationship between 'car_models_default' and 'car_model_sales' in the schema 'test', but no matches were found.", - "code":"PGRST200", - "message":"Could not find a relationship between 'car_models_default' and 'car_model_sales' in the schema cache"} |] - { matchStatus = 400 - , matchHeaders = [matchContentTypeJson] - } + it "can request many to many relationships between partitioned tables ignoring the intermediate table partitions" $ + get "/car_models?select=name,year,car_dealers(name,city)&order=name.asc&limit=4" `shouldRespondWith` + [json| + [{"name":"DeLorean","year":1981,"car_dealers":[{"name":"Springfield Cars S.A.","city":"Springfield"}]}, + {"name":"F310-B","year":1997,"car_dealers":[]}, + {"name":"Murcielago","year":2001,"car_dealers":[{"name":"The Best Deals S.A.","city":"Franklin"}]}, + {"name":"Veneno","year":2013,"car_dealers":[]}] |] + { matchStatus = 200 + , matchHeaders = [matchContentTypeJson] + } + + it "cannot request partitions as children from a partitioned table" $ + get "/car_models?id=in.(1,2,4)&select=id,name,car_model_sales_202101(id)&order=id.asc" `shouldRespondWith` + [json| + {"hint":"Perhaps you meant 'car_model_sales' instead of 'car_model_sales_202101'.", + "details":"Searched for a foreign key relationship between 'car_models' and 'car_model_sales_202101' in the schema 'test', but no matches were found.", + "code":"PGRST200", + "message":"Could not find a relationship between 'car_models' and 'car_model_sales_202101' in the schema cache"} |] + { matchStatus = 400 + , matchHeaders = [matchContentTypeJson] + } + + it "cannot request a partitioned table as parent from a partition" $ + get "/car_model_sales_202101?select=id,name,car_models(id,name)&order=id.asc" `shouldRespondWith` + [json| + {"hint":"Perhaps you meant 'car_model_sales' instead of 'car_model_sales_202101'.", + "details":"Searched for a foreign key relationship between 'car_model_sales_202101' and 'car_models' in the schema 'test', but no matches were found.", + "code":"PGRST200", + "message":"Could not find a relationship between 'car_model_sales_202101' and 'car_models' in the schema cache"} |] + { matchStatus = 400 + , matchHeaders = [matchContentTypeJson] + } + + it "cannot request a partition as parent from a partitioned table" $ + get "/car_model_sales?id=in.(1,3,4)&select=id,name,car_models_default(id,name)&order=id.asc" `shouldRespondWith` + [json| + {"hint":"Perhaps you meant 'car_models' instead of 'car_models_default'.", + "details":"Searched for a foreign key relationship between 'car_model_sales' and 'car_models_default' in the schema 'test', but no matches were found.", + "code":"PGRST200", + "message":"Could not find a relationship between 'car_model_sales' and 'car_models_default' in the schema cache"} |] + { matchStatus = 400 + , matchHeaders = [matchContentTypeJson] + } + + it "cannot request partitioned tables as children from a partition" $ + get "/car_models_default?select=id,name,car_model_sales(id,name)&order=id.asc" `shouldRespondWith` + [json| + {"hint":"Perhaps you meant 'car_model_sales' instead of 'car_models_default'.", + "details":"Searched for a foreign key relationship between 'car_models_default' and 'car_model_sales' in the schema 'test', but no matches were found.", + "code":"PGRST200", + "message":"Could not find a relationship between 'car_models_default' and 'car_model_sales' in the schema cache"} |] + { matchStatus = 400 + , matchHeaders = [matchContentTypeJson] + } describe "view embedding" $ do it "can detect fk relations through views to tables in the public schema" $ @@ -1138,12 +1131,8 @@ spec actualPgVersion = do it "only returns an empty result set if the in value is empty" $ get "/items_with_different_col_types?int_data=in.( ,3,4)" - `shouldRespondWith` ( - if actualPgVersion >= pgVersion121 then + `shouldRespondWith` [json| {"hint":null,"details":null,"code":"22P02","message":"invalid input syntax for type integer: \"\""} |] - else - [json| {"hint":null,"details":null,"code":"22P02","message":"invalid input syntax for integer: \"\""} |] - ) { matchStatus = 400 , matchHeaders = [matchContentTypeJson] } @@ -1362,22 +1351,19 @@ spec actualPgVersion = do { matchStatus = 400 , matchHeaders = [matchContentTypeJson] } - -- Before PG 11, this will fail because we need arrays of domain type values. The docs should explain data reps are - -- not supported in this case. - when (actualPgVersion >= pgVersion110) $ do - it "uses text parser for filter with 'IN' predicates" $ - get "/datarep_todos?select=id,due_at&label_color=in.(000100,01E240)" `shouldRespondWith` - [json| [ - {"id":2, "due_at": "2018-01-03T00:00:00Z"}, - {"id":3, "due_at": "2018-01-01T14:12:34.123456Z"} - ] |] - { matchHeaders = [matchContentTypeJson] } - it "uses text parser for filter with 'NOT IN' predicates" $ - get "/datarep_todos?select=id,due_at&label_color=not.in.(000000,01E240)" `shouldRespondWith` - [json| [ - {"id":2, "due_at": "2018-01-03T00:00:00Z"} - ] |] - { matchHeaders = [matchContentTypeJson] } + it "uses text parser for filter with 'IN' predicates" $ + get "/datarep_todos?select=id,due_at&label_color=in.(000100,01E240)" `shouldRespondWith` + [json| [ + {"id":2, "due_at": "2018-01-03T00:00:00Z"}, + {"id":3, "due_at": "2018-01-01T14:12:34.123456Z"} + ] |] + { matchHeaders = [matchContentTypeJson] } + it "uses text parser for filter with 'NOT IN' predicates" $ + get "/datarep_todos?select=id,due_at&label_color=not.in.(000000,01E240)" `shouldRespondWith` + [json| [ + {"id":2, "due_at": "2018-01-03T00:00:00Z"} + ] |] + { matchHeaders = [matchContentTypeJson] } it "uses text parser on value for filter across relations" $ get "/datarep_next_two_todos?select=id,name,datarep_todos!datarep_next_two_todos_first_item_id_fkey(label_color,due_at)&datarep_todos.label_color=neq.000100" `shouldRespondWith` [json| [{"id":1,"name":"school related","datarep_todos":null},{"id":2,"name":"do these first","datarep_todos":{"label_color":"#000000","due_at":"2018-01-02T00:00:00Z"}}] |] @@ -1385,15 +1371,10 @@ spec actualPgVersion = do -- This is not supported by data reps (would be hard to make it work with high performance). So the test just -- verifies we don't panic or add inappropriate SQL to the filters. it "fails safely on user trying to use ilike operator on data reps column" $ - get "/datarep_todos?select=id,name&label_color=ilike.#*100" `shouldRespondWith` ( - if actualPgVersion >= pgVersion110 then + get "/datarep_todos?select=id,name&label_color=ilike.#*100" `shouldRespondWith` [json| {"code":"42883","details":null,"hint":"No operator matches the given name and argument types. You might need to add explicit type casts.","message":"operator does not exist: public.color ~~* unknown"} |] - else - [json| - {"code":"42883","details":null,"hint":"No operator matches the given name and argument type(s). You might need to add explicit type casts.","message":"operator does not exist: public.color ~~* unknown"} - |]) { matchStatus = 404 , matchHeaders = [matchContentTypeJson] } diff --git a/test/spec/Feature/Query/RpcSpec.hs b/test/spec/Feature/Query/RpcSpec.hs index 0e4af838e2..7f03db3acd 100644 --- a/test/spec/Feature/Query/RpcSpec.hs +++ b/test/spec/Feature/Query/RpcSpec.hs @@ -11,15 +11,11 @@ import Test.Hspec.Wai import Test.Hspec.Wai.JSON import Text.Heredoc -import PostgREST.Config.PgVersion (PgVersion, pgVersion100, - pgVersion109, pgVersion110, - 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 @@ -395,15 +391,14 @@ spec actualPgVersion = ]|] { matchHeaders = [matchContentTypeJson] } - when (actualPgVersion >= pgVersion110) $ - it "can embed if rpc returns domain of table type" $ do - post "/rpc/getproject_domain?select=id,name,client:clients(id),tasks(id)" - [json| { "id": 1} |] - `shouldRespondWith` - [json|[{"id":1,"name":"Windows 7","client":{"id":1},"tasks":[{"id":1},{"id":2}]}]|] - get "/rpc/getproject_domain?id=1&select=id,name,client:clients(id),tasks(id)" - `shouldRespondWith` - [json|[{"id":1,"name":"Windows 7","client":{"id":1},"tasks":[{"id":1},{"id":2}]}]|] + it "can embed if rpc returns domain of table type" $ do + post "/rpc/getproject_domain?select=id,name,client:clients(id),tasks(id)" + [json| { "id": 1} |] + `shouldRespondWith` + [json|[{"id":1,"name":"Windows 7","client":{"id":1},"tasks":[{"id":1},{"id":2}]}]|] + get "/rpc/getproject_domain?id=1&select=id,name,client:clients(id),tasks(id)" + `shouldRespondWith` + [json|[{"id":1,"name":"Windows 7","client":{"id":1},"tasks":[{"id":1},{"id":2}]}]|] context "a proc that returns an empty rowset" $ it "returns empty json array" $ do @@ -466,12 +461,11 @@ spec actualPgVersion = it "cannot return composite type in hidden schema" $ post "/rpc/ret_point_3d" [json|{}|] `shouldRespondWith` 401 - when (actualPgVersion >= pgVersion110) $ - it "returns domain of composite type" $ - post "/rpc/ret_composite_domain" - [json|{}|] - `shouldRespondWith` - [json|{"x": 10, "y": 5}|] + it "returns domain of composite type" $ + post "/rpc/ret_composite_domain" + [json|{}|] + `shouldRespondWith` + [json|{"x": 10, "y": 5}|] it "returns single row from table" $ post "/rpc/single_article?select=id" @@ -494,26 +488,25 @@ spec actualPgVersion = `shouldRespondWith` [json|null|] - when (actualPgVersion >= pgVersion110) $ do - it "returns a record type" $ do - post "/rpc/returns_record" - "" - `shouldRespondWith` - [json|{"id":1,"name":"Windows 7","client_id":1}|] - post "/rpc/returns_record_params" - [json|{"id":1, "name": "Windows%"}|] - `shouldRespondWith` - [json|{"id":1,"name":"Windows 7","client_id":1}|] - - it "returns a setof record type" $ do - post "/rpc/returns_setof_record" - "" - `shouldRespondWith` - [json|[{"id":1,"name":"Windows 7","client_id":1},{"id":2,"name":"Windows 10","client_id":1}]|] - post "/rpc/returns_setof_record_params" - [json|{"id":1,"name":"Windows%"}|] - `shouldRespondWith` - [json|[{"id":1,"name":"Windows 7","client_id":1},{"id":2,"name":"Windows 10","client_id":1}]|] + it "returns a record type" $ do + post "/rpc/returns_record" + "" + `shouldRespondWith` + [json|{"id":1,"name":"Windows 7","client_id":1}|] + post "/rpc/returns_record_params" + [json|{"id":1, "name": "Windows%"}|] + `shouldRespondWith` + [json|{"id":1,"name":"Windows 7","client_id":1}|] + + it "returns a setof record type" $ do + post "/rpc/returns_setof_record" + "" + `shouldRespondWith` + [json|[{"id":1,"name":"Windows 7","client_id":1},{"id":2,"name":"Windows 10","client_id":1}]|] + post "/rpc/returns_setof_record_params" + [json|{"id":1,"name":"Windows%"}|] + `shouldRespondWith` + [json|[{"id":1,"name":"Windows 7","client_id":1},{"id":2,"name":"Windows 10","client_id":1}]|] context "different types when overloaded" $ do it "returns composite type" $ @@ -530,66 +523,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,22 +596,12 @@ 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)" $ - 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 @@ -765,68 +716,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|[]|] + + it "n=1" $ + get "/rpc/variadic_param?v=hi" + `shouldRespondWith` + [json|["hi"]|] + + it "n>1" $ + get "/rpc/variadic_param?v=hi&v=there" + `shouldRespondWith` + [json|["hi", "there"]|] + + 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|["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" $ + request methodPost "/rpc/variadic_param" + [("Content-Type", "application/x-www-form-urlencoded")] + "v=hi" `shouldRespondWith` - [json|["hi", "hello", "there"]|] - - 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" - `shouldRespondWith` - [json|["hi"]|] - - it "n>1" $ - get "/rpc/variadic_param?v=hi&v=there" - `shouldRespondWith` - [json|["hi", "there"]|] - - 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"]|] + [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 "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" @@ -1073,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/Feature/Query/UpdateSpec.hs b/test/spec/Feature/Query/UpdateSpec.hs index 68c09ebdb7..3faaab4518 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/*"] - } - - 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"] - } + 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 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 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 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=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?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 "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?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 "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?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/Feature/Query/UpsertSpec.hs b/test/spec/Feature/Query/UpsertSpec.hs index 165eada3e5..db08aa89bb 100644 --- a/test/spec/Feature/Query/UpsertSpec.hs +++ b/test/spec/Feature/Query/UpsertSpec.hs @@ -7,13 +7,11 @@ import Test.Hspec import Test.Hspec.Wai import Test.Hspec.Wai.JSON -import PostgREST.Config.PgVersion (PgVersion, pgVersion110) - import Protolude hiding (get, put) import SpecHelper -spec :: PgVersion -> SpecWith ((), Application) -spec actualPgVersion = +spec :: SpecWith ((), Application) +spec = describe "UPSERT" $ do context "with POST" $ do context "when Prefer: resolution=merge-duplicates is specified" $ do @@ -60,19 +58,18 @@ spec actualPgVersion = , matchHeaders = ["Preference-Applied" <:> "resolution=merge-duplicates, return=representation", matchContentTypeJson] } - when (actualPgVersion >= pgVersion110) $ - it "INSERTs and UPDATEs rows on composite pk conflict for partitioned tables" $ - request methodPost "/car_models" [("Prefer", "return=representation"), ("Prefer", "resolution=merge-duplicates")] - [json| [ - { "name": "Murcielago", "year": 2001, "car_brand_name": null}, - { "name": "Roma", "year": 2021, "car_brand_name": "Ferrari" } - ]|] `shouldRespondWith` [json| [ - { "name": "Murcielago", "year": 2001, "car_brand_name": null}, - { "name": "Roma", "year": 2021, "car_brand_name": "Ferrari" } - ]|] - { matchStatus = 201 - , matchHeaders = ["Preference-Applied" <:> "resolution=merge-duplicates, return=representation", matchContentTypeJson] - } + it "INSERTs and UPDATEs rows on composite pk conflict for partitioned tables" $ + request methodPost "/car_models" [("Prefer", "return=representation"), ("Prefer", "resolution=merge-duplicates")] + [json| [ + { "name": "Murcielago", "year": 2001, "car_brand_name": null}, + { "name": "Roma", "year": 2021, "car_brand_name": "Ferrari" } + ]|] `shouldRespondWith` [json| [ + { "name": "Murcielago", "year": 2001, "car_brand_name": null}, + { "name": "Roma", "year": 2021, "car_brand_name": "Ferrari" } + ]|] + { matchStatus = 201 + , matchHeaders = ["Preference-Applied" <:> "resolution=merge-duplicates, return=representation", matchContentTypeJson] + } it "succeeds when the payload has no elements" $ request methodPost "/articles" [("Prefer", "return=representation"), ("Prefer", "resolution=merge-duplicates")] @@ -131,18 +128,17 @@ spec actualPgVersion = , matchHeaders = ["Preference-Applied" <:> "resolution=ignore-duplicates, return=representation", matchContentTypeJson] } - when (actualPgVersion >= pgVersion110) $ - it "INSERTs and ignores rows on composite pk conflict for partitioned tables" $ - request methodPost "/car_models" [("Prefer", "return=representation"), ("Prefer", "resolution=ignore-duplicates")] - [json| [ - { "name": "Murcielago", "year": 2001, "car_brand_name": "Ferrari" }, - { "name": "Huracán", "year": 2021, "car_brand_name": "Lamborghini" } - ]|] `shouldRespondWith` [json| [ - { "name": "Huracán", "year": 2021, "car_brand_name": "Lamborghini" } - ]|] - { matchStatus = 201 - , matchHeaders = ["Preference-Applied" <:> "resolution=ignore-duplicates, return=representation", matchContentTypeJson] - } + it "INSERTs and ignores rows on composite pk conflict for partitioned tables" $ + request methodPost "/car_models" [("Prefer", "return=representation"), ("Prefer", "resolution=ignore-duplicates")] + [json| [ + { "name": "Murcielago", "year": 2001, "car_brand_name": "Ferrari" }, + { "name": "Huracán", "year": 2021, "car_brand_name": "Lamborghini" } + ]|] `shouldRespondWith` [json| [ + { "name": "Huracán", "year": 2021, "car_brand_name": "Lamborghini" } + ]|] + { matchStatus = 201 + , matchHeaders = ["Preference-Applied" <:> "resolution=ignore-duplicates, return=representation", matchContentTypeJson] + } it "INSERTs and ignores rows on single unique key conflict" $ request methodPost "/single_unique?on_conflict=unique_key" @@ -312,19 +308,18 @@ spec actualPgVersion = [json| [ { "first_name": "Susan", "last_name": "Heidt", "salary": "$48,000.00", "company": "GEX", "occupation": "Railroad engineer" } ]|] { matchStatus = 201 } - when (actualPgVersion >= pgVersion110) $ - it "succeeds on a partitioned table with composite pk" $ do - -- assert that the next request will indeed be an insert - get "/car_models?name=eq.Supra&year=eq.2021" - `shouldRespondWith` - [json|[]|] - - request methodPut "/car_models?name=eq.Supra&year=eq.2021" - [("Prefer", "return=representation")] - [json| [ { "name": "Supra", "year": 2021 } ]|] - `shouldRespondWith` - [json| [ { "name": "Supra", "year": 2021, "car_brand_name": null } ]|] - { matchStatus = 201 } + it "succeeds on a partitioned table with composite pk" $ do + -- assert that the next request will indeed be an insert + get "/car_models?name=eq.Supra&year=eq.2021" + `shouldRespondWith` + [json|[]|] + + request methodPut "/car_models?name=eq.Supra&year=eq.2021" + [("Prefer", "return=representation")] + [json| [ { "name": "Supra", "year": 2021 } ]|] + `shouldRespondWith` + [json| [ { "name": "Supra", "year": 2021, "car_brand_name": null } ]|] + { matchStatus = 201 } it "succeeds if the table has only PK cols and no other cols" $ do -- assert that the next request will indeed be an insert @@ -378,18 +373,17 @@ spec actualPgVersion = `shouldRespondWith` [json| [ { "first_name": "Frances M.", "last_name": "Roe", "salary": "$60,000.00", "company": "Gamma Gas", "occupation": "Railroad engineer" } ]|] - when (actualPgVersion >= pgVersion110) $ - it "succeeds on a partitioned table with composite pk" $ do - -- assert that the next request will indeed be an update - get "/car_models?name=eq.DeLorean&year=eq.1981" - `shouldRespondWith` - [json| [ { "name": "DeLorean", "year": 1981, "car_brand_name": "DMC" } ]|] - - request methodPut "/car_models?name=eq.DeLorean&year=eq.1981" - [("Prefer", "return=representation")] - [json| [ { "name": "DeLorean", "year": 1981, "car_brand_name": null } ]|] - `shouldRespondWith` - [json| [ { "name": "DeLorean", "year": 1981, "car_brand_name": null } ]|] + it "succeeds on a partitioned table with composite pk" $ do + -- assert that the next request will indeed be an update + get "/car_models?name=eq.DeLorean&year=eq.1981" + `shouldRespondWith` + [json| [ { "name": "DeLorean", "year": 1981, "car_brand_name": "DMC" } ]|] + + request methodPut "/car_models?name=eq.DeLorean&year=eq.1981" + [("Prefer", "return=representation")] + [json| [ { "name": "DeLorean", "year": 1981, "car_brand_name": null } ]|] + `shouldRespondWith` + [json| [ { "name": "DeLorean", "year": 1981, "car_brand_name": null } ]|] it "succeeds if the table has only PK cols and no other cols" $ do -- assert that the next request will indeed be an update diff --git a/test/spec/Main.hs b/test/spec/Main.hs index 4072643355..070c6905de 100644 --- a/test/spec/Main.hs +++ b/test/spec/Main.hs @@ -137,33 +137,33 @@ 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 actualPgVersion) - , ("Feature.OptionsSpec" , Feature.OptionsSpec.spec actualPgVersion) - , ("Feature.Query.AndOrParamsSpec" , Feature.Query.AndOrParamsSpec.spec actualPgVersion) + , ("Feature.OpenApi.OpenApiSpec" , Feature.OpenApi.OpenApiSpec.spec) + , ("Feature.OptionsSpec" , Feature.OptionsSpec.spec) + , ("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) , ("Feature.Query.EmbedInnerJoinSpec" , Feature.Query.EmbedInnerJoinSpec.spec) , ("Feature.Query.InsertSpec" , Feature.Query.InsertSpec.spec actualPgVersion) - , ("Feature.Query.JsonOperatorSpec" , Feature.Query.JsonOperatorSpec.spec actualPgVersion) + , ("Feature.Query.JsonOperatorSpec" , Feature.Query.JsonOperatorSpec.spec) , ("Feature.Query.NullsStripSpec" , Feature.Query.NullsStripSpec.spec) , ("Feature.Query.PgErrorCodeMappingSpec" , Feature.Query.ErrorSpec.pgErrorCodeMapping) , ("Feature.Query.PgSafeUpdateSpec.disabledSpec" , Feature.Query.PgSafeUpdateSpec.disabledSpec) , ("Feature.Query.PlanSpec.disabledSpec" , Feature.Query.PlanSpec.disabledSpec) , ("Feature.Query.PreferencesSpec" , Feature.Query.PreferencesSpec.spec) - , ("Feature.Query.QuerySpec" , Feature.Query.QuerySpec.spec actualPgVersion) + , ("Feature.Query.QuerySpec" , Feature.Query.QuerySpec.spec) , ("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 actualPgVersion) - , ("Feature.Query.UpsertSpec" , Feature.Query.UpsertSpec.spec actualPgVersion) + , ("Feature.Query.UpdateSpec" , Feature.Query.UpdateSpec.spec) + , ("Feature.Query.UpsertSpec" , Feature.Query.UpsertSpec.spec) ] hspec $ do @@ -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 68a83a2303..3a5af3862c 100644 --- a/test/spec/fixtures/data.sql +++ b/test/spec/fixtures/data.sql @@ -680,41 +680,33 @@ 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; - - 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'); - INSERT INTO test.car_brands(name) VALUES ('Lamborghini'); - - UPDATE test.car_models SET car_brand_name = 'DMC' WHERE name = 'DeLorean'; - 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'; - END IF; - - 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_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_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(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); + +INSERT INTO test.car_brands(name) VALUES ('DMC'); +INSERT INTO test.car_brands(name) VALUES ('Ferrari'); +INSERT INTO test.car_brands(name) VALUES ('Lamborghini'); + +UPDATE test.car_models SET car_brand_name = 'DMC' WHERE name = 'DeLorean'; +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'; + +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_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); 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/privileges.sql b/test/spec/fixtures/privileges.sql index e5fc79e9fa..c4df1b02d1 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 fdb472bbd2..d20c368891 100644 --- a/test/spec/fixtures/schema.sql +++ b/test/spec/fixtures/schema.sql @@ -1046,19 +1046,14 @@ CREATE FUNCTION setprojects(id_l int, id_h int, name text) RETURNS SETOF project update test.projects set name = $3 WHERE id >= $1 AND id <= $2 returning *; $_$; --- domains on tables are only supported from pg 11 on -DO $do$BEGIN - IF (SELECT current_setting('server_version_num')::INT >= 110000) THEN - CREATE DOMAIN projects_domain AS projects; - - CREATE FUNCTION getproject_domain(id int) RETURNS SETOF projects_domain - LANGUAGE sql - STABLE - AS $_$ - SELECT projects::projects_domain FROM test.projects WHERE id = $1; - $_$; - END IF; -END$do$; +CREATE DOMAIN projects_domain AS projects; + +CREATE FUNCTION getproject_domain(id int) RETURNS SETOF projects_domain + LANGUAGE sql + STABLE + AS $_$ + SELECT projects::projects_domain FROM test.projects WHERE id = $1; +$_$; create table images ( name text not null, @@ -1119,16 +1114,11 @@ create function test.ret_point_overloaded(x json) returns json as $$ select $1; $$ language sql; --- domains on composite types are only supported from pg 11 on -do $do$begin - if (SELECT current_setting('server_version_num')::int >= 110000) then - create domain test.composite_domain as test.point_2d; +create domain test.composite_domain as test.point_2d; - create function test.ret_composite_domain() returns test.composite_domain as $$ - select row(10, 5)::test.composite_domain; - $$ language sql; - end if; -end$do$; +create function test.ret_composite_domain() returns test.composite_domain as $$ + select row(10, 5)::test.composite_domain; +$$ language sql; create type private.point_3d as (x integer, y integer, z integer); @@ -2289,90 +2279,78 @@ 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); - -- 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 ( - name varchar(64) primary key - ); +create table test.car_brands ( + name varchar(64) primary key +); - 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); - end if; +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); - -- 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_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_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_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_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_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_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_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_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_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_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_default partition of test.car_models_car_dealers + default; create or replace function test.unnamed_json_param(json) returns json as $$ select $1; @@ -2492,22 +2470,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 ( @@ -2529,12 +2500,8 @@ create table test.arrays ( -- This procedure is to confirm that procedures don't show up in the OpenAPI output right now. -- Procedures are not supported, yet. -do $do$begin - if (select current_setting('server_version_num')::int >= 110000) then - CREATE PROCEDURE test.unsupported_proc () - LANGUAGE SQL AS ''; - end if; -end $do$; +CREATE PROCEDURE test.unsupported_proc () +LANGUAGE SQL AS ''; CREATE FUNCTION public.dummy(int) RETURNS int LANGUAGE SQL AS $$ SELECT 1 $$; @@ -3277,17 +3244,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 @@ -3295,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;