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:
@@ -37,6 +37,8 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
- Previously, this would silently return 200 - OK on the root endpoint, but don't provide any usable endpoints.
|
- Previously, this would silently return 200 - OK on the root endpoint, but don't provide any usable endpoints.
|
||||||
- #3757, Remove support for `Prefer: params=single-object` - @joelonsql
|
- #3757, Remove support for `Prefer: params=single-object` - @joelonsql
|
||||||
+ This preference was deprecated in favor of Functions with an array of JSON objects
|
+ This preference was deprecated in favor of Functions with an array of JSON objects
|
||||||
|
- #3013, Drop support for Limited updates/deletes
|
||||||
|
+ The feature was complicated and largely unused.
|
||||||
|
|
||||||
## [12.2.8] - 2025-02-10
|
## [12.2.8] - 2025-02-10
|
||||||
|
|
||||||
|
|||||||
@@ -730,31 +730,6 @@ Deletions also support :ref:`prefer_return`, :ref:`resource_embedding` and :ref:
|
|||||||
|
|
||||||
Beware of accidentally deleting all rows in a table. To learn to prevent that see :ref:`block_fulltable`.
|
Beware of accidentally deleting all rows in a table. To learn to prevent that see :ref:`block_fulltable`.
|
||||||
|
|
||||||
.. _limited_update_delete:
|
|
||||||
|
|
||||||
Limited Update/Delete
|
|
||||||
=====================
|
|
||||||
|
|
||||||
You can limit the amount of affected rows by :ref:`update` or :ref:`delete` with the ``limit`` query parameter. For this, you must add an explicit ``order`` on a unique column(s).
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
curl -X PATCH "/users?limit=10&order=id&last_login=lt.2020-01-01" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{ "status": "inactive" }'
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
curl -X DELETE "http://localhost:3000/users?limit=10&order=id&status=eq.inactive"
|
|
||||||
|
|
||||||
If your table has no unique columns, you can use the `ctid <https://www.postgresql.org/docs/current/ddl-system-columns.html>`_ system column.
|
|
||||||
|
|
||||||
Using ``offset`` to target a different subset of rows is also possible.
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
There is no native ``UPDATE...LIMIT`` or ``DELETE...LIMIT`` support in PostgreSQL; the generated query simulates that behavior and is based on `this Crunchy Data blog post <https://www.crunchydata.com/blog/simulating-update-or-delete-with-limit-in-postgres-ctes-to-the-rescue>`_.
|
|
||||||
|
|
||||||
.. raw:: html
|
.. raw:: html
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
|
|||||||
@@ -205,14 +205,6 @@ Related to the HTTP request elements.
|
|||||||
| | | specified in the ``select`` part of the query string. |
|
| | | specified in the ``select`` part of the query string. |
|
||||||
| PGRST108 | | See :ref:`embed_filters`. |
|
| PGRST108 | | See :ref:`embed_filters`. |
|
||||||
+---------------+-------------+-------------------------------------------------------------+
|
+---------------+-------------+-------------------------------------------------------------+
|
||||||
| .. _pgrst109: | 400 | Restricting a Deletion or an Update using limits must |
|
|
||||||
| | | include the ordering of a unique column. |
|
|
||||||
| PGRST109 | | See :ref:`limited_update_delete`. |
|
|
||||||
+---------------+-------------+-------------------------------------------------------------+
|
|
||||||
| .. _pgrst110: | 400 | When restricting a Deletion or an Update using limits |
|
|
||||||
| | | modifies more rows than the maximum specified in the limit. |
|
|
||||||
| PGRST110 | | See :ref:`limited_update_delete`. |
|
|
||||||
+---------------+-------------+-------------------------------------------------------------+
|
|
||||||
| .. _pgrst111: | 500 | An invalid ``response.headers`` was set. |
|
| .. _pgrst111: | 500 | An invalid ``response.headers`` was set. |
|
||||||
| | | See :ref:`guc_resp_hdrs`. |
|
| | | See :ref:`guc_resp_hdrs`. |
|
||||||
| PGRST111 | | |
|
| PGRST111 | | |
|
||||||
|
|||||||
@@ -230,7 +230,6 @@ test-suite spec
|
|||||||
Feature.Query.ErrorSpec
|
Feature.Query.ErrorSpec
|
||||||
Feature.Query.InsertSpec
|
Feature.Query.InsertSpec
|
||||||
Feature.Query.JsonOperatorSpec
|
Feature.Query.JsonOperatorSpec
|
||||||
Feature.Query.LimitedMutationSpec
|
|
||||||
Feature.Query.MultipleSchemaSpec
|
Feature.Query.MultipleSchemaSpec
|
||||||
Feature.Query.NullsStripSpec
|
Feature.Query.NullsStripSpec
|
||||||
Feature.Query.PgSafeUpdateSpec
|
Feature.Query.PgSafeUpdateSpec
|
||||||
|
|||||||
@@ -218,9 +218,8 @@ getSchema AppConfig{configDbSchemas} hdrs method = do
|
|||||||
lookupHeader = flip lookup hdrs
|
lookupHeader = flip lookup hdrs
|
||||||
|
|
||||||
getRanges :: ByteString -> QueryParams -> RequestHeaders -> Either ApiRequestError (NonnegRange, HM.HashMap Text NonnegRange)
|
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)
|
| 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
|
| method == "PUT" && topLevelRange /= allRange = Left PutLimitNotAllowedError
|
||||||
| otherwise = Right (topLevelRange, ranges)
|
| otherwise = Right (topLevelRange, ranges)
|
||||||
where
|
where
|
||||||
|
|||||||
@@ -79,7 +79,6 @@ data ApiRequestError
|
|||||||
| InvalidPreferences [ByteString]
|
| InvalidPreferences [ByteString]
|
||||||
| InvalidRange RangeError
|
| InvalidRange RangeError
|
||||||
| InvalidRpcMethod ByteString
|
| InvalidRpcMethod ByteString
|
||||||
| LimitNoOrderError
|
|
||||||
| NotFound
|
| NotFound
|
||||||
| NoRelBetween Text Text (Maybe Text) Text RelationshipsMap
|
| NoRelBetween Text Text (Maybe Text) Text RelationshipsMap
|
||||||
| NoRpc Text Text [Text] MediaType Bool [QualifiedIdentifier] [Routine]
|
| NoRpc Text Text [Text] MediaType Bool [QualifiedIdentifier] [Routine]
|
||||||
@@ -94,7 +93,6 @@ data ApiRequestError
|
|||||||
| ColumnNotFound Text Text
|
| ColumnNotFound Text Text
|
||||||
| GucHeadersError
|
| GucHeadersError
|
||||||
| GucStatusError
|
| GucStatusError
|
||||||
| OffLimitsChangesError Int64 Integer
|
|
||||||
| PutMatchingPkError
|
| PutMatchingPkError
|
||||||
| SingularityError Integer
|
| SingularityError Integer
|
||||||
| PGRSTParseError RaiseError
|
| PGRSTParseError RaiseError
|
||||||
|
|||||||
+2
-15
@@ -84,11 +84,9 @@ instance PgrstError ApiRequestError where
|
|||||||
status UnacceptableFilter{} = HTTP.status400
|
status UnacceptableFilter{} = HTTP.status400
|
||||||
status UnacceptableSchema{} = HTTP.status406
|
status UnacceptableSchema{} = HTTP.status406
|
||||||
status UnsupportedMethod{} = HTTP.status405
|
status UnsupportedMethod{} = HTTP.status405
|
||||||
status LimitNoOrderError = HTTP.status400
|
|
||||||
status ColumnNotFound{} = HTTP.status400
|
status ColumnNotFound{} = HTTP.status400
|
||||||
status GucHeadersError = HTTP.status500
|
status GucHeadersError = HTTP.status500
|
||||||
status GucStatusError = HTTP.status500
|
status GucStatusError = HTTP.status500
|
||||||
status OffLimitsChangesError{} = HTTP.status400
|
|
||||||
status PutMatchingPkError = HTTP.status400
|
status PutMatchingPkError = HTTP.status400
|
||||||
status SingularityError{} = HTTP.status406
|
status SingularityError{} = HTTP.status406
|
||||||
status PGRSTParseError{} = HTTP.status500
|
status PGRSTParseError{} = HTTP.status500
|
||||||
@@ -140,15 +138,6 @@ instance JSON.ToJSON ApiRequestError where
|
|||||||
Nothing
|
Nothing
|
||||||
(Just $ JSON.String $ "Verify that '" <> resource <> "' is included in the 'select' query parameter.")
|
(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
|
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
|
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
|
| ApiRequestErrorCode06
|
||||||
| ApiRequestErrorCode07
|
| ApiRequestErrorCode07
|
||||||
| ApiRequestErrorCode08
|
| ApiRequestErrorCode08
|
||||||
| ApiRequestErrorCode09
|
-- | ApiRequestErrorCode09 -- no longer used (used to be mapped to LimitNoOrderError)
|
||||||
| ApiRequestErrorCode10
|
-- | ApiRequestErrorCode10 -- no longer used (used to be mapped to OffLimitsChangesError)
|
||||||
| ApiRequestErrorCode11
|
| ApiRequestErrorCode11
|
||||||
-- | ApiRequestErrorCode13 -- no longer used (used to be mapped to BinaryFieldError)
|
-- | ApiRequestErrorCode13 -- no longer used (used to be mapped to BinaryFieldError)
|
||||||
| ApiRequestErrorCode12
|
| ApiRequestErrorCode12
|
||||||
@@ -668,8 +657,6 @@ buildErrorCode code = case code of
|
|||||||
ApiRequestErrorCode06 -> "PGRST106"
|
ApiRequestErrorCode06 -> "PGRST106"
|
||||||
ApiRequestErrorCode07 -> "PGRST107"
|
ApiRequestErrorCode07 -> "PGRST107"
|
||||||
ApiRequestErrorCode08 -> "PGRST108"
|
ApiRequestErrorCode08 -> "PGRST108"
|
||||||
ApiRequestErrorCode09 -> "PGRST109"
|
|
||||||
ApiRequestErrorCode10 -> "PGRST110"
|
|
||||||
ApiRequestErrorCode11 -> "PGRST111"
|
ApiRequestErrorCode11 -> "PGRST111"
|
||||||
ApiRequestErrorCode12 -> "PGRST112"
|
ApiRequestErrorCode12 -> "PGRST112"
|
||||||
ApiRequestErrorCode14 -> "PGRST114"
|
ApiRequestErrorCode14 -> "PGRST114"
|
||||||
|
|||||||
@@ -937,7 +937,7 @@ mutatePlan mutation qi ApiRequest{iPreferences=Preferences{..}, ..} SchemaCache{
|
|||||||
MutationCreate ->
|
MutationCreate ->
|
||||||
mapRight (\typedColumns -> Insert qi typedColumns body ((,) <$> preferResolution <*> Just confCols) [] returnings pkCols applyDefaults) typedColumnsOrError
|
mapRight (\typedColumns -> Insert qi typedColumns body ((,) <$> preferResolution <*> Just confCols) [] returnings pkCols applyDefaults) typedColumnsOrError
|
||||||
MutationUpdate ->
|
MutationUpdate ->
|
||||||
mapRight (\typedColumns -> Update qi typedColumns body combinedLogic iTopLevelRange rootOrder returnings applyDefaults) typedColumnsOrError
|
mapRight (\typedColumns -> Update qi typedColumns body combinedLogic returnings applyDefaults) typedColumnsOrError
|
||||||
MutationSingleUpsert ->
|
MutationSingleUpsert ->
|
||||||
if null qsLogic &&
|
if null qsLogic &&
|
||||||
qsFilterFields == S.fromList pkCols &&
|
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
|
then mapRight (\typedColumns -> Insert qi typedColumns body (Just (MergeDuplicates, pkCols)) combinedLogic returnings mempty False) typedColumnsOrError
|
||||||
else
|
else
|
||||||
Left InvalidFilters
|
Left InvalidFilters
|
||||||
MutationDelete -> Right $ Delete qi combinedLogic iTopLevelRange rootOrder returnings
|
MutationDelete -> Right $ Delete qi combinedLogic returnings
|
||||||
where
|
where
|
||||||
ctx = ResolverContext dbTables dbRepresentations qi "json"
|
ctx = ResolverContext dbTables dbRepresentations qi "json"
|
||||||
confCols = fromMaybe pkCols qsOnConflict
|
confCols = fromMaybe pkCols qsOnConflict
|
||||||
@@ -960,7 +960,6 @@ mutatePlan mutation qi ApiRequest{iPreferences=Preferences{..}, ..} SchemaCache{
|
|||||||
tbl = HM.lookup qi dbTables
|
tbl = HM.lookup qi dbTables
|
||||||
pkCols = maybe mempty tablePKCols tbl
|
pkCols = maybe mempty tablePKCols tbl
|
||||||
logic = map (resolveLogicTree ctx . snd) qsLogic
|
logic = map (resolveLogicTree ctx . snd) qsLogic
|
||||||
rootOrder = resolveOrder ctx <$> maybe [] snd (find (\(x, _) -> null x) qsOrder)
|
|
||||||
combinedLogic = foldr (addFilterToLogicForest . resolveFilter ctx) logic qsFiltersRoot
|
combinedLogic = foldr (addFilterToLogicForest . resolveFilter ctx) logic qsFiltersRoot
|
||||||
body = payRaw <$> iPayload -- the body is assumed to be json at this stage(ApiRequest validates)
|
body = payRaw <$> iPayload -- the body is assumed to be json at this stage(ApiRequest validates)
|
||||||
applyDefaults = preferMissing == Just ApplyDefaults
|
applyDefaults = preferMissing == Just ApplyDefaults
|
||||||
|
|||||||
@@ -7,9 +7,7 @@ import qualified Data.ByteString.Lazy as LBS
|
|||||||
|
|
||||||
import PostgREST.ApiRequest.Preferences (PreferResolution)
|
import PostgREST.ApiRequest.Preferences (PreferResolution)
|
||||||
import PostgREST.Plan.Types (CoercibleField,
|
import PostgREST.Plan.Types (CoercibleField,
|
||||||
CoercibleLogicTree,
|
CoercibleLogicTree)
|
||||||
CoercibleOrderTerm)
|
|
||||||
import PostgREST.RangeQuery (NonnegRange)
|
|
||||||
import PostgREST.SchemaCache.Identifiers (FieldName,
|
import PostgREST.SchemaCache.Identifiers (FieldName,
|
||||||
QualifiedIdentifier)
|
QualifiedIdentifier)
|
||||||
|
|
||||||
@@ -32,15 +30,11 @@ data MutatePlan
|
|||||||
, updCols :: [CoercibleField]
|
, updCols :: [CoercibleField]
|
||||||
, updBody :: Maybe LBS.ByteString
|
, updBody :: Maybe LBS.ByteString
|
||||||
, where_ :: [CoercibleLogicTree]
|
, where_ :: [CoercibleLogicTree]
|
||||||
, mutRange :: NonnegRange
|
|
||||||
, mutOrder :: [CoercibleOrderTerm]
|
|
||||||
, returning :: [FieldName]
|
, returning :: [FieldName]
|
||||||
, applyDefs :: Bool
|
, applyDefs :: Bool
|
||||||
}
|
}
|
||||||
| Delete
|
| Delete
|
||||||
{ in_ :: QualifiedIdentifier
|
{ in_ :: QualifiedIdentifier
|
||||||
, where_ :: [CoercibleLogicTree]
|
, where_ :: [CoercibleLogicTree]
|
||||||
, mutRange :: NonnegRange
|
|
||||||
, mutOrder :: [CoercibleOrderTerm]
|
|
||||||
, returning :: [FieldName]
|
, returning :: [FieldName]
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-13
@@ -25,9 +25,9 @@ import qualified PostgREST.AppState as AppState
|
|||||||
import qualified PostgREST.Error as Error
|
import qualified PostgREST.Error as Error
|
||||||
import qualified PostgREST.Query.QueryBuilder as QueryBuilder
|
import qualified PostgREST.Query.QueryBuilder as QueryBuilder
|
||||||
import qualified PostgREST.Query.Statements as Statements
|
import qualified PostgREST.Query.Statements as Statements
|
||||||
import qualified PostgREST.RangeQuery as RangeQuery
|
|
||||||
import qualified PostgREST.SchemaCache as SchemaCache
|
import qualified PostgREST.SchemaCache as SchemaCache
|
||||||
|
|
||||||
|
|
||||||
import PostgREST.ApiRequest (ApiRequest (..),
|
import PostgREST.ApiRequest (ApiRequest (..),
|
||||||
Mutation (..))
|
Mutation (..))
|
||||||
import PostgREST.ApiRequest.Preferences (PreferCount (..),
|
import PostgREST.ApiRequest.Preferences (PreferCount (..),
|
||||||
@@ -135,11 +135,10 @@ actionQuery (DbCrud plan@MutateReadPlan{mrMutation=MutationCreate, ..}) conf api
|
|||||||
optionalRollback conf apiReq
|
optionalRollback conf apiReq
|
||||||
pure $ DbCrudResult plan resultSet
|
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
|
resultSet <- writeQuery mrReadPlan mrMutatePlan mrMedia mrHandler apiReq conf
|
||||||
failNotSingular mrMedia resultSet
|
failNotSingular mrMedia resultSet
|
||||||
failExceedsMaxAffectedPref (preferMaxAffected,preferHandling) resultSet
|
failExceedsMaxAffectedPref (preferMaxAffected,preferHandling) resultSet
|
||||||
failsChangesOffLimits (RangeQuery.rangeLimit iTopLevelRange) resultSet
|
|
||||||
optionalRollback conf apiReq
|
optionalRollback conf apiReq
|
||||||
pure $ DbCrudResult plan resultSet
|
pure $ DbCrudResult plan resultSet
|
||||||
|
|
||||||
@@ -149,11 +148,10 @@ actionQuery (DbCrud plan@MutateReadPlan{mrMutation=MutationSingleUpsert, ..}) co
|
|||||||
optionalRollback conf apiReq
|
optionalRollback conf apiReq
|
||||||
pure $ DbCrudResult plan resultSet
|
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
|
resultSet <- writeQuery mrReadPlan mrMutatePlan mrMedia mrHandler apiReq conf
|
||||||
failNotSingular mrMedia resultSet
|
failNotSingular mrMedia resultSet
|
||||||
failExceedsMaxAffectedPref (preferMaxAffected,preferHandling) resultSet
|
failExceedsMaxAffectedPref (preferMaxAffected,preferHandling) resultSet
|
||||||
failsChangesOffLimits (RangeQuery.rangeLimit iTopLevelRange) resultSet
|
|
||||||
optionalRollback conf apiReq
|
optionalRollback conf apiReq
|
||||||
pure $ DbCrudResult plan resultSet
|
pure $ DbCrudResult plan resultSet
|
||||||
|
|
||||||
@@ -260,14 +258,6 @@ failExceedsMaxAffectedPref (Just (PreferMaxAffected n), handling) RSStandard{rsQ
|
|||||||
lift SQL.condemn
|
lift SQL.condemn
|
||||||
throwError $ Error.ApiRequestError . ApiRequestTypes.MaxAffectedViolationError $ toInteger queryTotal
|
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
|
-- | Set a transaction to roll back if requested
|
||||||
optionalRollback :: AppConfig -> ApiRequest -> DbHandler ()
|
optionalRollback :: AppConfig -> ApiRequest -> DbHandler ()
|
||||||
optionalRollback AppConfig{..} ApiRequest{iPreferences=Preferences{..}} = do
|
optionalRollback AppConfig{..} ApiRequest{iPreferences=Preferences{..}} = do
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ import PostgREST.Plan.MutatePlan
|
|||||||
import PostgREST.Plan.ReadPlan
|
import PostgREST.Plan.ReadPlan
|
||||||
import PostgREST.Plan.Types
|
import PostgREST.Plan.Types
|
||||||
import PostgREST.Query.SqlFragment
|
import PostgREST.Query.SqlFragment
|
||||||
import PostgREST.RangeQuery (allRange)
|
|
||||||
|
|
||||||
import Protolude
|
import Protolude
|
||||||
|
|
||||||
@@ -134,64 +133,31 @@ mutatePlanToQuery (Insert mainQi iCols body onConflict putConditions returnings
|
|||||||
cols = intercalateSnippet ", " $ pgFmtIdent . cfName <$> iCols
|
cols = intercalateSnippet ", " $ pgFmtIdent . cfName <$> iCols
|
||||||
mergeDups = case onConflict of {Just (MergeDuplicates,_) -> True; _ -> False;}
|
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 returnings applyDefaults)
|
||||||
mutatePlanToQuery (Update mainQi uCols body logicForest range ordts returnings applyDefaults)
|
|
||||||
| null uCols =
|
| null uCols =
|
||||||
-- if there are no columns we cannot do UPDATE table SET {empty}, it'd be invalid syntax
|
-- 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=
|
-- 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
|
-- the select has to be based on "returnings" to make computed overloaded functions not throw
|
||||||
"SELECT " <> emptyBodyReturnedColumns <> " FROM " <> fromQi mainQi <> " WHERE false"
|
"SELECT " <> emptyBodyReturnedColumns <> " FROM " <> fromQi mainQi <> " WHERE false"
|
||||||
|
|
||||||
| range == allRange =
|
| otherwise =
|
||||||
"UPDATE " <> mainTbl <> " SET " <> nonRangeCols <> " " <>
|
"UPDATE " <> mainTbl <> " SET " <> cols <> " " <>
|
||||||
fromJsonBodyF body uCols False False applyDefaults <>
|
fromJsonBodyF body uCols False False applyDefaults <>
|
||||||
whereLogic <> " " <>
|
whereLogic <> " " <>
|
||||||
returningF mainQi returnings
|
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
|
where
|
||||||
whereLogic = if null logicForest then mempty else " WHERE " <> intercalateSnippet " AND " (pgFmtLogicTree mainQi <$> logicForest)
|
whereLogic = if null logicForest then mempty else " WHERE " <> intercalateSnippet " AND " (pgFmtLogicTree mainQi <$> logicForest)
|
||||||
mainTbl = fromQi mainQi
|
mainTbl = fromQi mainQi
|
||||||
emptyBodyReturnedColumns = if null returnings then "NULL" else intercalateSnippet ", " (pgFmtColumn (QualifiedIdentifier mempty $ qiName mainQi) <$> returnings)
|
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)
|
cols = 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
|
|
||||||
|
|
||||||
|
mutatePlanToQuery (Delete mainQi logicForest returnings) =
|
||||||
|
"DELETE FROM " <> fromQi mainQi <> " " <>
|
||||||
|
whereLogic <> " " <>
|
||||||
|
returningF mainQi returnings
|
||||||
where
|
where
|
||||||
whereLogic = if null logicForest then mempty else " WHERE " <> intercalateSnippet " AND " (pgFmtLogicTree mainQi <$> logicForest)
|
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 :: CallPlan -> PgVersion -> SQL.Snippet
|
||||||
callPlanToQuery (FunctionCall qi params arguments returnsScalar returnsSetOfScalar returnsCompositeAlias returnings) pgVer =
|
callPlanToQuery (FunctionCall qi params arguments returnsScalar returnsSetOfScalar returnsCompositeAlias returnings) pgVer =
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ module PostgREST.Query.SqlFragment
|
|||||||
, fromQi
|
, fromQi
|
||||||
, limitOffsetF
|
, limitOffsetF
|
||||||
, locationF
|
, locationF
|
||||||
, mutRangeF
|
|
||||||
, orderF
|
, orderF
|
||||||
, pgFmtColumn
|
, pgFmtColumn
|
||||||
, pgFmtFilter
|
, pgFmtFilter
|
||||||
@@ -513,13 +512,6 @@ currentSettingF setting =
|
|||||||
-- nullif is used because of https://gist.github.com/steve-chavez/8d7033ea5655096903f3b52f8ed09a15
|
-- nullif is used because of https://gist.github.com/steve-chavez/8d7033ea5655096903f3b52f8ed09a15
|
||||||
"nullif(current_setting('" <> setting <> "', true), '')"
|
"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 :: QualifiedIdentifier -> [CoercibleOrderTerm] -> SQL.Snippet
|
||||||
orderF _ [] = mempty
|
orderF _ [] = mempty
|
||||||
orderF qi ordts = "ORDER BY " <> intercalateSnippet ", " (pgFmtOrderTerm qi <$> ordts)
|
orderF qi ordts = "ORDER BY " <> intercalateSnippet ", " (pgFmtOrderTerm qi <$> ordts)
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ data Table = Table
|
|||||||
, tableDescription :: Maybe Text
|
, tableDescription :: Maybe Text
|
||||||
-- TODO Find a better way to separate tables and views
|
-- TODO Find a better way to separate tables and views
|
||||||
, tableIsView :: Bool
|
, 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
|
, tableInsertable :: Bool
|
||||||
, tableUpdatable :: Bool
|
, tableUpdatable :: Bool
|
||||||
, tableDeletable :: Bool
|
, tableDeletable :: Bool
|
||||||
|
|||||||
@@ -1,305 +0,0 @@
|
|||||||
module Feature.Query.LimitedMutationSpec where
|
|
||||||
|
|
||||||
import Data.Aeson.QQ
|
|
||||||
|
|
||||||
import Network.Wai (Application)
|
|
||||||
|
|
||||||
import Network.HTTP.Types
|
|
||||||
import Test.Hspec hiding (pendingWith)
|
|
||||||
import Test.Hspec.Wai
|
|
||||||
import Test.Hspec.Wai.JSON
|
|
||||||
|
|
||||||
import Protolude hiding (get)
|
|
||||||
import SpecHelper
|
|
||||||
|
|
||||||
tblDataBefore = [aesonQQ|[
|
|
||||||
{ "id": 1, "name": "item-1" }
|
|
||||||
, { "id": 2, "name": "item-2" }
|
|
||||||
, { "id": 3, "name": "item-3" }
|
|
||||||
]|]
|
|
||||||
|
|
||||||
spec :: SpecWith ((), Application)
|
|
||||||
spec = do
|
|
||||||
describe "limited delete" $ do
|
|
||||||
it "works with the limit and offset query params" $
|
|
||||||
baseTable "limited_delete_items" "id" tblDataBefore
|
|
||||||
`mutatesWith`
|
|
||||||
requestMutation methodDelete "/limited_delete_items?order=id&limit=1&offset=1" mempty mempty
|
|
||||||
`shouldMutateInto`
|
|
||||||
[json|[
|
|
||||||
{ "id": 1, "name": "item-1" }
|
|
||||||
, { "id": 3, "name": "item-3" }
|
|
||||||
]|]
|
|
||||||
|
|
||||||
it "works with the limit query param plus a filter" $
|
|
||||||
baseTable "limited_delete_items" "id" tblDataBefore
|
|
||||||
`mutatesWith`
|
|
||||||
requestMutation methodDelete "/limited_delete_items?order=id&limit=1&id=gt.1" mempty mempty
|
|
||||||
`shouldMutateInto`
|
|
||||||
[json|[
|
|
||||||
{ "id": 1, "name": "item-1" }
|
|
||||||
, { "id": 3, "name": "item-3" }
|
|
||||||
]|]
|
|
||||||
|
|
||||||
it "fails without an explicit order by" $
|
|
||||||
request methodDelete "/limited_delete_items?limit=1&offset=1"
|
|
||||||
mempty
|
|
||||||
mempty
|
|
||||||
`shouldRespondWith`
|
|
||||||
[json| {
|
|
||||||
"code":"PGRST109",
|
|
||||||
"hint": "Apply an 'order' using unique column(s)",
|
|
||||||
"details": null,
|
|
||||||
"message": "A 'limit' was applied without an explicit 'order'"
|
|
||||||
}|]
|
|
||||||
{ matchStatus = 400 }
|
|
||||||
|
|
||||||
it "fails when not ordering by a unique column" $
|
|
||||||
request methodDelete "/limited_delete_items_wnonuniq_view?order=static&limit=1"
|
|
||||||
mempty
|
|
||||||
mempty
|
|
||||||
`shouldRespondWith`
|
|
||||||
[json| {
|
|
||||||
"code":"PGRST110",
|
|
||||||
"hint": null,
|
|
||||||
"details":"Results contain 3 rows changed but the maximum number allowed is 1",
|
|
||||||
"message":"The maximum number of rows allowed to change was surpassed"
|
|
||||||
}|]
|
|
||||||
{ matchStatus = 400 }
|
|
||||||
|
|
||||||
it "works with views with an explicit order by unique col" $
|
|
||||||
baseTable "limited_delete_items_view" "id" tblDataBefore
|
|
||||||
`mutatesWith`
|
|
||||||
requestMutation methodDelete "/limited_delete_items_view?order=id&limit=1&offset=1" mempty mempty
|
|
||||||
`shouldMutateInto`
|
|
||||||
[json|[
|
|
||||||
{ "id": 1, "name": "item-1" }
|
|
||||||
, { "id": 3, "name": "item-3" }
|
|
||||||
]|]
|
|
||||||
|
|
||||||
it "works with views with an explicit order by composite pk" $
|
|
||||||
baseTable "limited_delete_items_cpk_view" "id" tblDataBefore
|
|
||||||
`mutatesWith`
|
|
||||||
requestMutation methodDelete "/limited_delete_items_cpk_view?order=id,name&limit=1&offset=1" mempty mempty
|
|
||||||
`shouldMutateInto`
|
|
||||||
[json|[
|
|
||||||
{ "id": 1, "name": "item-1" }
|
|
||||||
, { "id": 3, "name": "item-3" }
|
|
||||||
]|]
|
|
||||||
|
|
||||||
it "works on a table without a pk by ordering by 'ctid'" $
|
|
||||||
baseTable "limited_delete_items_no_pk" "id" tblDataBefore
|
|
||||||
`mutatesWith`
|
|
||||||
requestMutation methodDelete "/limited_delete_items_no_pk?order=ctid&limit=1&offset=1" mempty mempty
|
|
||||||
`shouldMutateInto`
|
|
||||||
[json|[
|
|
||||||
{ "id": 1, "name": "item-1" }
|
|
||||||
, { "id": 3, "name": "item-3" }
|
|
||||||
]|]
|
|
||||||
|
|
||||||
it "ignores the Range header" $ do
|
|
||||||
baseTable "limited_delete_items" "id" tblDataBefore
|
|
||||||
`mutatesWith`
|
|
||||||
requestMutation methodDelete "/limited_delete_items"
|
|
||||||
(rangeHdrs (ByteRangeFromTo 0 0)) mempty
|
|
||||||
`shouldMutateInto`
|
|
||||||
[json|[]|]
|
|
||||||
|
|
||||||
baseTable "limited_delete_items" "id" tblDataBefore
|
|
||||||
`mutatesWith`
|
|
||||||
requestMutation methodDelete "/limited_delete_items?id=gte.2"
|
|
||||||
(rangeHdrs (ByteRangeFromTo 0 0)) mempty
|
|
||||||
`shouldMutateInto`
|
|
||||||
[json|[ { "id": 1, "name": "item-1" } ]|]
|
|
||||||
|
|
||||||
it "ignores the Range header and does not do a limited delete" $
|
|
||||||
baseTable "limited_delete_items" "id" tblDataBefore
|
|
||||||
`mutatesWith`
|
|
||||||
requestMutation methodDelete "/limited_delete_items?order=id"
|
|
||||||
(rangeHdrs (ByteRangeFromTo 0 0)) mempty
|
|
||||||
`shouldMutateInto`
|
|
||||||
[json|[]|]
|
|
||||||
|
|
||||||
it "ignores the Range header and does not throw an invalid range error" $
|
|
||||||
baseTable "limited_delete_items" "id" tblDataBefore
|
|
||||||
`mutatesWith`
|
|
||||||
requestMutation methodDelete "/limited_delete_items?order=id&limit=1&offset=1"
|
|
||||||
(rangeHdrs (ByteRangeFromTo 0 0)) mempty
|
|
||||||
`shouldMutateInto`
|
|
||||||
[json|[
|
|
||||||
{ "id": 1, "name": "item-1" }
|
|
||||||
, { "id": 3, "name": "item-3" }
|
|
||||||
]|]
|
|
||||||
|
|
||||||
it "ignores the Range header but not the limit and offset params" $
|
|
||||||
baseTable "limited_delete_items" "id" tblDataBefore
|
|
||||||
`mutatesWith`
|
|
||||||
requestMutation methodDelete "/limited_delete_items?order=id&limit=2&offset=1"
|
|
||||||
(rangeHdrs (ByteRangeFromTo 1 1)) mempty
|
|
||||||
`shouldMutateInto`
|
|
||||||
[json|[
|
|
||||||
{ "id": 1, "name": "item-1" }
|
|
||||||
]|]
|
|
||||||
|
|
||||||
describe "limited update" $ do
|
|
||||||
it "works with the limit query param" $
|
|
||||||
baseTable "limited_update_items" "id" tblDataBefore
|
|
||||||
`mutatesWith`
|
|
||||||
requestMutation methodPatch "/limited_update_items?order=id&limit=2" mempty
|
|
||||||
[json| {"name": "updated-item"} |]
|
|
||||||
`shouldMutateInto`
|
|
||||||
[json|[
|
|
||||||
{ "id": 1, "name": "updated-item" }
|
|
||||||
, { "id": 2, "name": "updated-item" }
|
|
||||||
, { "id": 3, "name": "item-3" }
|
|
||||||
]|]
|
|
||||||
|
|
||||||
it "works with the limit query param plus a filter" $
|
|
||||||
baseTable "limited_update_items" "id" tblDataBefore
|
|
||||||
`mutatesWith`
|
|
||||||
requestMutation methodPatch "/limited_update_items?order=id&limit=1&id=gt.2" mempty
|
|
||||||
[json| {"name": "updated-item"} |]
|
|
||||||
`shouldMutateInto`
|
|
||||||
[json|[
|
|
||||||
{ "id": 1, "name": "item-1" }
|
|
||||||
, { "id": 2, "name": "item-2" }
|
|
||||||
, { "id": 3, "name": "updated-item" }
|
|
||||||
]|]
|
|
||||||
|
|
||||||
it "works with the limit and offset query params" $
|
|
||||||
baseTable "limited_update_items" "id" tblDataBefore
|
|
||||||
`mutatesWith`
|
|
||||||
requestMutation methodPatch "/limited_update_items?order=id&limit=1&offset=1" mempty
|
|
||||||
[json| {"name": "updated-item"} |]
|
|
||||||
`shouldMutateInto`
|
|
||||||
[json|[
|
|
||||||
{ "id": 1, "name": "item-1" }
|
|
||||||
, { "id": 2, "name": "updated-item" }
|
|
||||||
, { "id": 3, "name": "item-3" }
|
|
||||||
]|]
|
|
||||||
|
|
||||||
it "fails without an explicit order by" $
|
|
||||||
request methodPatch "/limited_update_items?limit=1&offset=1"
|
|
||||||
mempty
|
|
||||||
[json| {"name": "updated-item"} |]
|
|
||||||
`shouldRespondWith`
|
|
||||||
[json| {
|
|
||||||
"code":"PGRST109",
|
|
||||||
"hint": "Apply an 'order' using unique column(s)",
|
|
||||||
"details": null,
|
|
||||||
"message": "A 'limit' was applied without an explicit 'order'"
|
|
||||||
}|]
|
|
||||||
{ matchStatus = 400 }
|
|
||||||
|
|
||||||
it "fails when not ordering by a unique column" $
|
|
||||||
request methodPatch "/limited_update_items_wnonuniq_view?order=static&limit=1"
|
|
||||||
mempty
|
|
||||||
[json| {"name": "updated-item"} |]
|
|
||||||
`shouldRespondWith`
|
|
||||||
[json| {
|
|
||||||
"code":"PGRST110",
|
|
||||||
"hint": null,
|
|
||||||
"details":"Results contain 3 rows changed but the maximum number allowed is 1",
|
|
||||||
"message":"The maximum number of rows allowed to change was surpassed"
|
|
||||||
}|]
|
|
||||||
{ matchStatus = 400 }
|
|
||||||
|
|
||||||
it "works with views with an explicit order by unique col" $
|
|
||||||
baseTable "limited_update_items_view" "id" tblDataBefore
|
|
||||||
`mutatesWith`
|
|
||||||
requestMutation methodPatch "/limited_update_items_view?order=id&limit=1&offset=1" mempty
|
|
||||||
[json| {"name": "updated-item"} |]
|
|
||||||
`shouldMutateInto`
|
|
||||||
[json|[
|
|
||||||
{ "id": 1, "name": "item-1" }
|
|
||||||
, { "id": 2, "name": "updated-item" }
|
|
||||||
, { "id": 3, "name": "item-3" }
|
|
||||||
]|]
|
|
||||||
|
|
||||||
it "works with views with an explicit order by composite pk" $
|
|
||||||
baseTable "limited_update_items_cpk_view" "id" tblDataBefore
|
|
||||||
`mutatesWith`
|
|
||||||
requestMutation methodPatch "/limited_update_items_cpk_view?order=id,name&limit=1&offset=1" mempty
|
|
||||||
[json| {"name": "updated-item"} |]
|
|
||||||
`shouldMutateInto`
|
|
||||||
[json|[
|
|
||||||
{ "id": 1, "name": "item-1" }
|
|
||||||
, { "id": 2, "name": "updated-item" }
|
|
||||||
, { "id": 3, "name": "item-3" }
|
|
||||||
]|]
|
|
||||||
|
|
||||||
it "works on a table without a pk by ordering by 'ctid'" $
|
|
||||||
baseTable "limited_update_items_no_pk" "id" tblDataBefore
|
|
||||||
`mutatesWith`
|
|
||||||
requestMutation methodPatch "/limited_update_items_no_pk?order=ctid&limit=1" mempty
|
|
||||||
[json| {"name": "updated-item"} |]
|
|
||||||
`shouldMutateInto`
|
|
||||||
[json|[
|
|
||||||
{ "id": 1, "name": "updated-item" }
|
|
||||||
, { "id": 2, "name": "item-2" }
|
|
||||||
, { "id": 3, "name": "item-3" }
|
|
||||||
]|]
|
|
||||||
|
|
||||||
it "ignores the Range header" $ do
|
|
||||||
baseTable "limited_update_items" "id" tblDataBefore
|
|
||||||
`mutatesWith`
|
|
||||||
requestMutation methodPatch "/limited_update_items"
|
|
||||||
(rangeHdrs (ByteRangeFromTo 0 0))
|
|
||||||
[json| {"name": "updated-item"} |]
|
|
||||||
`shouldMutateInto`
|
|
||||||
[json|[
|
|
||||||
{ "id": 1, "name": "updated-item" }
|
|
||||||
, { "id": 2, "name": "updated-item" }
|
|
||||||
, { "id": 3, "name": "updated-item" }
|
|
||||||
]|]
|
|
||||||
|
|
||||||
baseTable "limited_update_items" "id" tblDataBefore
|
|
||||||
`mutatesWith`
|
|
||||||
requestMutation methodPatch "/limited_update_items?id=gte.2"
|
|
||||||
(rangeHdrs (ByteRangeFromTo 0 0))
|
|
||||||
[json| {"name": "updated-item"} |]
|
|
||||||
`shouldMutateInto`
|
|
||||||
[json|[
|
|
||||||
{ "id": 1, "name": "item-1" }
|
|
||||||
, { "id": 2, "name": "updated-item" }
|
|
||||||
, { "id": 3, "name": "updated-item" }
|
|
||||||
]|]
|
|
||||||
|
|
||||||
it "ignores the Range header and does not do a limited update" $
|
|
||||||
baseTable "limited_update_items" "id" tblDataBefore
|
|
||||||
`mutatesWith`
|
|
||||||
requestMutation methodPatch "/limited_update_items?order=id"
|
|
||||||
(rangeHdrs (ByteRangeFromTo 0 0))
|
|
||||||
[json| {"name": "updated-item"} |]
|
|
||||||
`shouldMutateInto`
|
|
||||||
[json|[
|
|
||||||
{ "id": 1, "name": "updated-item" }
|
|
||||||
, { "id": 2, "name": "updated-item" }
|
|
||||||
, { "id": 3, "name": "updated-item" }
|
|
||||||
]|]
|
|
||||||
|
|
||||||
it "ignores the Range header and does not throw an invalid range error" $
|
|
||||||
baseTable "limited_update_items" "id" tblDataBefore
|
|
||||||
`mutatesWith`
|
|
||||||
requestMutation methodPatch "/limited_update_items?order=id&limit=1&offset=1"
|
|
||||||
(rangeHdrs (ByteRangeFromTo 0 0))
|
|
||||||
[json| {"name": "updated-item"} |]
|
|
||||||
`shouldMutateInto`
|
|
||||||
[json|[
|
|
||||||
{ "id": 1, "name": "item-1" }
|
|
||||||
, { "id": 2, "name": "updated-item" }
|
|
||||||
, { "id": 3, "name": "item-3" }
|
|
||||||
]|]
|
|
||||||
|
|
||||||
it "ignores the Range header but not the limit and offset params" $
|
|
||||||
baseTable "limited_update_items" "id" tblDataBefore
|
|
||||||
`mutatesWith`
|
|
||||||
requestMutation methodPatch "/limited_update_items?order=id&limit=2&offset=1"
|
|
||||||
(rangeHdrs (ByteRangeFromTo 1 1))
|
|
||||||
[json| {"name": "updated-item"} |]
|
|
||||||
`shouldMutateInto`
|
|
||||||
[json|[
|
|
||||||
{ "id": 1, "name": "item-1" }
|
|
||||||
, { "id": 2, "name": "updated-item" }
|
|
||||||
, { "id": 3, "name": "updated-item" }
|
|
||||||
]|]
|
|
||||||
@@ -361,18 +361,6 @@ spec = do
|
|||||||
, matchHeaders = ["Preference-Applied" <:> "missing=default, return=representation"]
|
, matchHeaders = ["Preference-Applied" <:> "missing=default, return=representation"]
|
||||||
}
|
}
|
||||||
|
|
||||||
it "updates with limit/offset using table default values(field-with_sep) when json keys are undefined" $ do
|
|
||||||
request methodPatch "/complex_items?select=id,name&columns=name,field-with_sep&limit=1&offset=2&order=id"
|
|
||||||
[("Prefer", "return=representation"), ("Prefer", "missing=default")]
|
|
||||||
[json|{"name": "Tres"}|]
|
|
||||||
`shouldRespondWith`
|
|
||||||
[json|[
|
|
||||||
{"id":3,"name":"Tres"}
|
|
||||||
]|]
|
|
||||||
{ matchStatus = 200
|
|
||||||
, matchHeaders = ["Preference-Applied" <:> "missing=default, return=representation"]
|
|
||||||
}
|
|
||||||
|
|
||||||
it "updates table default values(field-with_sep) when json keys are undefined" $ do
|
it "updates table default values(field-with_sep) when json keys are undefined" $ do
|
||||||
request methodPatch "/complex_items?id=eq.3&columns=name,field-with_sep"
|
request methodPatch "/complex_items?id=eq.3&columns=name,field-with_sep"
|
||||||
[("Prefer", "return=representation"), ("Prefer", "missing=default")]
|
[("Prefer", "return=representation"), ("Prefer", "missing=default")]
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ import qualified Feature.Query.EmbedInnerJoinSpec
|
|||||||
import qualified Feature.Query.ErrorSpec
|
import qualified Feature.Query.ErrorSpec
|
||||||
import qualified Feature.Query.InsertSpec
|
import qualified Feature.Query.InsertSpec
|
||||||
import qualified Feature.Query.JsonOperatorSpec
|
import qualified Feature.Query.JsonOperatorSpec
|
||||||
import qualified Feature.Query.LimitedMutationSpec
|
|
||||||
import qualified Feature.Query.MultipleSchemaSpec
|
import qualified Feature.Query.MultipleSchemaSpec
|
||||||
import qualified Feature.Query.NullsStripSpec
|
import qualified Feature.Query.NullsStripSpec
|
||||||
import qualified Feature.Query.PgSafeUpdateSpec
|
import qualified Feature.Query.PgSafeUpdateSpec
|
||||||
@@ -269,9 +268,6 @@ main = do
|
|||||||
before forceRollbackApp $
|
before forceRollbackApp $
|
||||||
describe "Feature.RollbackForcedSpec" Feature.RollbackSpec.forced
|
describe "Feature.RollbackForcedSpec" Feature.RollbackSpec.forced
|
||||||
|
|
||||||
before withApp $
|
|
||||||
describe "Feature.Query.LimitedMutationSpec" Feature.Query.LimitedMutationSpec.spec
|
|
||||||
|
|
||||||
-- This test runs with a pre request to enable the pg-safeupdate library per-session.
|
-- This test runs with a pre request to enable the pg-safeupdate library per-session.
|
||||||
-- This needs to run last, because once pg safe update is loaded, it can't be unloaded again.
|
-- This needs to run last, because once pg safe update is loaded, it can't be unloaded again.
|
||||||
before pgSafeUpdateApp $
|
before pgSafeUpdateApp $
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import Text.Regex.TDFA ((=~))
|
|||||||
import Network.HTTP.Types
|
import Network.HTTP.Types
|
||||||
import Test.Hspec
|
import Test.Hspec
|
||||||
import Test.Hspec.Wai
|
import Test.Hspec.Wai
|
||||||
import Test.Hspec.Wai.JSON
|
|
||||||
import Text.Heredoc
|
import Text.Heredoc
|
||||||
|
|
||||||
import Data.String (String)
|
import Data.String (String)
|
||||||
@@ -299,38 +298,6 @@ isErrorFormat s =
|
|||||||
keys = maybe S.empty M.keysSet obj
|
keys = maybe S.empty M.keysSet obj
|
||||||
validKeys = S.fromList ["message", "details", "hint", "code"]
|
validKeys = S.fromList ["message", "details", "hint", "code"]
|
||||||
|
|
||||||
-- | Follows these steps to verify if the table data changed in the db:
|
|
||||||
-- * Verifies the table data in the db before the change
|
|
||||||
-- * Does the mutation
|
|
||||||
-- * Verifies that the table data changed in the db
|
|
||||||
-- * Resets the table with the original data
|
|
||||||
shouldMutateInto :: MutationCheck -> ResponseMatcher -> WaiExpectation ()
|
|
||||||
shouldMutateInto (MutationCheck (BaseTable tblName tblOrd dataBefore) mutation) dataAfter = do
|
|
||||||
get ("/" <> tblName) `shouldRespondWith` [json|#{dataBefore}|]
|
|
||||||
mutation
|
|
||||||
get ("/" <> tblName <> "?order=" <> tblOrd) `shouldRespondWith` dataAfter
|
|
||||||
request methodPost "/rpc/reset_table"
|
|
||||||
[("Prefer", "tx=commit")]
|
|
||||||
[json| {"tbl_name": #{decodeUtf8 tblName}, "tbl_data": #{dataBefore}} |]
|
|
||||||
`shouldRespondWith` 204
|
|
||||||
|
|
||||||
-- | How the base table data will change using the requested mutation
|
|
||||||
mutatesWith :: BaseTable -> WaiExpectation () -> MutationCheck
|
|
||||||
mutatesWith = MutationCheck
|
|
||||||
|
|
||||||
-- | The original table data before it is modified.
|
|
||||||
-- The column order is needed for an accurate comparison after the mutation
|
|
||||||
baseTable :: ByteString -> ByteString -> JSON.Value -> BaseTable
|
|
||||||
baseTable = BaseTable
|
|
||||||
|
|
||||||
-- | The mutation (update/delete) that will be applied to the base table
|
|
||||||
requestMutation :: Method -> ByteString -> [Header] -> BL.ByteString -> WaiExpectation ()
|
|
||||||
requestMutation method path headers body =
|
|
||||||
request method path (("Prefer", "tx=commit") : headers) body `shouldRespondWith` "" { matchStatus = 204 }
|
|
||||||
|
|
||||||
data BaseTable = BaseTable ByteString ByteString JSON.Value
|
|
||||||
data MutationCheck = MutationCheck BaseTable (WaiExpectation ())
|
|
||||||
|
|
||||||
planCost :: SResponse -> Float
|
planCost :: SResponse -> Float
|
||||||
planCost resp =
|
planCost resp =
|
||||||
let res = simpleBody resp ^? nth 0 . key "Plan" . key "Total Cost" in
|
let res = simpleBody resp ^? nth 0 . key "Plan" . key "Total Cost" in
|
||||||
|
|||||||
Vendored
-18
@@ -741,24 +741,6 @@ INSERT INTO test.fav_numbers VALUES (ROW(0.5, 0.5), 'A'), (ROW(0.6, 0.6), 'B');
|
|||||||
TRUNCATE TABLE test.arrays CASCADE;
|
TRUNCATE TABLE test.arrays CASCADE;
|
||||||
INSERT INTO test.arrays VALUES (0, '{1,2,3}', '{{1,2,3},{4,5,6},{7,8,9}}'), (1, '{11,12,13}', '{{11,12,13},{14,15,16},{17,18,19}}');
|
INSERT INTO test.arrays VALUES (0, '{1,2,3}', '{{1,2,3},{4,5,6},{7,8,9}}'), (1, '{11,12,13}', '{{11,12,13},{14,15,16},{17,18,19}}');
|
||||||
|
|
||||||
TRUNCATE TABLE test.limited_update_items CASCADE;
|
|
||||||
INSERT INTO test.limited_update_items VALUES (1, 'item-1'), (2, 'item-2'), (3, 'item-3');
|
|
||||||
|
|
||||||
TRUNCATE TABLE test.limited_update_items_cpk CASCADE;
|
|
||||||
INSERT INTO test.limited_update_items_cpk VALUES (1, 'item-1'), (2, 'item-2'), (3, 'item-3');
|
|
||||||
|
|
||||||
TRUNCATE TABLE test.limited_update_items_no_pk CASCADE;
|
|
||||||
INSERT INTO test.limited_update_items_no_pk VALUES (1, 'item-1'), (2, 'item-2'), (3, 'item-3');
|
|
||||||
|
|
||||||
TRUNCATE TABLE test.limited_delete_items CASCADE;
|
|
||||||
INSERT INTO test.limited_delete_items VALUES (1, 'item-1'), (2, 'item-2'), (3, 'item-3');
|
|
||||||
|
|
||||||
TRUNCATE TABLE test.limited_delete_items_cpk CASCADE;
|
|
||||||
INSERT INTO test.limited_delete_items_cpk VALUES (1, 'item-1'), (2, 'item-2'), (3, 'item-3');
|
|
||||||
|
|
||||||
TRUNCATE TABLE test.limited_delete_items_no_pk CASCADE;
|
|
||||||
INSERT INTO test.limited_delete_items_no_pk VALUES (1, 'item-1'), (2, 'item-2'), (3, 'item-3');
|
|
||||||
|
|
||||||
TRUNCATE TABLE test.xmltest CASCADE;
|
TRUNCATE TABLE test.xmltest CASCADE;
|
||||||
INSERT INTO test.xmltest VALUES
|
INSERT INTO test.xmltest VALUES
|
||||||
(1, '<myxml>foo</myxml>'),
|
(1, '<myxml>foo</myxml>'),
|
||||||
|
|||||||
Vendored
-50
@@ -2512,56 +2512,6 @@ CREATE AGGREGATE test.unsupported_agg (*) (
|
|||||||
STYPE = int
|
STYPE = int
|
||||||
);
|
);
|
||||||
|
|
||||||
create table limited_update_items(
|
|
||||||
id int primary key
|
|
||||||
, name text
|
|
||||||
);
|
|
||||||
|
|
||||||
create table limited_update_items_cpk(
|
|
||||||
id int
|
|
||||||
, name text
|
|
||||||
, primary key (id, name)
|
|
||||||
);
|
|
||||||
|
|
||||||
create table limited_update_items_no_pk(
|
|
||||||
id int
|
|
||||||
, name text
|
|
||||||
);
|
|
||||||
|
|
||||||
create view limited_update_items_view as
|
|
||||||
select * from limited_update_items;
|
|
||||||
|
|
||||||
create view limited_update_items_wnonuniq_view as
|
|
||||||
select *, 'static'::text as static from limited_update_items;
|
|
||||||
|
|
||||||
create view limited_update_items_cpk_view as
|
|
||||||
select * from limited_update_items_cpk;
|
|
||||||
|
|
||||||
create table limited_delete_items(
|
|
||||||
id int primary key
|
|
||||||
, name text
|
|
||||||
);
|
|
||||||
|
|
||||||
create table limited_delete_items_cpk(
|
|
||||||
id int
|
|
||||||
, name text
|
|
||||||
, primary key (id, name)
|
|
||||||
);
|
|
||||||
|
|
||||||
create table limited_delete_items_no_pk(
|
|
||||||
id int
|
|
||||||
, name text
|
|
||||||
);
|
|
||||||
|
|
||||||
create view limited_delete_items_view as
|
|
||||||
select * from limited_delete_items;
|
|
||||||
|
|
||||||
create view limited_delete_items_wnonuniq_view as
|
|
||||||
select *, 'static'::text as static from limited_delete_items;
|
|
||||||
|
|
||||||
create view limited_delete_items_cpk_view as
|
|
||||||
select * from limited_delete_items_cpk;
|
|
||||||
|
|
||||||
create function reset_table(tbl_name text default '', tbl_data json default '[]') returns void as $_$ begin
|
create function reset_table(tbl_name text default '', tbl_data json default '[]') returns void as $_$ begin
|
||||||
execute format(
|
execute format(
|
||||||
$$
|
$$
|
||||||
|
|||||||
Reference in New Issue
Block a user