diff --git a/src/PostgREST/ApiRequest.hs b/src/PostgREST/ApiRequest.hs index 4c3136156..ed87bd98f 100644 --- a/src/PostgREST/ApiRequest.hs +++ b/src/PostgREST/ApiRequest.hs @@ -116,8 +116,8 @@ userApiRequest schema req reqBody , iPreferRepresentation = representation , iPreferSingleObjectParameter = singleObject , iPreferCount = hasPrefer "count=exact" - , iPreferResolution = if hasPrefer "resolution=merge-duplicates" then Just MergeDuplicates - else if hasPrefer "resolution=ignore-duplicates" then Just IgnoreDuplicates + , iPreferResolution = if hasPrefer (show MergeDuplicates) then Just MergeDuplicates + else if hasPrefer (show IgnoreDuplicates) then Just IgnoreDuplicates else Nothing , iFilters = filters , iLogic = [(toS k, toS $ fromJust v) | (k,v) <- qParams, isJust v, endingIn ["and", "or"] k ] diff --git a/src/PostgREST/App.hs b/src/PostgREST/App.hs index 8c1df521e..ffe163d02 100644 --- a/src/PostgREST/App.hs +++ b/src/PostgREST/App.hs @@ -157,10 +157,10 @@ app dbStructure proc conf apiRequest = && iPreferRepresentation apiRequest == Full then return $ singularityError (toInteger nRows) else do - let stm = createWriteStatement sq mq + let pkCols = tablePKCols dbStructure tSchema tName + stm = createWriteStatement sq mq (contentType == CTSingularJSON) isSingle - (contentType == CTTextCSV) (iPreferRepresentation apiRequest) - (tablePKCols dbStructure tSchema tName) + (contentType == CTTextCSV) (iPreferRepresentation apiRequest) pkCols row <- H.query (toS pjRaw) stm let (_, _, fs, body) = extractQueryResult row headers = catMaybes [ @@ -172,6 +172,9 @@ app dbStructure proc conf apiRequest = else Nothing , Just . contentRangeH 1 0 $ toInteger <$> if shouldCount then Just nRows else Nothing + , if null pkCols + then Nothing + else (\x -> ("Preference-Applied", show x)) <$> iPreferResolution apiRequest ] return . responseLBS status201 headers $ diff --git a/src/PostgREST/QueryBuilder.hs b/src/PostgREST/QueryBuilder.hs index c3092b99e..69fd5c980 100644 --- a/src/PostgREST/QueryBuilder.hs +++ b/src/PostgREST/QueryBuilder.hs @@ -121,7 +121,7 @@ createWriteStatement selectQuery mutateQuery wantSingle wantHdrs asCsv rep pKeys "'' AS total_result_set", -- when updateing it does not make sense "pg_catalog.count(_postgrest_t) AS page_total", if wantHdrs - then locationF pKeys + then "coalesce(" <> locationF pKeys <> ", " <> noLocationF <> ")" else noLocationF <> " AS header", if rep == Full then bodyF <> " AS body" @@ -278,7 +278,7 @@ requestToQuery schema isParent (DbRead (Node (Select colSelects tbls logicForest requestToQuery schema _ (DbMutate (Insert mainTbl pkCols p@(PayloadJSON _ pType pKeys) onConflct logicForest returnings)) = unwords [ ("WITH " <> ignoredBody) `emptyOnFalse` not payloadIsEmpty, - "INSERT INTO ", fromQi qi, if payloadIsEmpty then " " else "(" <> cols <> ") ", + "INSERT INTO ", fromQi qi, if payloadIsEmpty then " " else "(" <> cols <> ")", case (pType, payloadIsEmpty) of (PJArray _, True) -> "SELECT null WHERE false" (PJObject, True) -> "DEFAULT VALUES" @@ -369,16 +369,12 @@ asBinaryF :: FieldName -> SqlFragment asBinaryF fieldName = "coalesce(string_agg(_postgrest_t." <> pgFmtIdent fieldName <> ", ''), '')" locationF :: [Text] -> SqlFragment -locationF pKeys = - "(" <> - " WITH s AS (SELECT row_to_json(ss) as r from " <> sourceCTEName <> " as ss limit 1)" <> - " SELECT array_agg(json_data.key || '=' || coalesce('eq.' || json_data.value, 'is.null'))" <> - " FROM s, json_each_text(s.r) AS json_data" <> - ( - if null pKeys - then "" - else " WHERE json_data.key IN ('" <> intercalate "','" pKeys <> "')" - ) <> ")" +locationF pKeys = [qc|( + WITH data AS (SELECT row_to_json(_) AS row FROM {sourceCTEName} AS _ LIMIT 1) + SELECT array_agg(json_data.key || '=' || coalesce('eq.' || json_data.value, 'is.null')) + FROM data CROSS JOIN json_each_text(data.row) AS json_data + {("WHERE json_data.key IN ('" <> intercalate "','" pKeys <> "')") `emptyOnFalse` null pKeys} +)|] limitF :: NonnegRange -> SqlFragment limitF r = if r == allRange diff --git a/src/PostgREST/Types.hs b/src/PostgREST/Types.hs index 5e8adb052..00ec9610a 100644 --- a/src/PostgREST/Types.hs +++ b/src/PostgREST/Types.hs @@ -26,7 +26,10 @@ data ApiRequestError = ActionInappropriate | InvalidFilters deriving (Show, Eq) -data PreferResolution = MergeDuplicates | IgnoreDuplicates deriving (Eq, Show) +data PreferResolution = MergeDuplicates | IgnoreDuplicates deriving Eq +instance Show PreferResolution where + show MergeDuplicates = "resolution=merge-duplicates" + show IgnoreDuplicates = "resolution=ignore-duplicates" data DbStructure = DbStructure { dbTables :: [Table] diff --git a/test/Feature/UpsertSpec.hs b/test/Feature/UpsertSpec.hs index 7112ec8b3..3c52b9696 100644 --- a/test/Feature/UpsertSpec.hs +++ b/test/Feature/UpsertSpec.hs @@ -28,7 +28,7 @@ spec = { "name": "C", "rank": 1 } ]|] { matchStatus = 201 - , matchHeaders = [matchContentTypeJson] + , matchHeaders = ["Preference-Applied" <:> "resolution=merge-duplicates", matchContentTypeJson] } it "INSERTs and UPDATEs row on composite pk conflict" $ @@ -41,7 +41,7 @@ spec = { "first_name": "Peter S.", "last_name": "Yang", "salary": "$42,000.00", "company": null, "occupation": null } ]|] { matchStatus = 201 - , matchHeaders = [matchContentTypeJson] + , matchHeaders = ["Preference-Applied" <:> "resolution=merge-duplicates", matchContentTypeJson] } context "when Prefer: resolution=ignore-duplicates is specified" $ do @@ -54,7 +54,7 @@ spec = { "name": "PHP", "rank": 9 } ]|] { matchStatus = 201 - , matchHeaders = [matchContentTypeJson] + , matchHeaders = ["Preference-Applied" <:> "resolution=ignore-duplicates", matchContentTypeJson] } it "INSERTs and ignores rows on composite pk conflict" $ @@ -66,25 +66,40 @@ spec = { "first_name": "Sara M.", "last_name": "Torpey", "salary": "$60,000.00", "company": "Burstein-Applebee", "occupation": "Soil scientist" } ]|] { matchStatus = 201 - , matchHeaders = [matchContentTypeJson] + , matchHeaders = ["Preference-Applied" <:> "resolution=ignore-duplicates", matchContentTypeJson] } it "succeeds if the table has only PK cols and no other cols" $ do request methodPost "/only_pk" [("Prefer", "return=representation"), ("Prefer", "resolution=ignore-duplicates")] [json|[ { "id": 1 }, { "id": 2 }, { "id": 3} ]|] `shouldRespondWith` - [json|[ { "id": 3} ]|] { matchStatus = 201 , matchHeaders = [matchContentTypeJson] } + [json|[ { "id": 3} ]|] + { matchStatus = 201 , + matchHeaders = ["Preference-Applied" <:> "resolution=ignore-duplicates", + matchContentTypeJson] } + request methodPost "/only_pk" [("Prefer", "return=representation"), ("Prefer", "resolution=merge-duplicates")] [json|[ { "id": 1 }, { "id": 2 }, { "id": 4} ]|] `shouldRespondWith` - [json|[ { "id": 1 }, { "id": 2 }, { "id": 4} ]|] { matchStatus = 201 , matchHeaders = [matchContentTypeJson] } + [json|[ { "id": 1 }, { "id": 2 }, { "id": 4} ]|] + { matchStatus = 201 , + matchHeaders = ["Preference-Applied" <:> "resolution=merge-duplicates", + matchContentTypeJson] } - it "succeeds and ignores the Prefer: resolution header if the table has no PK" $ + it "succeeds and ignores the Prefer: resolution header(no Preference-Applied present) if the table has no PK" $ request methodPost "/no_pk" [("Prefer", "return=representation"), ("Prefer", "resolution=merge-duplicates")] [json|[ { "a": "1", "b": "0" } ]|] `shouldRespondWith` [json|[ { "a": "1", "b": "0" } ]|] { matchStatus = 201 , matchHeaders = [matchContentTypeJson] } + it "succeeds if not a single resource is created" $ do + request methodPost "/tiobe_pls" [("Prefer", "return=representation"), ("Prefer", "resolution=ignore-duplicates")] + [json|[ { "name": "Java", "rank": 1 } ]|] `shouldRespondWith` + [json|[]|] { matchStatus = 201 , matchHeaders = [matchContentTypeJson] } + request methodPost "/tiobe_pls" [("Prefer", "return=representation"), ("Prefer", "resolution=ignore-duplicates")] + [json|[ { "name": "Java", "rank": 1 }, { "name": "C", "rank": 2 } ]|] `shouldRespondWith` + [json|[]|] { matchStatus = 201 , matchHeaders = [matchContentTypeJson] } + context "with PUT" $ do context "Restrictions" $ do it "fails if Range is specified" $