fix: Avoid casting to table type when select= and media type handler are used

Previously using a generic mimetype handler failed when any kind of select= was given, because
we tried to cast the select-result to the original table type. With this change, this cast is
only applied when select=* is given implicitly or explicitly. This is the only case where this
makes sense, because this guarantees that correct columns are selected in the correct order for
this cast to succeed.

Resolves #3160
This commit is contained in:
Wolfgang Walther
2024-02-15 19:01:12 +01:00
parent 2466f4e738
commit 00fbe9ff3e
10 changed files with 131 additions and 43 deletions
+1
View File
@@ -18,6 +18,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
- #2815, Build static executable with GSSAPI support - @wolfgangwalther - #2815, Build static executable with GSSAPI support - @wolfgangwalther
- #3205, Fix wrong subquery error returning a status of 400 Bad Request - @steve-chavez - #3205, Fix wrong subquery error returning a status of 400 Bad Request - @steve-chavez
- #3224, Return status code 406 for non-accepted media type instead of code 415 - @wolfgangwalther - #3224, Return status code 406 for non-accepted media type instead of code 415 - @wolfgangwalther
- #3160, Fix using select= query parameter for custom media type handlers - @wolfgangwalther
### Deprecated ### Deprecated
+18 -14
View File
@@ -97,7 +97,6 @@ data WrappedReadPlan = WrappedReadPlan {
, wrTxMode :: SQL.Mode , wrTxMode :: SQL.Mode
, wrHandler :: MediaHandler , wrHandler :: MediaHandler
, wrMedia :: MediaType , wrMedia :: MediaType
, wrIdent :: QualifiedIdentifier
} }
data MutateReadPlan = MutateReadPlan { data MutateReadPlan = MutateReadPlan {
@@ -106,7 +105,6 @@ data MutateReadPlan = MutateReadPlan {
, mrTxMode :: SQL.Mode , mrTxMode :: SQL.Mode
, mrHandler :: MediaHandler , mrHandler :: MediaHandler
, mrMedia :: MediaType , mrMedia :: MediaType
, mrIdent :: QualifiedIdentifier
} }
data CallReadPlan = CallReadPlan { data CallReadPlan = CallReadPlan {
@@ -116,7 +114,6 @@ data CallReadPlan = CallReadPlan {
, crProc :: Routine , crProc :: Routine
, crHandler :: MediaHandler , crHandler :: MediaHandler
, crMedia :: MediaType , crMedia :: MediaType
, crIdent :: QualifiedIdentifier
} }
data InspectPlan = InspectPlan { data InspectPlan = InspectPlan {
@@ -127,17 +124,17 @@ data InspectPlan = InspectPlan {
wrappedReadPlan :: QualifiedIdentifier -> AppConfig -> SchemaCache -> ApiRequest -> Either Error WrappedReadPlan wrappedReadPlan :: QualifiedIdentifier -> AppConfig -> SchemaCache -> ApiRequest -> Either Error WrappedReadPlan
wrappedReadPlan identifier conf sCache apiRequest@ApiRequest{iPreferences=Preferences{..},..} = do wrappedReadPlan identifier conf sCache apiRequest@ApiRequest{iPreferences=Preferences{..},..} = do
rPlan <- readPlan identifier conf sCache apiRequest rPlan <- readPlan identifier conf sCache apiRequest
(hdler, mediaType) <- mapLeft ApiRequestError $ negotiateContent conf apiRequest identifier iAcceptMediaType (dbMediaHandlers sCache) (handler, mediaType) <- mapLeft ApiRequestError $ negotiateContent conf apiRequest identifier iAcceptMediaType (dbMediaHandlers sCache) (hasDefaultSelect rPlan)
if not (null invalidPrefs) && preferHandling == Just Strict then Left $ ApiRequestError $ InvalidPreferences invalidPrefs else Right () if not (null invalidPrefs) && preferHandling == Just Strict then Left $ ApiRequestError $ InvalidPreferences invalidPrefs else Right ()
return $ WrappedReadPlan rPlan SQL.Read hdler mediaType identifier return $ WrappedReadPlan rPlan SQL.Read handler mediaType
mutateReadPlan :: Mutation -> ApiRequest -> QualifiedIdentifier -> AppConfig -> SchemaCache -> Either Error MutateReadPlan mutateReadPlan :: Mutation -> ApiRequest -> QualifiedIdentifier -> AppConfig -> SchemaCache -> Either Error MutateReadPlan
mutateReadPlan mutation apiRequest@ApiRequest{iPreferences=Preferences{..},..} identifier conf sCache = do mutateReadPlan mutation apiRequest@ApiRequest{iPreferences=Preferences{..},..} identifier conf sCache = do
rPlan <- readPlan identifier conf sCache apiRequest rPlan <- readPlan identifier conf sCache apiRequest
mPlan <- mutatePlan mutation identifier apiRequest sCache rPlan mPlan <- mutatePlan mutation identifier apiRequest sCache rPlan
if not (null invalidPrefs) && preferHandling == Just Strict then Left $ ApiRequestError $ InvalidPreferences invalidPrefs else Right () if not (null invalidPrefs) && preferHandling == Just Strict then Left $ ApiRequestError $ InvalidPreferences invalidPrefs else Right ()
(hdler, mediaType) <- mapLeft ApiRequestError $ negotiateContent conf apiRequest identifier iAcceptMediaType (dbMediaHandlers sCache) (handler, mediaType) <- mapLeft ApiRequestError $ negotiateContent conf apiRequest identifier iAcceptMediaType (dbMediaHandlers sCache) (hasDefaultSelect rPlan)
return $ MutateReadPlan rPlan mPlan SQL.Write hdler mediaType identifier return $ MutateReadPlan rPlan mPlan SQL.Write handler mediaType
callReadPlan :: QualifiedIdentifier -> AppConfig -> SchemaCache -> ApiRequest -> InvokeMethod -> Either Error CallReadPlan callReadPlan :: QualifiedIdentifier -> AppConfig -> SchemaCache -> ApiRequest -> InvokeMethod -> Either Error CallReadPlan
callReadPlan identifier conf sCache apiRequest@ApiRequest{iPreferences=Preferences{..},..} invMethod = do callReadPlan identifier conf sCache apiRequest@ApiRequest{iPreferences=Preferences{..},..} invMethod = do
@@ -161,12 +158,16 @@ callReadPlan identifier conf sCache apiRequest@ApiRequest{iPreferences=Preferenc
(InvPost, Routine.Immutable) -> SQL.Read (InvPost, Routine.Immutable) -> SQL.Read
(InvPost, Routine.Volatile) -> SQL.Write (InvPost, Routine.Volatile) -> SQL.Write
cPlan = callPlan proc apiRequest paramKeys args rPlan cPlan = callPlan proc apiRequest paramKeys args rPlan
(hdler, mediaType) <- mapLeft ApiRequestError $ negotiateContent conf apiRequest relIdentifier iAcceptMediaType (dbMediaHandlers sCache) (handler, mediaType) <- mapLeft ApiRequestError $ negotiateContent conf apiRequest relIdentifier iAcceptMediaType (dbMediaHandlers sCache) (hasDefaultSelect rPlan)
if not (null invalidPrefs) && preferHandling == Just Strict then Left $ ApiRequestError $ InvalidPreferences invalidPrefs else Right () if not (null invalidPrefs) && preferHandling == Just Strict then Left $ ApiRequestError $ InvalidPreferences invalidPrefs else Right ()
return $ CallReadPlan rPlan cPlan txMode proc hdler mediaType relIdentifier return $ CallReadPlan rPlan cPlan txMode proc handler mediaType
where where
qsParams' = QueryParams.qsParams iQueryParams qsParams' = QueryParams.qsParams iQueryParams
hasDefaultSelect :: ReadPlanTree -> Bool
hasDefaultSelect (Node ReadPlan{select=[CoercibleSelectField{csField=CoercibleField{cfName}}]} []) = cfName == "*"
hasDefaultSelect _ = False
inspectPlan :: ApiRequest -> Either Error InspectPlan inspectPlan :: ApiRequest -> Either Error InspectPlan
inspectPlan apiRequest = do inspectPlan apiRequest = do
let producedMTs = [MTOpenAPI, MTApplicationJSON, MTAny] let producedMTs = [MTOpenAPI, MTApplicationJSON, MTAny]
@@ -993,8 +994,8 @@ addFilterToLogicForest :: CoercibleFilter -> [CoercibleLogicTree] -> [CoercibleL
addFilterToLogicForest flt lf = CoercibleStmnt flt : lf addFilterToLogicForest flt lf = CoercibleStmnt flt : lf
-- | Do content negotiation. i.e. choose a media type based on the intersection of accepted/produced media types. -- | Do content negotiation. i.e. choose a media type based on the intersection of accepted/produced media types.
negotiateContent :: AppConfig -> ApiRequest -> QualifiedIdentifier -> [MediaType] -> MediaHandlerMap -> Either ApiRequestError ResolvedHandler negotiateContent :: AppConfig -> ApiRequest -> QualifiedIdentifier -> [MediaType] -> MediaHandlerMap -> Bool -> Either ApiRequestError ResolvedHandler
negotiateContent conf ApiRequest{iAction=act, iPreferences=Preferences{preferRepresentation=rep}} identifier accepts produces = negotiateContent conf ApiRequest{iAction=act, iPreferences=Preferences{preferRepresentation=rep}} identifier accepts produces defaultSelect =
case (act, firstAcceptedPick) of case (act, firstAcceptedPick) of
(_, Nothing) -> Left . MediaTypeError $ map MediaType.toMime accepts (_, Nothing) -> Left . MediaTypeError $ map MediaType.toMime accepts
(ActionMutate _, Just (x, mt)) -> Right (if rep == Just Full then x else NoAgg, mt) (ActionMutate _, Just (x, mt)) -> Right (if rep == Just Full then x else NoAgg, mt)
@@ -1017,6 +1018,9 @@ negotiateContent conf ApiRequest{iAction=act, iPreferences=Preferences{preferRep
x -> lookupHandler x x -> lookupHandler x
mtPlanToNothing x = if configDbPlanEnabled conf then x else Nothing -- don't find anything if the plan media type is not allowed mtPlanToNothing x = if configDbPlanEnabled conf then x else Nothing -- don't find anything if the plan media type is not allowed
lookupHandler mt = lookupHandler mt =
HM.lookup (RelId identifier, MTAny) produces <|> -- lookup for identifier and `*/*` when' defaultSelect (HM.lookup (RelId identifier, MTAny) produces) <|> -- lookup for identifier and `*/*`
HM.lookup (RelId identifier, mt) produces <|> -- lookup for identifier and a particular media type when' defaultSelect (HM.lookup (RelId identifier, mt) produces) <|> -- lookup for identifier and a particular media type
HM.lookup (RelAnyElement, mt) produces -- lookup for anyelement and a particular media type HM.lookup (RelAnyElement, mt) produces -- lookup for anyelement and a particular media type
when' :: Bool -> Maybe a -> Maybe a
when' True (Just a) = Just a
when' _ _ = Nothing
-3
View File
@@ -70,7 +70,6 @@ readQuery WrappedReadPlan{..} conf@AppConfig{..} apiReq@ApiRequest{iPreferences=
resultSet <- resultSet <-
lift . SQL.statement mempty $ lift . SQL.statement mempty $
Statements.prepareRead Statements.prepareRead
wrIdent
(QueryBuilder.readPlanToQuery wrReadPlan) (QueryBuilder.readPlanToQuery wrReadPlan)
(if preferCount == Just EstimatedCount then (if preferCount == Just EstimatedCount then
-- LIMIT maxRows + 1 so we can determine below that maxRows was surpassed -- LIMIT maxRows + 1 so we can determine below that maxRows was surpassed
@@ -157,7 +156,6 @@ invokeQuery rout CallReadPlan{..} apiReq@ApiRequest{iPreferences=Preferences{..}
resultSet <- resultSet <-
lift . SQL.statement mempty $ lift . SQL.statement mempty $
Statements.prepareCall Statements.prepareCall
crIdent
rout rout
(QueryBuilder.callPlanToQuery crCallPlan pgVer) (QueryBuilder.callPlanToQuery crCallPlan pgVer)
(QueryBuilder.readPlanToQuery crReadPlan) (QueryBuilder.readPlanToQuery crReadPlan)
@@ -196,7 +194,6 @@ writeQuery MutateReadPlan{..} ApiRequest{iPreferences=Preferences{..}} conf =
in in
lift . SQL.statement mempty $ lift . SQL.statement mempty $
Statements.prepareWrite Statements.prepareWrite
mrIdent
(QueryBuilder.readPlanToQuery mrReadPlan) (QueryBuilder.readPlanToQuery mrReadPlan)
(QueryBuilder.mutatePlanToQuery mrMutatePlan) (QueryBuilder.mutatePlanToQuery mrMutatePlan)
isInsert isInsert
+9 -7
View File
@@ -85,7 +85,8 @@ import PostgREST.Plan.Types (CoercibleField (..),
import PostgREST.RangeQuery (NonnegRange, allRange, import PostgREST.RangeQuery (NonnegRange, allRange,
rangeLimit, rangeOffset) rangeLimit, rangeOffset)
import PostgREST.SchemaCache.Identifiers (FieldName, import PostgREST.SchemaCache.Identifiers (FieldName,
QualifiedIdentifier (..)) QualifiedIdentifier (..),
RelIdentifier (..))
import PostgREST.SchemaCache.Routine (MediaHandler (..), import PostgREST.SchemaCache.Routine (MediaHandler (..),
Routine (..), Routine (..),
funcReturnsScalar, funcReturnsScalar,
@@ -221,10 +222,11 @@ asJsonF rout strip
asGeoJsonF :: SQL.Snippet asGeoJsonF :: SQL.Snippet
asGeoJsonF = "json_build_object('type', 'FeatureCollection', 'features', coalesce(json_agg(ST_AsGeoJSON(_postgrest_t)::json), '[]'))" asGeoJsonF = "json_build_object('type', 'FeatureCollection', 'features', coalesce(json_agg(ST_AsGeoJSON(_postgrest_t)::json), '[]'))"
customFuncF :: Maybe Routine -> QualifiedIdentifier -> QualifiedIdentifier -> SQL.Snippet customFuncF :: Maybe Routine -> QualifiedIdentifier -> RelIdentifier -> SQL.Snippet
customFuncF rout funcQi target customFuncF rout funcQi _
| (funcReturnsScalar <$> rout) == Just True = fromQi funcQi <> "(_postgrest_t.pgrst_scalar)" | (funcReturnsScalar <$> rout) == Just True = fromQi funcQi <> "(_postgrest_t.pgrst_scalar)"
| otherwise = fromQi funcQi <> "(_postgrest_t::" <> fromQi target <> ")" customFuncF _ funcQi RelAnyElement = fromQi funcQi <> "(_postgrest_t)"
customFuncF _ funcQi (RelId target) = fromQi funcQi <> "(_postgrest_t::" <> fromQi target <> ")"
locationF :: [Text] -> SQL.Snippet locationF :: [Text] -> SQL.Snippet
locationF pKeys = [qc|( locationF pKeys = [qc|(
@@ -559,12 +561,12 @@ setConfigWithConstantNameJSON prefix keyVals = [setConfigWithConstantName (prefi
arrayByteStringToText :: [(ByteString, ByteString)] -> [(Text,Text)] arrayByteStringToText :: [(ByteString, ByteString)] -> [(Text,Text)]
arrayByteStringToText keyVal = (T.decodeUtf8 *** T.decodeUtf8) <$> keyVal arrayByteStringToText keyVal = (T.decodeUtf8 *** T.decodeUtf8) <$> keyVal
handlerF :: Maybe Routine -> QualifiedIdentifier -> MediaHandler -> SQL.Snippet handlerF :: Maybe Routine -> MediaHandler -> SQL.Snippet
handlerF rout target = \case handlerF rout = \case
BuiltinAggArrayJsonStrip -> asJsonF rout True BuiltinAggArrayJsonStrip -> asJsonF rout True
BuiltinAggSingleJson strip -> asJsonSingleF rout strip BuiltinAggSingleJson strip -> asJsonSingleF rout strip
BuiltinOvAggJson -> asJsonF rout False BuiltinOvAggJson -> asJsonF rout False
BuiltinOvAggGeoJson -> asGeoJsonF BuiltinOvAggGeoJson -> asGeoJsonF
BuiltinOvAggCsv -> asCsvF BuiltinOvAggCsv -> asCsvF
CustomFunc funcQi -> customFuncF rout funcQi target CustomFunc funcQi target -> customFuncF rout funcQi target
NoAgg -> "''::text" NoAgg -> "''::text"
+13 -14
View File
@@ -25,12 +25,11 @@ import qualified Hasql.Statement as SQL
import Control.Lens ((^?)) import Control.Lens ((^?))
import PostgREST.ApiRequest.Preferences import PostgREST.ApiRequest.Preferences
import PostgREST.MediaType (MTVndPlanFormat (..), import PostgREST.MediaType (MTVndPlanFormat (..),
MediaType (..)) MediaType (..))
import PostgREST.Query.SqlFragment import PostgREST.Query.SqlFragment
import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier) import PostgREST.SchemaCache.Routine (MediaHandler (..), Routine,
import PostgREST.SchemaCache.Routine (MediaHandler (..), Routine, funcReturnsSingle)
funcReturnsSingle)
import Protolude import Protolude
@@ -56,9 +55,9 @@ data ResultSet
| RSPlan BS.ByteString -- ^ the plan of the query | RSPlan BS.ByteString -- ^ the plan of the query
prepareWrite :: QualifiedIdentifier -> SQL.Snippet -> SQL.Snippet -> Bool -> Bool -> MediaType -> MediaHandler -> prepareWrite :: SQL.Snippet -> SQL.Snippet -> Bool -> Bool -> MediaType -> MediaHandler ->
Maybe PreferRepresentation -> Maybe PreferResolution -> [Text] -> Bool -> SQL.Statement () ResultSet Maybe PreferRepresentation -> Maybe PreferResolution -> [Text] -> Bool -> SQL.Statement () ResultSet
prepareWrite qi selectQuery mutateQuery isInsert isPut mt handler rep resolution pKeys = prepareWrite selectQuery mutateQuery isInsert isPut mt handler rep resolution pKeys =
SQL.dynamicallyParameterized (mtSnippet mt snippet) decodeIt SQL.dynamicallyParameterized (mtSnippet mt snippet) decodeIt
where where
checkUpsert snip = if isInsert && (isPut || resolution == Just MergeDuplicates) then snip else "''" checkUpsert snip = if isInsert && (isPut || resolution == Just MergeDuplicates) then snip else "''"
@@ -69,7 +68,7 @@ prepareWrite qi selectQuery mutateQuery isInsert isPut mt handler rep resolution
"'' AS total_result_set, " <> "'' AS total_result_set, " <>
"pg_catalog.count(_postgrest_t) AS page_total, " <> "pg_catalog.count(_postgrest_t) AS page_total, " <>
locF <> " AS header, " <> locF <> " AS header, " <>
handlerF Nothing qi handler <> " AS body, " <> handlerF Nothing handler <> " AS body, " <>
responseHeadersF <> " AS response_headers, " <> responseHeadersF <> " AS response_headers, " <>
responseStatusF <> " AS response_status, " <> responseStatusF <> " AS response_status, " <>
pgrstInsertedF <> " AS response_inserted " <> pgrstInsertedF <> " AS response_inserted " <>
@@ -94,8 +93,8 @@ prepareWrite qi selectQuery mutateQuery isInsert isPut mt handler rep resolution
MTVndPlan{} -> planRow MTVndPlan{} -> planRow
_ -> fromMaybe (RSStandard Nothing 0 mempty mempty Nothing Nothing Nothing) <$> HD.rowMaybe (standardRow False) _ -> fromMaybe (RSStandard Nothing 0 mempty mempty Nothing Nothing Nothing) <$> HD.rowMaybe (standardRow False)
prepareRead :: QualifiedIdentifier -> SQL.Snippet -> SQL.Snippet -> Bool -> MediaType -> MediaHandler -> Bool -> SQL.Statement () ResultSet prepareRead :: SQL.Snippet -> SQL.Snippet -> Bool -> MediaType -> MediaHandler -> Bool -> SQL.Statement () ResultSet
prepareRead qi selectQuery countQuery countTotal mt handler = prepareRead selectQuery countQuery countTotal mt handler =
SQL.dynamicallyParameterized (mtSnippet mt snippet) decodeIt SQL.dynamicallyParameterized (mtSnippet mt snippet) decodeIt
where where
snippet = snippet =
@@ -104,7 +103,7 @@ prepareRead qi selectQuery countQuery countTotal mt handler =
"SELECT " <> "SELECT " <>
countResultF <> " AS total_result_set, " <> countResultF <> " AS total_result_set, " <>
"pg_catalog.count(_postgrest_t) AS page_total, " <> "pg_catalog.count(_postgrest_t) AS page_total, " <>
handlerF Nothing qi handler <> " AS body, " <> handlerF Nothing handler <> " AS body, " <>
responseHeadersF <> " AS response_headers, " <> responseHeadersF <> " AS response_headers, " <>
responseStatusF <> " AS response_status, " <> responseStatusF <> " AS response_status, " <>
"''" <> " AS response_inserted " <> "''" <> " AS response_inserted " <>
@@ -117,10 +116,10 @@ prepareRead qi selectQuery countQuery countTotal mt handler =
MTVndPlan{} -> planRow MTVndPlan{} -> planRow
_ -> HD.singleRow $ standardRow True _ -> HD.singleRow $ standardRow True
prepareCall :: QualifiedIdentifier -> Routine -> SQL.Snippet -> SQL.Snippet -> SQL.Snippet -> Bool -> prepareCall :: Routine -> SQL.Snippet -> SQL.Snippet -> SQL.Snippet -> Bool ->
MediaType -> MediaHandler -> Bool -> MediaType -> MediaHandler -> Bool ->
SQL.Statement () ResultSet SQL.Statement () ResultSet
prepareCall qi rout callProcQuery selectQuery countQuery countTotal mt handler = prepareCall rout callProcQuery selectQuery countQuery countTotal mt handler =
SQL.dynamicallyParameterized (mtSnippet mt snippet) decodeIt SQL.dynamicallyParameterized (mtSnippet mt snippet) decodeIt
where where
snippet = snippet =
@@ -131,7 +130,7 @@ prepareCall qi rout callProcQuery selectQuery countQuery countTotal mt handler =
(if funcReturnsSingle rout (if funcReturnsSingle rout
then "1" then "1"
else "pg_catalog.count(_postgrest_t)") <> " AS page_total, " <> else "pg_catalog.count(_postgrest_t)") <> " AS page_total, " <>
handlerF (Just rout) qi handler <> " AS body, " <> handlerF (Just rout) handler <> " AS body, " <>
responseHeadersF <> " AS response_headers, " <> responseHeadersF <> " AS response_headers, " <>
responseStatusF <> " AS response_status, " <> responseStatusF <> " AS response_status, " <>
"''" <> " AS response_inserted " <> "''" <> " AS response_inserted " <>
+3 -1
View File
@@ -1207,7 +1207,9 @@ mediaHandlers pgVer =
decodeMediaHandlers :: HD.Result MediaHandlerMap decodeMediaHandlers :: HD.Result MediaHandlerMap
decodeMediaHandlers = decodeMediaHandlers =
HM.fromList . fmap (\(x, y, z, w) -> ((if isAnyElement y then RelAnyElement else RelId y, z), (CustomFunc x, w)) ) <$> HD.rowList caggRow HM.fromList . fmap (\(x, y, z, w) ->
let rel = if isAnyElement y then RelAnyElement else RelId y
in ((rel, z), (CustomFunc x rel, w)) ) <$> HD.rowList caggRow
where where
caggRow = (,,,) caggRow = (,,,)
<$> (QualifiedIdentifier <$> column HD.text <*> column HD.text) <$> (QualifiedIdentifier <$> column HD.text <*> column HD.text)
+1 -1
View File
@@ -20,7 +20,7 @@ import qualified Data.Text as T
import Protolude import Protolude
data RelIdentifier = RelId QualifiedIdentifier | RelAnyElement data RelIdentifier = RelId QualifiedIdentifier | RelAnyElement
deriving (Eq, Ord, Generic, JSON.ToJSON, JSON.ToJSONKey) deriving (Eq, Ord, Generic, JSON.ToJSON, JSON.ToJSONKey, Show)
instance Hashable RelIdentifier instance Hashable RelIdentifier
-- | Represents a pg identifier with a prepended schema name "schema.table". -- | Represents a pg identifier with a prepended schema name "schema.table".
+1 -1
View File
@@ -108,7 +108,7 @@ data MediaHandler
| BuiltinOvAggGeoJson | BuiltinOvAggGeoJson
| BuiltinOvAggCsv | BuiltinOvAggCsv
-- custom -- custom
| CustomFunc QualifiedIdentifier | CustomFunc QualifiedIdentifier RelIdentifier
| NoAgg | NoAgg
deriving (Eq, Show) deriving (Eq, Show)
@@ -230,6 +230,76 @@ spec = describe "custom media types" $ do
simpleHeaders r `shouldContain` [("Content-Type", "text/csv; charset=utf-8")] simpleHeaders r `shouldContain` [("Content-Type", "text/csv; charset=utf-8")]
simpleHeaders r `shouldContain` [("Content-Disposition", "attachment; filename=\"lines.csv\"")] simpleHeaders r `shouldContain` [("Content-Disposition", "attachment; filename=\"lines.csv\"")]
-- https://github.com/PostgREST/postgrest/issues/3160
context "using select query parameter" $ do
it "without select" $ do
request methodGet "/projects?id=in.(1,2)" (acceptHdrs "pg/outfunc") ""
`shouldRespondWith`
[str|(1,"Windows 7",1)
|(2,"Windows 10",1)
|]
{ matchStatus = 200
, matchHeaders = ["Content-Type" <:> "pg/outfunc"]
}
it "with fewer columns selected" $ do
request methodGet "/projects?id=in.(1,2)&select=id,name" (acceptHdrs "pg/outfunc") ""
`shouldRespondWith`
[str|(1,"Windows 7")
|(2,"Windows 10")
|]
{ matchStatus = 200
, matchHeaders = ["Content-Type" <:> "pg/outfunc"]
}
it "with columns in different order" $ do
request methodGet "/projects?id=in.(1,2)&select=name,id,client_id" (acceptHdrs "pg/outfunc") ""
`shouldRespondWith`
[str|("Windows 7",1,1)
|("Windows 10",2,1)
|]
{ matchStatus = 200
, matchHeaders = ["Content-Type" <:> "pg/outfunc"]
}
it "with computed columns" $ do
request methodGet "/items?id=in.(1,2)&select=id,always_true" (acceptHdrs "pg/outfunc") ""
`shouldRespondWith`
[str|(1,t)
|(2,t)
|]
{ matchStatus = 200
, matchHeaders = ["Content-Type" <:> "pg/outfunc"]
}
-- TODO: Embeddings should not return JSON. Arrays of record would be much better.
it "with embedding" $ do
request methodGet "/projects?id=in.(1,2)&select=*,clients(id)" (acceptHdrs "pg/outfunc") ""
`shouldRespondWith`
[str|(1,"Windows 7",1,"{""id"": 1}")
|(2,"Windows 10",1,"{""id"": 1}")
|]
{ matchStatus = 200
, matchHeaders = ["Content-Type" <:> "pg/outfunc"]
}
it "will fail for specific aggregate with fewer columns" $ do
request methodGet "/lines?select=id" (acceptHdrs "application/vnd.twkb") ""
`shouldRespondWith` 406
it "will fail for specific aggregate with more columns" $ do
request methodGet "/lines?select=id,name,geom,id" (acceptHdrs "application/vnd.twkb") ""
`shouldRespondWith` 406
it "will fail for specific aggregate with columns in different order" $ do
request methodGet "/lines?select=name,id,geom" (acceptHdrs "application/vnd.twkb") ""
`shouldRespondWith` 406
-- This is just because it would be hard to detect this case, so we better error in this case, too.
it "will fail for specific aggregate with columns in same order" $ do
request methodGet "/lines?select=id,name,geom" (acceptHdrs "application/vnd.twkb") ""
`shouldRespondWith` 406
context "any media type" $ do context "any media type" $ do
context "on functions" $ do context "on functions" $ do
it "returns application/json for */* if not explicitly set" $ do it "returns application/json for */* if not explicitly set" $ do
+15 -2
View File
@@ -3550,8 +3550,8 @@ returns "application/vnd.geo2+json" as $$
select (jsonb_build_object('type', 'FeatureCollection', 'hello', 'world'))::"application/vnd.geo2+json"; select (jsonb_build_object('type', 'FeatureCollection', 'hello', 'world'))::"application/vnd.geo2+json";
$$ language sql; $$ language sql;
drop aggregate if exists test.geo2json_agg(anyelement); drop aggregate if exists test.geo2json_agg_any(anyelement);
create aggregate test.geo2json_agg(anyelement) ( create aggregate test.geo2json_agg_any(anyelement) (
initcond = '[]' initcond = '[]'
, stype = "application/vnd.geo2+json" , stype = "application/vnd.geo2+json"
, sfunc = geo2json_trans , sfunc = geo2json_trans
@@ -3755,3 +3755,16 @@ create aggregate test.some_agg (some_numbers) (
create view bad_subquery as create view bad_subquery as
select * from projects where id = (select id from projects); select * from projects where id = (select id from projects);
-- custom generic mimetype
create domain "pg/outfunc" as text;
create function test.outfunc_trans (state text, next anyelement)
returns "pg/outfunc" as $$
select (state || next::text || E'\n')::"pg/outfunc";
$$ language sql;
create aggregate test.outfunc_agg (anyelement) (
initcond = ''
, stype = "pg/outfunc"
, sfunc = outfunc_trans
);