disallow limit/offset on views
This commit is contained in:
committed by
Steve Chavez
parent
73dc2692b1
commit
f3f9030c48
+2
-1
@@ -21,7 +21,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
- #2156, Allow applying `limit/offset` to UPDATE/DELETE to only affect a subset of rows - @steve-chavez
|
- #2156, Allow applying `limit/offset` to UPDATE/DELETE to only affect a subset of rows - @steve-chavez
|
||||||
+ Uses the table primary key, so it needs a select privilege on the primary key columns
|
+ Uses the table primary key, so it needs a select privilege on the primary key columns
|
||||||
+ If no primary key is available, it will fallback to using the "ctid" system column(will also require a select privilege on it)
|
+ If no primary key is available, it will fallback to using the "ctid" system column(will also require a select privilege on it)
|
||||||
+ Will work on views if the PK(or "ctid") is present on its SELECT clause
|
+ Doesn't work on views and it will throw an error if tried
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
@@ -50,6 +50,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
+ Previously, those RPCs would return "null" as a body with Content-Type: application/json.
|
+ Previously, those RPCs would return "null" as a body with Content-Type: application/json.
|
||||||
- #2156, `limit/offset` now limits the affected rows on UPDATE/DELETE - @steve-chavez
|
- #2156, `limit/offset` now limits the affected rows on UPDATE/DELETE - @steve-chavez
|
||||||
+ Previously, `limit/offset` only limited the returned rows but not the actual updated rows
|
+ Previously, `limit/offset` only limited the returned rows but not the actual updated rows
|
||||||
|
- #2156, using PATCH/DELETE with `limit/offset` throws an error on views - @steve-chavez
|
||||||
- #2155, `max-rows` is no longer applied on POST/PATCH/PUT/DELETE returned rows - @steve-chavez
|
- #2155, `max-rows` is no longer applied on POST/PATCH/PUT/DELETE returned rows - @steve-chavez
|
||||||
+ This was misleading because the affected rows were not really affected by `max-rows`, only the returned rows were limited
|
+ This was misleading because the affected rows were not really affected by `max-rows`, only the returned rows were limited
|
||||||
|
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ import PostgREST.Config (AppConfig (..),
|
|||||||
import PostgREST.Config.PgVersion (PgVersion (..))
|
import PostgREST.Config.PgVersion (PgVersion (..))
|
||||||
import PostgREST.ContentType (ContentType (..))
|
import PostgREST.ContentType (ContentType (..))
|
||||||
import PostgREST.DbStructure (DbStructure (..),
|
import PostgREST.DbStructure (DbStructure (..),
|
||||||
findTable,
|
findIfView, findTable,
|
||||||
tablePKCols)
|
tablePKCols)
|
||||||
import PostgREST.DbStructure.Identifiers (FieldName,
|
import PostgREST.DbStructure.Identifiers (FieldName,
|
||||||
QualifiedIdentifier (..),
|
QualifiedIdentifier (..),
|
||||||
@@ -346,7 +346,10 @@ handleCreate identifier@QualifiedIdentifier{..} context@RequestContext{..} = do
|
|||||||
response HTTP.status201 headers mempty
|
response HTTP.status201 headers mempty
|
||||||
|
|
||||||
handleUpdate :: QualifiedIdentifier -> RequestContext -> DbHandler Wai.Response
|
handleUpdate :: QualifiedIdentifier -> RequestContext -> DbHandler Wai.Response
|
||||||
handleUpdate identifier context@(RequestContext _ _ ApiRequest{..} _) = do
|
handleUpdate identifier context@(RequestContext _ ctxDbStructure ApiRequest{..} _) = do
|
||||||
|
when (iTopLevelRange /= RangeQuery.allRange && findIfView identifier (dbTables ctxDbStructure)) $
|
||||||
|
throwError $ Error.NotImplemented "limit/offset is not implemented for views"
|
||||||
|
|
||||||
WriteQueryResult{..} <- writeQuery MutationUpdate identifier False mempty context
|
WriteQueryResult{..} <- writeQuery MutationUpdate identifier False mempty context
|
||||||
|
|
||||||
let
|
let
|
||||||
@@ -392,7 +395,10 @@ handleSingleUpsert identifier context@(RequestContext _ _ ApiRequest{..} _) = do
|
|||||||
response HTTP.status204 [] mempty
|
response HTTP.status204 [] mempty
|
||||||
|
|
||||||
handleDelete :: QualifiedIdentifier -> RequestContext -> DbHandler Wai.Response
|
handleDelete :: QualifiedIdentifier -> RequestContext -> DbHandler Wai.Response
|
||||||
handleDelete identifier context@(RequestContext _ _ ApiRequest{..} _) = do
|
handleDelete identifier context@(RequestContext _ ctxDbStructure ApiRequest{..} _) = do
|
||||||
|
when (iTopLevelRange /= RangeQuery.allRange && findIfView identifier (dbTables ctxDbStructure)) $
|
||||||
|
throwError $ Error.NotImplemented "limit/offset is not implemented for views"
|
||||||
|
|
||||||
WriteQueryResult{..} <- writeQuery MutationDelete identifier False mempty context
|
WriteQueryResult{..} <- writeQuery MutationDelete identifier False mempty context
|
||||||
|
|
||||||
let
|
let
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ module PostgREST.DbStructure
|
|||||||
, queryDbStructure
|
, queryDbStructure
|
||||||
, accessibleTables
|
, accessibleTables
|
||||||
, accessibleProcs
|
, accessibleProcs
|
||||||
|
, findIfView
|
||||||
, findTable
|
, findTable
|
||||||
, schemaDescription
|
, schemaDescription
|
||||||
, tableCols
|
, tableCols
|
||||||
@@ -80,7 +81,10 @@ tablePKCols :: DbStructure -> Schema -> TableName -> [Text]
|
|||||||
tablePKCols dbs tSchema tName = pkName <$> filter (\pk -> tSchema == (tableSchema . pkTable) pk && tName == (tableName . pkTable) pk) (dbPrimaryKeys dbs)
|
tablePKCols dbs tSchema tName = pkName <$> filter (\pk -> tSchema == (tableSchema . pkTable) pk && tName == (tableName . pkTable) pk) (dbPrimaryKeys dbs)
|
||||||
|
|
||||||
findTable :: Schema -> TableName -> [Table] -> Maybe Table
|
findTable :: Schema -> TableName -> [Table] -> Maybe Table
|
||||||
findTable tSchema tName tbls = find (\tbl -> tableName tbl == tName && tableSchema tbl == tSchema) tbls
|
findTable tSchema tName = find (\tbl -> tableSchema tbl == tSchema && tableName tbl == tName)
|
||||||
|
|
||||||
|
findIfView :: QualifiedIdentifier -> [Table] -> Bool
|
||||||
|
findIfView identifier tbls = maybe False tableIsView (findTable (qiSchema identifier) (qiName identifier) tbls)
|
||||||
|
|
||||||
-- | The source table column a view column refers to
|
-- | The source table column a view column refers to
|
||||||
type SourceColumn = (Column, ViewColumn)
|
type SourceColumn = (Column, ViewColumn)
|
||||||
@@ -138,6 +142,7 @@ decodeTables =
|
|||||||
<*> column HD.bool
|
<*> column HD.bool
|
||||||
<*> column HD.bool
|
<*> column HD.bool
|
||||||
<*> column HD.bool
|
<*> column HD.bool
|
||||||
|
<*> column HD.bool
|
||||||
|
|
||||||
decodeColumns :: [Table] -> HD.Result [Column]
|
decodeColumns :: [Table] -> HD.Result [Column]
|
||||||
decodeColumns tables =
|
decodeColumns tables =
|
||||||
@@ -337,6 +342,7 @@ accessibleTables pgVer =
|
|||||||
n.nspname as table_schema,
|
n.nspname as table_schema,
|
||||||
relname as table_name,
|
relname as table_name,
|
||||||
d.description as table_description,
|
d.description as table_description,
|
||||||
|
c.relkind IN ('v','m') as is_view,
|
||||||
(
|
(
|
||||||
c.relkind IN ('r','p')
|
c.relkind IN ('r','p')
|
||||||
OR (
|
OR (
|
||||||
@@ -472,6 +478,7 @@ allTables pgVer =
|
|||||||
n.nspname AS table_schema,
|
n.nspname AS table_schema,
|
||||||
c.relname AS table_name,
|
c.relname AS table_name,
|
||||||
d.description AS table_description,
|
d.description AS table_description,
|
||||||
|
c.relkind IN ('v','m') as is_view,
|
||||||
(
|
(
|
||||||
c.relkind IN ('r','p')
|
c.relkind IN ('r','p')
|
||||||
OR (
|
OR (
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ data Table = Table
|
|||||||
{ tableSchema :: Schema
|
{ tableSchema :: Schema
|
||||||
, tableName :: TableName
|
, tableName :: TableName
|
||||||
, tableDescription :: Maybe Text
|
, 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 can be done on the table/view, they're not related to the privileges granted to it
|
||||||
, tableInsertable :: Bool
|
, tableInsertable :: Bool
|
||||||
, tableUpdatable :: Bool
|
, tableUpdatable :: Bool
|
||||||
|
|||||||
@@ -314,6 +314,7 @@ data Error
|
|||||||
| JwtTokenInvalid Text
|
| JwtTokenInvalid Text
|
||||||
| JwtTokenMissing
|
| JwtTokenMissing
|
||||||
| JwtTokenRequired
|
| JwtTokenRequired
|
||||||
|
| NotImplemented Text
|
||||||
| NoSchemaCacheError
|
| NoSchemaCacheError
|
||||||
| NotFound
|
| NotFound
|
||||||
| PgErr PgError
|
| PgErr PgError
|
||||||
@@ -335,6 +336,7 @@ instance PgrstError Error where
|
|||||||
status (PgErr err) = status err
|
status (PgErr err) = status err
|
||||||
status PutMatchingPkError = HTTP.status400
|
status PutMatchingPkError = HTTP.status400
|
||||||
status PutRangeNotAllowedError = HTTP.status400
|
status PutRangeNotAllowedError = HTTP.status400
|
||||||
|
status (NotImplemented _) = HTTP.status501
|
||||||
status SingularityError{} = HTTP.status406
|
status SingularityError{} = HTTP.status406
|
||||||
status UnsupportedVerb{} = HTTP.status405
|
status UnsupportedVerb{} = HTTP.status405
|
||||||
|
|
||||||
@@ -407,6 +409,12 @@ instance JSON.ToJSON Error where
|
|||||||
"details" .= JSON.Null,
|
"details" .= JSON.Null,
|
||||||
"hint" .= JSON.Null]
|
"hint" .= JSON.Null]
|
||||||
|
|
||||||
|
toJSON (NotImplemented msg) = JSON.object [
|
||||||
|
"code" .= GeneralErrorCode07,
|
||||||
|
"message" .= msg,
|
||||||
|
"details" .= JSON.Null,
|
||||||
|
"hint" .= JSON.Null]
|
||||||
|
|
||||||
toJSON NotFound = JSON.object []
|
toJSON NotFound = JSON.object []
|
||||||
toJSON (PgErr err) = JSON.toJSON err
|
toJSON (PgErr err) = JSON.toJSON err
|
||||||
toJSON (ApiRequestError err) = JSON.toJSON err
|
toJSON (ApiRequestError err) = JSON.toJSON err
|
||||||
@@ -460,6 +468,7 @@ data ErrorCode
|
|||||||
| GeneralErrorCode04
|
| GeneralErrorCode04
|
||||||
| GeneralErrorCode05
|
| GeneralErrorCode05
|
||||||
| GeneralErrorCode06
|
| GeneralErrorCode06
|
||||||
|
| GeneralErrorCode07
|
||||||
|
|
||||||
instance JSON.ToJSON ErrorCode where
|
instance JSON.ToJSON ErrorCode where
|
||||||
toJSON e = JSON.toJSON (buildErrorCode e)
|
toJSON e = JSON.toJSON (buildErrorCode e)
|
||||||
@@ -504,3 +513,4 @@ buildErrorCode code = "PGRST" <> case code of
|
|||||||
GeneralErrorCode04 -> "504"
|
GeneralErrorCode04 -> "504"
|
||||||
GeneralErrorCode05 -> "505"
|
GeneralErrorCode05 -> "505"
|
||||||
GeneralErrorCode06 -> "506"
|
GeneralErrorCode06 -> "506"
|
||||||
|
GeneralErrorCode07 -> "507"
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ spec =
|
|||||||
[json| {
|
[json| {
|
||||||
"tableName": "orders_view", "tableSchema": "test",
|
"tableName": "orders_view", "tableSchema": "test",
|
||||||
"tableDeletable": true, "tableUpdatable": true,
|
"tableDeletable": true, "tableUpdatable": true,
|
||||||
"tableInsertable": true, "tableDescription": null
|
"tableIsView":true, "tableInsertable": true,
|
||||||
|
"tableDescription": null
|
||||||
} |]
|
} |]
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ module Feature.Query.DeleteSpec where
|
|||||||
import Network.Wai (Application)
|
import Network.Wai (Application)
|
||||||
|
|
||||||
import Network.HTTP.Types
|
import Network.HTTP.Types
|
||||||
import Test.Hspec
|
import Test.Hspec hiding (pendingWith)
|
||||||
import Test.Hspec.Wai
|
import Test.Hspec.Wai
|
||||||
import Test.Hspec.Wai.JSON
|
import Test.Hspec.Wai.JSON
|
||||||
|
|
||||||
@@ -213,7 +213,16 @@ spec =
|
|||||||
`shouldRespondWith` ""
|
`shouldRespondWith` ""
|
||||||
{ matchStatus = 204 }
|
{ matchStatus = 204 }
|
||||||
|
|
||||||
|
it "doesn't work with views" $
|
||||||
|
request methodDelete "/limited_delete_items_view?limit=1&offset=1"
|
||||||
|
[("Prefer", "tx=commit")]
|
||||||
|
mempty
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json| {"hint":null,"details":null,"code":"PGRST507","message":"limit/offset is not implemented for views"} |]
|
||||||
|
{ matchStatus = 501 }
|
||||||
|
|
||||||
it "works with views with an inferred pk" $ do
|
it "works with views with an inferred pk" $ do
|
||||||
|
pendingWith "not implemented yet"
|
||||||
get "/limited_delete_items_view"
|
get "/limited_delete_items_view"
|
||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
[json|[
|
[json|[
|
||||||
|
|||||||
@@ -520,7 +520,16 @@ spec = do
|
|||||||
`shouldRespondWith` ""
|
`shouldRespondWith` ""
|
||||||
{ matchStatus = 204 }
|
{ matchStatus = 204 }
|
||||||
|
|
||||||
|
it "doesn't work with views" $
|
||||||
|
request methodPatch "/limited_update_items_view?limit=1&offset=1"
|
||||||
|
[("Prefer", "tx=commit")]
|
||||||
|
[json| {"name": "updated-item"} |]
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json| {"hint":null,"details":null,"code":"PGRST507","message":"limit/offset is not implemented for views"} |]
|
||||||
|
{ matchStatus = 501 }
|
||||||
|
|
||||||
it "works with views with an inferred pk" $ do
|
it "works with views with an inferred pk" $ do
|
||||||
|
pendingWith "not implemented yet"
|
||||||
get "/limited_update_items_view"
|
get "/limited_update_items_view"
|
||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
[json|[
|
[json|[
|
||||||
|
|||||||
Reference in New Issue
Block a user