Allow query param 'on_conflict=key1,key2,...' to upsert with explicit columns with unique constraint.
This commit is contained in:
committed by
Steve Chavez
parent
e12c1319b6
commit
99b13fa25f
@@ -11,6 +11,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
- #1415, Add support for user defined socket permission via `server-unix-socket-mode` config option - @Dansvidania
|
- #1415, Add support for user defined socket permission via `server-unix-socket-mode` config option - @Dansvidania
|
||||||
- #1383, Add support for HEAD request - @steve-chavez
|
- #1383, Add support for HEAD request - @steve-chavez
|
||||||
- #1378, Add support for `Prefer: count=planned` and `Prefer: count=estimated` on GET /table - @steve-chavez
|
- #1378, Add support for `Prefer: count=planned` and `Prefer: count=estimated` on GET /table - @steve-chavez
|
||||||
|
- #1327, Add support for optional query parameter `on_conflict` to upsert with specified keys for POST - @ykst
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ data ApiRequest = ApiRequest {
|
|||||||
, iFilters :: [(Text, Text)] -- ^ Filters on the result ("id", "eq.10")
|
, iFilters :: [(Text, Text)] -- ^ Filters on the result ("id", "eq.10")
|
||||||
, iLogic :: [(Text, Text)] -- ^ &and and &or parameters used for complex boolean logic
|
, iLogic :: [(Text, Text)] -- ^ &and and &or parameters used for complex boolean logic
|
||||||
, iSelect :: Maybe Text -- ^ &select parameter used to shape the response
|
, iSelect :: Maybe Text -- ^ &select parameter used to shape the response
|
||||||
|
, iOnConflict :: Maybe Text -- ^ &on_conflict parameter used to upsert on specific unique keys
|
||||||
, iColumns :: Maybe Text -- ^ &columns parameter used to shape the payload
|
, iColumns :: Maybe Text -- ^ &columns parameter used to shape the payload
|
||||||
, iOrder :: [(Text, Text)] -- ^ &order parameters for each level
|
, iOrder :: [(Text, Text)] -- ^ &order parameters for each level
|
||||||
, iCanonicalQS :: ByteString -- ^ Alphabetized (canonical) request query string for response URLs
|
, iCanonicalQS :: ByteString -- ^ Alphabetized (canonical) request query string for response URLs
|
||||||
@@ -122,6 +123,7 @@ userApiRequest schema rootSpec req reqBody
|
|||||||
, iFilters = filters
|
, iFilters = filters
|
||||||
, iLogic = [(toS k, toS $ fromJust v) | (k,v) <- qParams, isJust v, endingIn ["and", "or"] k ]
|
, iLogic = [(toS k, toS $ fromJust v) | (k,v) <- qParams, isJust v, endingIn ["and", "or"] k ]
|
||||||
, iSelect = toS <$> join (lookup "select" qParams)
|
, iSelect = toS <$> join (lookup "select" qParams)
|
||||||
|
, iOnConflict = toS <$> join (lookup "on_conflict" qParams)
|
||||||
, iColumns = columns
|
, iColumns = columns
|
||||||
, iOrder = [(toS k, toS $ fromJust v) | (k,v) <- qParams, isJust v, endingIn ["order"] k ]
|
, iOrder = [(toS k, toS $ fromJust v) | (k,v) <- qParams, isJust v, endingIn ["order"] k ]
|
||||||
, iCanonicalQS = toS $ urlEncodeVars
|
, iCanonicalQS = toS $ urlEncodeVars
|
||||||
|
|||||||
@@ -169,7 +169,7 @@ app dbStructure proc cols conf apiRequest =
|
|||||||
else Nothing
|
else Nothing
|
||||||
, Just $ contentRangeH 1 0 $
|
, Just $ contentRangeH 1 0 $
|
||||||
if shouldCount then Just queryTotal else Nothing
|
if shouldCount then Just queryTotal else Nothing
|
||||||
, if null pkCols
|
, if null pkCols && isNothing (iOnConflict apiRequest)
|
||||||
then Nothing
|
then Nothing
|
||||||
else (\x -> ("Preference-Applied", show x)) <$> iPreferResolution apiRequest
|
else (\x -> ("Preference-Applied", show x)) <$> iPreferResolution apiRequest
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -324,7 +324,11 @@ addProperty f (targetNodeName:remainingPath, a) (Node rn forest) =
|
|||||||
mutateRequest :: Schema -> TableName -> ApiRequest -> S.Set FieldName -> [FieldName] -> ReadRequest -> Either Response MutateRequest
|
mutateRequest :: Schema -> TableName -> ApiRequest -> S.Set FieldName -> [FieldName] -> ReadRequest -> Either Response MutateRequest
|
||||||
mutateRequest schema tName apiRequest cols pkCols readReq = mapLeft errorResponseFor $
|
mutateRequest schema tName apiRequest cols pkCols readReq = mapLeft errorResponseFor $
|
||||||
case action of
|
case action of
|
||||||
ActionCreate -> Right $ Insert qi cols ((,) <$> iPreferResolution apiRequest <*> Just pkCols) [] returnings
|
ActionCreate -> do
|
||||||
|
confCols <- case iOnConflict apiRequest of
|
||||||
|
Nothing -> pure pkCols
|
||||||
|
Just param -> pRequestOnConflict param
|
||||||
|
pure $ Insert qi cols ((,) <$> iPreferResolution apiRequest <*> Just confCols) [] returnings
|
||||||
ActionUpdate -> Update qi cols <$> combinedLogic <*> pure returnings
|
ActionUpdate -> Update qi cols <$> combinedLogic <*> pure returnings
|
||||||
ActionSingleUpsert ->
|
ActionSingleUpsert ->
|
||||||
(\flts ->
|
(\flts ->
|
||||||
|
|||||||
@@ -133,6 +133,13 @@ makeParamDefs ti =
|
|||||||
& schema .~ ParamOther ((mempty :: ParamOtherSchema)
|
& schema .~ ParamOther ((mempty :: ParamOtherSchema)
|
||||||
& in_ .~ ParamQuery
|
& in_ .~ ParamQuery
|
||||||
& type_ ?~ SwaggerString))
|
& type_ ?~ SwaggerString))
|
||||||
|
, ("on_conflict", (mempty :: Param)
|
||||||
|
& name .~ "on_conflict"
|
||||||
|
& description ?~ "On Conflict"
|
||||||
|
& required ?~ False
|
||||||
|
& schema .~ ParamOther ((mempty :: ParamOtherSchema)
|
||||||
|
& in_ .~ ParamQuery
|
||||||
|
& type_ ?~ SwaggerString))
|
||||||
, ("order", (mempty :: Param)
|
, ("order", (mempty :: Param)
|
||||||
& name .~ "order"
|
& name .~ "order"
|
||||||
& description ?~ "Ordering"
|
& description ?~ "Ordering"
|
||||||
|
|||||||
@@ -30,6 +30,10 @@ pRequestSelect :: Text -> Either ApiRequestError [Tree SelectItem]
|
|||||||
pRequestSelect selStr =
|
pRequestSelect selStr =
|
||||||
mapError $ parse pFieldForest ("failed to parse select parameter (" <> toS selStr <> ")") (toS selStr)
|
mapError $ parse pFieldForest ("failed to parse select parameter (" <> toS selStr <> ")") (toS selStr)
|
||||||
|
|
||||||
|
pRequestOnConflict :: Text -> Either ApiRequestError [FieldName]
|
||||||
|
pRequestOnConflict oncStr =
|
||||||
|
mapError $ parse pColumns ("failed to parse on_conflict parameter (" <> toS oncStr <> ")") (toS oncStr)
|
||||||
|
|
||||||
pRequestFilter :: (Text, Text) -> Either ApiRequestError (EmbedPath, Filter)
|
pRequestFilter :: (Text, Text) -> Either ApiRequestError (EmbedPath, Filter)
|
||||||
pRequestFilter (k, v) = mapError $ (,) <$> path <*> (Filter <$> fld <*> oper)
|
pRequestFilter (k, v) = mapError $ (,) <$> path <*> (Filter <$> fld <*> oper)
|
||||||
where
|
where
|
||||||
|
|||||||
@@ -49,6 +49,31 @@ spec =
|
|||||||
[json|[]|] `shouldRespondWith`
|
[json|[]|] `shouldRespondWith`
|
||||||
[json|[]|] { matchStatus = 201 , matchHeaders = [matchContentTypeJson] }
|
[json|[]|] { matchStatus = 201 , matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
it "INSERTs and UPDATEs rows on single unique key conflict" $
|
||||||
|
request methodPost "/single_unique?on_conflict=unique_key" [("Prefer", "return=representation"), ("Prefer", "resolution=merge-duplicates")]
|
||||||
|
[json| [
|
||||||
|
{ "unique_key": 1, "value": "B" },
|
||||||
|
{ "unique_key": 2, "value": "C" }
|
||||||
|
]|] `shouldRespondWith` [json| [
|
||||||
|
{ "unique_key": 1, "value": "B" },
|
||||||
|
{ "unique_key": 2, "value": "C" }
|
||||||
|
]|]
|
||||||
|
{ matchStatus = 201
|
||||||
|
, matchHeaders = ["Preference-Applied" <:> "resolution=merge-duplicates", matchContentTypeJson]
|
||||||
|
}
|
||||||
|
|
||||||
|
it "INSERTs and UPDATEs rows on compound unique keys conflict" $
|
||||||
|
request methodPost "/compound_unique?on_conflict=key1,key2" [("Prefer", "return=representation"), ("Prefer", "resolution=merge-duplicates")]
|
||||||
|
[json| [
|
||||||
|
{ "key1": 1, "key2": 1, "value": "B" },
|
||||||
|
{ "key1": 1, "key2": 2, "value": "C" }
|
||||||
|
]|] `shouldRespondWith` [json| [
|
||||||
|
{ "key1": 1, "key2": 1, "value": "B" },
|
||||||
|
{ "key1": 1, "key2": 2, "value": "C" }
|
||||||
|
]|]
|
||||||
|
{ matchStatus = 201
|
||||||
|
, matchHeaders = ["Preference-Applied" <:> "resolution=merge-duplicates", matchContentTypeJson]
|
||||||
|
}
|
||||||
|
|
||||||
context "when Prefer: resolution=ignore-duplicates is specified" $ do
|
context "when Prefer: resolution=ignore-duplicates is specified" $ do
|
||||||
it "INSERTs and ignores rows on pk conflict" $
|
it "INSERTs and ignores rows on pk conflict" $
|
||||||
@@ -75,6 +100,32 @@ spec =
|
|||||||
, matchHeaders = ["Preference-Applied" <:> "resolution=ignore-duplicates", matchContentTypeJson]
|
, matchHeaders = ["Preference-Applied" <:> "resolution=ignore-duplicates", matchContentTypeJson]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
it "INSERTs and ignores rows on single unique key conflict" $
|
||||||
|
request methodPost "/single_unique?on_conflict=unique_key" [("Prefer", "return=representation"), ("Prefer", "resolution=ignore-duplicates")]
|
||||||
|
[json| [
|
||||||
|
{ "unique_key": 1, "value": "B" },
|
||||||
|
{ "unique_key": 2, "value": "C" },
|
||||||
|
{ "unique_key": 3, "value": "D" }
|
||||||
|
]|] `shouldRespondWith` [json| [
|
||||||
|
{ "unique_key": 3, "value": "D" }
|
||||||
|
]|]
|
||||||
|
{ matchStatus = 201
|
||||||
|
, matchHeaders = ["Preference-Applied" <:> "resolution=ignore-duplicates", matchContentTypeJson]
|
||||||
|
}
|
||||||
|
|
||||||
|
it "INSERTs and UPDATEs rows on compound unique keys conflict" $
|
||||||
|
request methodPost "/compound_unique?on_conflict=key1,key2" [("Prefer", "return=representation"), ("Prefer", "resolution=ignore-duplicates")]
|
||||||
|
[json| [
|
||||||
|
{ "key1": 1, "key2": 1, "value": "B" },
|
||||||
|
{ "key1": 1, "key2": 2, "value": "C" },
|
||||||
|
{ "key1": 1, "key2": 3, "value": "D" }
|
||||||
|
]|] `shouldRespondWith` [json| [
|
||||||
|
{ "key1": 1, "key2": 3, "value": "D" }
|
||||||
|
]|]
|
||||||
|
{ matchStatus = 201
|
||||||
|
, matchHeaders = ["Preference-Applied" <:> "resolution=ignore-duplicates", matchContentTypeJson]
|
||||||
|
}
|
||||||
|
|
||||||
it "succeeds if the table has only PK cols and no other cols" $ do
|
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")]
|
request methodPost "/only_pk" [("Prefer", "return=representation"), ("Prefer", "resolution=ignore-duplicates")]
|
||||||
[json|[ { "id": 1 }, { "id": 2 }, { "id": 3} ]|]
|
[json|[ { "id": 1 }, { "id": 2 }, { "id": 3} ]|]
|
||||||
|
|||||||
Vendored
+6
@@ -344,6 +344,12 @@ INSERT INTO employees VALUES
|
|||||||
TRUNCATE TABLE tiobe_pls CASCADE;
|
TRUNCATE TABLE tiobe_pls CASCADE;
|
||||||
INSERT INTO tiobe_pls VALUES ('Java', 1), ('C', 2), ('Python', 4);
|
INSERT INTO tiobe_pls VALUES ('Java', 1), ('C', 2), ('Python', 4);
|
||||||
|
|
||||||
|
TRUNCATE TABLE single_unique CASCADE;
|
||||||
|
INSERT INTO single_unique (unique_key, value) VALUES (1, 'A');
|
||||||
|
|
||||||
|
TRUNCATE TABLE compound_unique CASCADE;
|
||||||
|
INSERT INTO compound_unique (key1, key2, value) VALUES (1, 1, 'A');
|
||||||
|
|
||||||
TRUNCATE TABLE only_pk CASCADE;
|
TRUNCATE TABLE only_pk CASCADE;
|
||||||
INSERT INTO only_pk VALUES (1), (2);
|
INSERT INTO only_pk VALUES (1), (2);
|
||||||
|
|
||||||
|
|||||||
Vendored
+2
@@ -66,6 +66,8 @@ GRANT ALL ON TABLE
|
|||||||
, perf_articles
|
, perf_articles
|
||||||
, employees
|
, employees
|
||||||
, tiobe_pls
|
, tiobe_pls
|
||||||
|
, single_unique
|
||||||
|
, compound_unique
|
||||||
, only_pk
|
, only_pk
|
||||||
, family_tree
|
, family_tree
|
||||||
, managers
|
, managers
|
||||||
|
|||||||
Vendored
+12
@@ -1379,6 +1379,18 @@ create table test.tiobe_pls(
|
|||||||
rank smallint
|
rank smallint
|
||||||
);
|
);
|
||||||
|
|
||||||
|
create table test.single_unique(
|
||||||
|
unique_key integer unique not null,
|
||||||
|
value text
|
||||||
|
);
|
||||||
|
|
||||||
|
create table test.compound_unique(
|
||||||
|
key1 integer not null,
|
||||||
|
key2 integer not null,
|
||||||
|
value text,
|
||||||
|
unique(key1, key2)
|
||||||
|
);
|
||||||
|
|
||||||
create table test.family_tree (
|
create table test.family_tree (
|
||||||
id text not null primary key,
|
id text not null primary key,
|
||||||
name text not null,
|
name text not null,
|
||||||
|
|||||||
Reference in New Issue
Block a user