break: remove limited updated/delete feature (#3907)

BREAKING CHANGE

As agreed on https://github.com/PostgREST/postgrest/issues/3013#issuecomment-1770186262,
this removes the limited update/delete feature.

The feature was complicated, largely unused and caused other bugs in
mutations.

It was added in #2195 and #2211.
This commit is contained in:
Steve Chavez
2025-02-12 14:48:08 -05:00
committed by GitHub
parent 94f0edb61a
commit 307692c325
19 changed files with 20 additions and 549 deletions
+1 -2
View File
@@ -218,9 +218,8 @@ getSchema AppConfig{configDbSchemas} hdrs method = do
lookupHeader = flip lookup hdrs
getRanges :: ByteString -> QueryParams -> RequestHeaders -> Either ApiRequestError (NonnegRange, HM.HashMap Text NonnegRange)
getRanges method QueryParams{qsOrder,qsRanges} hdrs
getRanges method QueryParams{qsRanges} hdrs
| isInvalidRange = Left $ InvalidRange (if rangeIsEmpty headerRange then LowerGTUpper else NegativeLimit)
| method `elem` ["PATCH", "DELETE"] && not (null qsRanges) && null qsOrder = Left LimitNoOrderError
| method == "PUT" && topLevelRange /= allRange = Left PutLimitNotAllowedError
| otherwise = Right (topLevelRange, ranges)
where
-2
View File
@@ -79,7 +79,6 @@ data ApiRequestError
| InvalidPreferences [ByteString]
| InvalidRange RangeError
| InvalidRpcMethod ByteString
| LimitNoOrderError
| NotFound
| NoRelBetween Text Text (Maybe Text) Text RelationshipsMap
| NoRpc Text Text [Text] MediaType Bool [QualifiedIdentifier] [Routine]
@@ -94,7 +93,6 @@ data ApiRequestError
| ColumnNotFound Text Text
| GucHeadersError
| GucStatusError
| OffLimitsChangesError Int64 Integer
| PutMatchingPkError
| SingularityError Integer
| PGRSTParseError RaiseError
+2 -15
View File
@@ -84,11 +84,9 @@ instance PgrstError ApiRequestError where
status UnacceptableFilter{} = HTTP.status400
status UnacceptableSchema{} = HTTP.status406
status UnsupportedMethod{} = HTTP.status405
status LimitNoOrderError = HTTP.status400
status ColumnNotFound{} = HTTP.status400
status GucHeadersError = HTTP.status500
status GucStatusError = HTTP.status500
status OffLimitsChangesError{} = HTTP.status400
status PutMatchingPkError = HTTP.status400
status SingularityError{} = HTTP.status406
status PGRSTParseError{} = HTTP.status500
@@ -140,15 +138,6 @@ instance JSON.ToJSON ApiRequestError where
Nothing
(Just $ JSON.String $ "Verify that '" <> resource <> "' is included in the 'select' query parameter.")
toJSON LimitNoOrderError = toJsonPgrstError
ApiRequestErrorCode09 "A 'limit' was applied without an explicit 'order'" Nothing (Just "Apply an 'order' using unique column(s)")
toJSON (OffLimitsChangesError n maxs) = toJsonPgrstError
ApiRequestErrorCode10
"The maximum number of rows allowed to change was surpassed"
(Just $ JSON.String $ T.unwords ["Results contain", show n, "rows changed but the maximum number allowed is", show maxs])
Nothing
toJSON GucHeadersError = toJsonPgrstError
ApiRequestErrorCode11 "response.headers guc must be a JSON array composed of objects with a single key and a string value" Nothing Nothing
@@ -618,8 +607,8 @@ data ErrorCode
| ApiRequestErrorCode06
| ApiRequestErrorCode07
| ApiRequestErrorCode08
| ApiRequestErrorCode09
| ApiRequestErrorCode10
-- | ApiRequestErrorCode09 -- no longer used (used to be mapped to LimitNoOrderError)
-- | ApiRequestErrorCode10 -- no longer used (used to be mapped to OffLimitsChangesError)
| ApiRequestErrorCode11
-- | ApiRequestErrorCode13 -- no longer used (used to be mapped to BinaryFieldError)
| ApiRequestErrorCode12
@@ -668,8 +657,6 @@ buildErrorCode code = case code of
ApiRequestErrorCode06 -> "PGRST106"
ApiRequestErrorCode07 -> "PGRST107"
ApiRequestErrorCode08 -> "PGRST108"
ApiRequestErrorCode09 -> "PGRST109"
ApiRequestErrorCode10 -> "PGRST110"
ApiRequestErrorCode11 -> "PGRST111"
ApiRequestErrorCode12 -> "PGRST112"
ApiRequestErrorCode14 -> "PGRST114"
+2 -3
View File
@@ -937,7 +937,7 @@ mutatePlan mutation qi ApiRequest{iPreferences=Preferences{..}, ..} SchemaCache{
MutationCreate ->
mapRight (\typedColumns -> Insert qi typedColumns body ((,) <$> preferResolution <*> Just confCols) [] returnings pkCols applyDefaults) typedColumnsOrError
MutationUpdate ->
mapRight (\typedColumns -> Update qi typedColumns body combinedLogic iTopLevelRange rootOrder returnings applyDefaults) typedColumnsOrError
mapRight (\typedColumns -> Update qi typedColumns body combinedLogic returnings applyDefaults) typedColumnsOrError
MutationSingleUpsert ->
if null qsLogic &&
qsFilterFields == S.fromList pkCols &&
@@ -948,7 +948,7 @@ mutatePlan mutation qi ApiRequest{iPreferences=Preferences{..}, ..} SchemaCache{
then mapRight (\typedColumns -> Insert qi typedColumns body (Just (MergeDuplicates, pkCols)) combinedLogic returnings mempty False) typedColumnsOrError
else
Left InvalidFilters
MutationDelete -> Right $ Delete qi combinedLogic iTopLevelRange rootOrder returnings
MutationDelete -> Right $ Delete qi combinedLogic returnings
where
ctx = ResolverContext dbTables dbRepresentations qi "json"
confCols = fromMaybe pkCols qsOnConflict
@@ -960,7 +960,6 @@ mutatePlan mutation qi ApiRequest{iPreferences=Preferences{..}, ..} SchemaCache{
tbl = HM.lookup qi dbTables
pkCols = maybe mempty tablePKCols tbl
logic = map (resolveLogicTree ctx . snd) qsLogic
rootOrder = resolveOrder ctx <$> maybe [] snd (find (\(x, _) -> null x) qsOrder)
combinedLogic = foldr (addFilterToLogicForest . resolveFilter ctx) logic qsFiltersRoot
body = payRaw <$> iPayload -- the body is assumed to be json at this stage(ApiRequest validates)
applyDefaults = preferMissing == Just ApplyDefaults
+1 -7
View File
@@ -7,9 +7,7 @@ import qualified Data.ByteString.Lazy as LBS
import PostgREST.ApiRequest.Preferences (PreferResolution)
import PostgREST.Plan.Types (CoercibleField,
CoercibleLogicTree,
CoercibleOrderTerm)
import PostgREST.RangeQuery (NonnegRange)
CoercibleLogicTree)
import PostgREST.SchemaCache.Identifiers (FieldName,
QualifiedIdentifier)
@@ -32,15 +30,11 @@ data MutatePlan
, updCols :: [CoercibleField]
, updBody :: Maybe LBS.ByteString
, where_ :: [CoercibleLogicTree]
, mutRange :: NonnegRange
, mutOrder :: [CoercibleOrderTerm]
, returning :: [FieldName]
, applyDefs :: Bool
}
| Delete
{ in_ :: QualifiedIdentifier
, where_ :: [CoercibleLogicTree]
, mutRange :: NonnegRange
, mutOrder :: [CoercibleOrderTerm]
, returning :: [FieldName]
}
+3 -13
View File
@@ -25,9 +25,9 @@ import qualified PostgREST.AppState as AppState
import qualified PostgREST.Error as Error
import qualified PostgREST.Query.QueryBuilder as QueryBuilder
import qualified PostgREST.Query.Statements as Statements
import qualified PostgREST.RangeQuery as RangeQuery
import qualified PostgREST.SchemaCache as SchemaCache
import PostgREST.ApiRequest (ApiRequest (..),
Mutation (..))
import PostgREST.ApiRequest.Preferences (PreferCount (..),
@@ -135,11 +135,10 @@ actionQuery (DbCrud plan@MutateReadPlan{mrMutation=MutationCreate, ..}) conf api
optionalRollback conf apiReq
pure $ DbCrudResult plan resultSet
actionQuery (DbCrud plan@MutateReadPlan{mrMutation=MutationUpdate, ..}) conf apiReq@ApiRequest{iPreferences=Preferences{..}, ..} _ _ = do
actionQuery (DbCrud plan@MutateReadPlan{mrMutation=MutationUpdate, ..}) conf apiReq@ApiRequest{iPreferences=Preferences{..}} _ _ = do
resultSet <- writeQuery mrReadPlan mrMutatePlan mrMedia mrHandler apiReq conf
failNotSingular mrMedia resultSet
failExceedsMaxAffectedPref (preferMaxAffected,preferHandling) resultSet
failsChangesOffLimits (RangeQuery.rangeLimit iTopLevelRange) resultSet
optionalRollback conf apiReq
pure $ DbCrudResult plan resultSet
@@ -149,11 +148,10 @@ actionQuery (DbCrud plan@MutateReadPlan{mrMutation=MutationSingleUpsert, ..}) co
optionalRollback conf apiReq
pure $ DbCrudResult plan resultSet
actionQuery (DbCrud plan@MutateReadPlan{mrMutation=MutationDelete, ..}) conf apiReq@ApiRequest{iPreferences=Preferences{..}, ..} _ _ = do
actionQuery (DbCrud plan@MutateReadPlan{mrMutation=MutationDelete, ..}) conf apiReq@ApiRequest{iPreferences=Preferences{..}} _ _ = do
resultSet <- writeQuery mrReadPlan mrMutatePlan mrMedia mrHandler apiReq conf
failNotSingular mrMedia resultSet
failExceedsMaxAffectedPref (preferMaxAffected,preferHandling) resultSet
failsChangesOffLimits (RangeQuery.rangeLimit iTopLevelRange) resultSet
optionalRollback conf apiReq
pure $ DbCrudResult plan resultSet
@@ -260,14 +258,6 @@ failExceedsMaxAffectedPref (Just (PreferMaxAffected n), handling) RSStandard{rsQ
lift SQL.condemn
throwError $ Error.ApiRequestError . ApiRequestTypes.MaxAffectedViolationError $ toInteger queryTotal
failsChangesOffLimits :: Maybe Integer -> ResultSet -> DbHandler ()
failsChangesOffLimits _ RSPlan{} = pure ()
failsChangesOffLimits Nothing _ = pure ()
failsChangesOffLimits (Just maxChanges) RSStandard{rsQueryTotal=queryTotal} =
when (queryTotal > fromIntegral maxChanges) $ do
lift SQL.condemn
throwError $ Error.ApiRequestError $ ApiRequestTypes.OffLimitsChangesError queryTotal maxChanges
-- | Set a transaction to roll back if requested
optionalRollback :: AppConfig -> ApiRequest -> DbHandler ()
optionalRollback AppConfig{..} ApiRequest{iPreferences=Preferences{..}} = do
+8 -42
View File
@@ -40,7 +40,6 @@ import PostgREST.Plan.MutatePlan
import PostgREST.Plan.ReadPlan
import PostgREST.Plan.Types
import PostgREST.Query.SqlFragment
import PostgREST.RangeQuery (allRange)
import Protolude
@@ -134,64 +133,31 @@ mutatePlanToQuery (Insert mainQi iCols body onConflict putConditions returnings
cols = intercalateSnippet ", " $ pgFmtIdent . cfName <$> iCols
mergeDups = case onConflict of {Just (MergeDuplicates,_) -> True; _ -> False;}
-- An update without a limit is always filtered with a WHERE
mutatePlanToQuery (Update mainQi uCols body logicForest range ordts returnings applyDefaults)
mutatePlanToQuery (Update mainQi uCols body logicForest returnings applyDefaults)
| null uCols =
-- if there are no columns we cannot do UPDATE table SET {empty}, it'd be invalid syntax
-- selecting an empty resultset from mainQi gives us the column names to prevent errors when using &select=
-- the select has to be based on "returnings" to make computed overloaded functions not throw
"SELECT " <> emptyBodyReturnedColumns <> " FROM " <> fromQi mainQi <> " WHERE false"
| range == allRange =
"UPDATE " <> mainTbl <> " SET " <> nonRangeCols <> " " <>
| otherwise =
"UPDATE " <> mainTbl <> " SET " <> cols <> " " <>
fromJsonBodyF body uCols False False applyDefaults <>
whereLogic <> " " <>
returningF mainQi returnings
| otherwise =
"WITH " <>
"pgrst_update_body AS (" <> fromJsonBodyF body uCols True True applyDefaults <> "), " <>
"pgrst_affected_rows AS (" <>
"SELECT " <> rangeIdF <> " FROM " <> mainTbl <>
whereLogic <> " " <>
orderF mainQi ordts <> " " <>
limitOffsetF range <>
") " <>
"UPDATE " <> mainTbl <> " SET " <> rangeCols <>
"FROM pgrst_affected_rows " <>
"WHERE " <> whereRangeIdF <> " " <>
returningF mainQi returnings
where
whereLogic = if null logicForest then mempty else " WHERE " <> intercalateSnippet " AND " (pgFmtLogicTree mainQi <$> logicForest)
mainTbl = fromQi mainQi
emptyBodyReturnedColumns = if null returnings then "NULL" else intercalateSnippet ", " (pgFmtColumn (QualifiedIdentifier mempty $ qiName mainQi) <$> returnings)
nonRangeCols = intercalateSnippet ", " (pgFmtIdent . cfName <> const " = " <> pgFmtColumn (QualifiedIdentifier mempty "pgrst_body") . cfName <$> uCols)
rangeCols = intercalateSnippet ", " ((\col -> pgFmtIdent (cfName col) <> " = (SELECT " <> pgFmtIdent (cfName col) <> " FROM pgrst_update_body) ") <$> uCols)
(whereRangeIdF, rangeIdF) = mutRangeF mainQi (cfName . coField <$> ordts)
mutatePlanToQuery (Delete mainQi logicForest range ordts returnings)
| range == allRange =
"DELETE FROM " <> fromQi mainQi <> " " <>
whereLogic <> " " <>
returningF mainQi returnings
| otherwise =
"WITH " <>
"pgrst_affected_rows AS (" <>
"SELECT " <> rangeIdF <> " FROM " <> fromQi mainQi <>
whereLogic <> " " <>
orderF mainQi ordts <> " " <>
limitOffsetF range <>
") " <>
"DELETE FROM " <> fromQi mainQi <> " " <>
"USING pgrst_affected_rows " <>
"WHERE " <> whereRangeIdF <> " " <>
returningF mainQi returnings
cols = intercalateSnippet ", " (pgFmtIdent . cfName <> const " = " <> pgFmtColumn (QualifiedIdentifier mempty "pgrst_body") . cfName <$> uCols)
mutatePlanToQuery (Delete mainQi logicForest returnings) =
"DELETE FROM " <> fromQi mainQi <> " " <>
whereLogic <> " " <>
returningF mainQi returnings
where
whereLogic = if null logicForest then mempty else " WHERE " <> intercalateSnippet " AND " (pgFmtLogicTree mainQi <$> logicForest)
(whereRangeIdF, rangeIdF) = mutRangeF mainQi (cfName . coField <$> ordts)
callPlanToQuery :: CallPlan -> PgVersion -> SQL.Snippet
callPlanToQuery (FunctionCall qi params arguments returnsScalar returnsSetOfScalar returnsCompositeAlias returnings) pgVer =
-8
View File
@@ -13,7 +13,6 @@ module PostgREST.Query.SqlFragment
, fromQi
, limitOffsetF
, locationF
, mutRangeF
, orderF
, pgFmtColumn
, pgFmtFilter
@@ -513,13 +512,6 @@ currentSettingF setting =
-- nullif is used because of https://gist.github.com/steve-chavez/8d7033ea5655096903f3b52f8ed09a15
"nullif(current_setting('" <> setting <> "', true), '')"
mutRangeF :: QualifiedIdentifier -> [FieldName] -> (SQL.Snippet, SQL.Snippet)
mutRangeF mainQi rangeId =
(
intercalateSnippet " AND " $ (\col -> pgFmtColumn mainQi col <> " = " <> pgFmtColumn (QualifiedIdentifier mempty "pgrst_affected_rows") col) <$> rangeId
, intercalateSnippet ", " (pgFmtColumn mainQi <$> rangeId)
)
orderF :: QualifiedIdentifier -> [CoercibleOrderTerm] -> SQL.Snippet
orderF _ [] = mempty
orderF qi ordts = "ORDER BY " <> intercalateSnippet ", " (pgFmtOrderTerm qi <$> ordts)
+1 -1
View File
@@ -27,7 +27,7 @@ data Table = Table
, tableDescription :: Maybe Text
-- TODO Find a better way to separate tables and views
, tableIsView :: Bool
-- The following fields identify what can be done on the table/view, they're not related to the privileges granted to it
-- The following fields identify what HTTP verbs can be executed on the table/view, they're not related to the privileges granted to it
, tableInsertable :: Bool
, tableUpdatable :: Bool
, tableDeletable :: Bool