fix: use index on jsonb/jsonb arrow filter/order
This commit is contained in:
committed by
Steve Chavez
parent
e332f038ef
commit
28d5278d62
@@ -29,6 +29,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
- #2834, Fix compilation on Ubuntu by being compatible with GHC 9.0.2 - @steve-chavez
|
- #2834, Fix compilation on Ubuntu by being compatible with GHC 9.0.2 - @steve-chavez
|
||||||
- #2840, Fix `Prefer: missing=default` with DOMAIN default values - @steve-chavez
|
- #2840, Fix `Prefer: missing=default` with DOMAIN default values - @steve-chavez
|
||||||
- #2849, Fix HEAD unnecessarily executing aggregates - @steve-chavez
|
- #2849, Fix HEAD unnecessarily executing aggregates - @steve-chavez
|
||||||
|
- #2594, Fix unused index on jsonb/jsonb arrow filter and order (``/bets?data->>contractId=eq.1`` and ``/bets?order=data->>contractId``) - @steve-chavez
|
||||||
|
|
||||||
## [11.1.0] - 2023-06-07
|
## [11.1.0] - 2023-06-07
|
||||||
|
|
||||||
|
|||||||
+25
-19
@@ -219,7 +219,7 @@ data ResolverContext = ResolverContext
|
|||||||
}
|
}
|
||||||
|
|
||||||
resolveColumnField :: Column -> CoercibleField
|
resolveColumnField :: Column -> CoercibleField
|
||||||
resolveColumnField col = CoercibleField (colName col) mempty (colNominalType col) Nothing (colDefault col)
|
resolveColumnField col = CoercibleField (colName col) mempty False (colNominalType col) Nothing (colDefault col)
|
||||||
|
|
||||||
resolveTableFieldName :: Table -> FieldName -> CoercibleField
|
resolveTableFieldName :: Table -> FieldName -> CoercibleField
|
||||||
resolveTableFieldName table fieldName =
|
resolveTableFieldName table fieldName =
|
||||||
@@ -228,11 +228,14 @@ resolveTableFieldName table fieldName =
|
|||||||
|
|
||||||
resolveTableField :: Table -> Field -> CoercibleField
|
resolveTableField :: Table -> Field -> CoercibleField
|
||||||
resolveTableField table (fieldName, []) = resolveTableFieldName table fieldName
|
resolveTableField table (fieldName, []) = resolveTableFieldName table fieldName
|
||||||
-- If the field is known and a JSON path is given, always assume the JSON type. But don't assume a type for entirely unknown fields.
|
|
||||||
resolveTableField table (fieldName, jp) =
|
resolveTableField table (fieldName, jp) =
|
||||||
case resolveTableFieldName table fieldName of
|
case resolveTableFieldName table fieldName of
|
||||||
cf@CoercibleField{cfIRType=""} -> cf{cfJsonPath=jp}
|
-- types that are already json/jsonb don't need to be converted with `to_jsonb` for using arrow operators `data->attr`
|
||||||
cf -> cf{cfJsonPath=jp, cfIRType="json"}
|
-- this prevents indexes not applying https://github.com/PostgREST/postgrest/issues/2594
|
||||||
|
cf@CoercibleField{cfIRType="json"} -> cf{cfJsonPath=jp}
|
||||||
|
cf@CoercibleField{cfIRType="jsonb"} -> cf{cfJsonPath=jp}
|
||||||
|
-- other types will get converted `to_jsonb(col)->attr`
|
||||||
|
cf -> cf{cfJsonPath=jp, cfToJson=True}
|
||||||
|
|
||||||
-- | Resolve a type within the context based on the given field name and JSON path. Although there are situations where failure to resolve a field is considered an error (see `resolveOrError`), there are also situations where we allow it (RPC calls). If it should be an error and `resolveOrError` doesn't fit, ensure to check the `cfIRType` isn't empty.
|
-- | Resolve a type within the context based on the given field name and JSON path. Although there are situations where failure to resolve a field is considered an error (see `resolveOrError`), there are also situations where we allow it (RPC calls). If it should be an error and `resolveOrError` doesn't fit, ensure to check the `cfIRType` isn't empty.
|
||||||
resolveTypeOrUnknown :: ResolverContext -> Field -> CoercibleField
|
resolveTypeOrUnknown :: ResolverContext -> Field -> CoercibleField
|
||||||
@@ -289,7 +292,7 @@ readPlan qi@QualifiedIdentifier{..} AppConfig{configDbMaxRows} SchemaCache{dbTab
|
|||||||
addRels qiSchema (iAction apiRequest) dbRelationships Nothing =<<
|
addRels qiSchema (iAction apiRequest) dbRelationships Nothing =<<
|
||||||
addLogicTrees ctx apiRequest =<<
|
addLogicTrees ctx apiRequest =<<
|
||||||
addRanges apiRequest =<<
|
addRanges apiRequest =<<
|
||||||
addOrders apiRequest =<<
|
addOrders ctx apiRequest =<<
|
||||||
addFilters ctx apiRequest (initReadRequest ctx $ QueryParams.qsSelect $ iQueryParams apiRequest)
|
addFilters ctx apiRequest (initReadRequest ctx $ QueryParams.qsSelect $ iQueryParams apiRequest)
|
||||||
|
|
||||||
-- Build the initial read plan tree
|
-- Build the initial read plan tree
|
||||||
@@ -526,8 +529,8 @@ addFilters ctx ApiRequest{..} rReq =
|
|||||||
addFilterToNode =
|
addFilterToNode =
|
||||||
updateNode (\flt (Node q@ReadPlan{from=fromTable, where_=lf} f) -> Node q{ReadPlan.where_=addFilterToLogicForest (resolveFilter ctx{qi=fromTable} flt) lf} f)
|
updateNode (\flt (Node q@ReadPlan{from=fromTable, where_=lf} f) -> Node q{ReadPlan.where_=addFilterToLogicForest (resolveFilter ctx{qi=fromTable} flt) lf} f)
|
||||||
|
|
||||||
addOrders :: ApiRequest -> ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
addOrders :: ResolverContext -> ApiRequest -> ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
||||||
addOrders ApiRequest{..} rReq =
|
addOrders ctx ApiRequest{..} rReq =
|
||||||
case iAction of
|
case iAction of
|
||||||
ActionMutate _ -> Right rReq
|
ActionMutate _ -> Right rReq
|
||||||
_ -> foldr addOrderToNode (Right rReq) qsOrder
|
_ -> foldr addOrderToNode (Right rReq) qsOrder
|
||||||
@@ -535,29 +538,32 @@ addOrders ApiRequest{..} rReq =
|
|||||||
QueryParams.QueryParams{..} = iQueryParams
|
QueryParams.QueryParams{..} = iQueryParams
|
||||||
|
|
||||||
addOrderToNode :: (EmbedPath, [OrderTerm]) -> Either ApiRequestError ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
addOrderToNode :: (EmbedPath, [OrderTerm]) -> Either ApiRequestError ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
||||||
addOrderToNode = updateNode (\o (Node q f) -> Node q{order=o} f)
|
addOrderToNode = updateNode (\o (Node q f) -> Node q{order=resolveOrder ctx <$> o} f)
|
||||||
|
|
||||||
|
resolveOrder :: ResolverContext -> OrderTerm -> CoercibleOrderTerm
|
||||||
|
resolveOrder _ (OrderRelationTerm a b c d) = CoercibleOrderRelationTerm a b c d
|
||||||
|
resolveOrder ctx (OrderTerm fld dir nulls) = CoercibleOrderTerm (resolveTypeOrUnknown ctx fld) dir nulls
|
||||||
|
|
||||||
-- Validates that the related resource on the order is an embedded resource,
|
-- Validates that the related resource on the order is an embedded resource,
|
||||||
-- e.g. if `clients` is inside the `select` in /projects?order=clients(id)&select=*,clients(*),
|
-- e.g. if `clients` is inside the `select` in /projects?order=clients(id)&select=*,clients(*),
|
||||||
-- and if it's a to-one relationship, it adds the right alias to the OrderRelationTerm so the generated query can succeed.
|
-- and if it's a to-one relationship, it adds the right alias to the OrderRelationTerm so the generated query can succeed.
|
||||||
-- TODO might be clearer if there's an additional intermediate type
|
|
||||||
addRelatedOrders :: ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
addRelatedOrders :: ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
||||||
addRelatedOrders (Node rp@ReadPlan{order,from} forest) = do
|
addRelatedOrders (Node rp@ReadPlan{order,from} forest) = do
|
||||||
newOrder <- getRelOrder `traverse` order
|
newOrder <- newRelOrder `traverse` order
|
||||||
Node rp{order=newOrder} <$> addRelatedOrders `traverse` forest
|
Node rp{order=newOrder} <$> addRelatedOrders `traverse` forest
|
||||||
where
|
where
|
||||||
getRelOrder ot@OrderTerm{} = Right ot
|
newRelOrder cot@CoercibleOrderTerm{} = Right cot
|
||||||
getRelOrder ot@OrderRelationTerm{otRelation} =
|
newRelOrder cot@CoercibleOrderRelationTerm{coRelation} =
|
||||||
let foundRP = rootLabel <$> find (\(Node ReadPlan{relName, relAlias} _) -> otRelation == fromMaybe relName relAlias) forest in
|
let foundRP = rootLabel <$> find (\(Node ReadPlan{relName, relAlias} _) -> coRelation == fromMaybe relName relAlias) forest in
|
||||||
case foundRP of
|
case foundRP of
|
||||||
Just ReadPlan{relName,relAlias,relAggAlias,relToParent} ->
|
Just ReadPlan{relName,relAlias,relAggAlias,relToParent} ->
|
||||||
let isToOne = relIsToOne <$> relToParent
|
let isToOne = relIsToOne <$> relToParent
|
||||||
name = fromMaybe relName relAlias in
|
name = fromMaybe relName relAlias in
|
||||||
if isToOne == Just True
|
if isToOne == Just True
|
||||||
then Right $ ot{otRelation=relAggAlias}
|
then Right $ cot{coRelation=relAggAlias}
|
||||||
else Left $ RelatedOrderNotToOne (qiName from) name
|
else Left $ RelatedOrderNotToOne (qiName from) name
|
||||||
Nothing ->
|
Nothing ->
|
||||||
Left $ NotEmbedded otRelation
|
Left $ NotEmbedded coRelation
|
||||||
|
|
||||||
-- | Searches for null filters on embeds, e.g. `projects=not.is.null` on `GET /clients?select=*,projects(*)&projects=not.is.null`
|
-- | Searches for null filters on embeds, e.g. `projects=not.is.null` on `GET /clients?select=*,projects(*)&projects=not.is.null`
|
||||||
--
|
--
|
||||||
@@ -598,7 +604,7 @@ addRelatedOrders (Node rp@ReadPlan{order,from} forest) = do
|
|||||||
-- where_ = [
|
-- where_ = [
|
||||||
-- CoercibleStmnt (
|
-- CoercibleStmnt (
|
||||||
-- CoercibleFilter {
|
-- CoercibleFilter {
|
||||||
-- field = CoercibleField {cfName = "projects", cfJsonPath = [], cfIRType = "", cfTransform = Nothing, cfDefault = Nothing},
|
-- field = CoercibleField {cfName = "projects", cfJsonPath = [], cfToJson=False, cfIRType = "", cfTransform = Nothing, cfDefault = Nothing},
|
||||||
-- opExpr = op
|
-- opExpr = op
|
||||||
-- }
|
-- }
|
||||||
-- )
|
-- )
|
||||||
@@ -613,7 +619,7 @@ addRelatedOrders (Node rp@ReadPlan{order,from} forest) = do
|
|||||||
-- Don't do anything to the filter if there's no embedding (a subtree) on projects. Assume it's a normal filter.
|
-- Don't do anything to the filter if there's no embedding (a subtree) on projects. Assume it's a normal filter.
|
||||||
--
|
--
|
||||||
-- >>> ReadPlan.where_ . rootLabel <$> addNullEmbedFilters (readPlanTree nullOp [])
|
-- >>> ReadPlan.where_ . rootLabel <$> addNullEmbedFilters (readPlanTree nullOp [])
|
||||||
-- Right [CoercibleStmnt (CoercibleFilter {field = CoercibleField {cfName = "projects", cfJsonPath = [], cfIRType = "", cfTransform = Nothing, cfDefault = Nothing}, opExpr = OpExpr True (Is TriNull)})]
|
-- Right [CoercibleStmnt (CoercibleFilter {field = CoercibleField {cfName = "projects", cfJsonPath = [], cfToJson = False, cfIRType = "", cfTransform = Nothing, cfDefault = Nothing}, opExpr = OpExpr True (Is TriNull)})]
|
||||||
--
|
--
|
||||||
-- If there's an embedding on projects, then change the filter to use the internal aggregate name (`clients_projects_1`) so the filter can succeed later.
|
-- If there's an embedding on projects, then change the filter to use the internal aggregate name (`clients_projects_1`) so the filter can succeed later.
|
||||||
--
|
--
|
||||||
@@ -637,7 +643,7 @@ addNullEmbedFilters (Node rp@ReadPlan{where_=curLogic} forest) = do
|
|||||||
newNullFilters rPlans = \case
|
newNullFilters rPlans = \case
|
||||||
(CoercibleExpr b lOp trees) ->
|
(CoercibleExpr b lOp trees) ->
|
||||||
CoercibleExpr b lOp <$> (newNullFilters rPlans `traverse` trees)
|
CoercibleExpr b lOp <$> (newNullFilters rPlans `traverse` trees)
|
||||||
flt@(CoercibleStmnt (CoercibleFilter (CoercibleField fld [] _ _ _) opExpr)) ->
|
flt@(CoercibleStmnt (CoercibleFilter (CoercibleField fld [] _ _ _ _) opExpr)) ->
|
||||||
let foundRP = find (\ReadPlan{relName, relAlias} -> fld == fromMaybe relName relAlias) rPlans in
|
let foundRP = find (\ReadPlan{relName, relAlias} -> fld == fromMaybe relName relAlias) rPlans in
|
||||||
case (foundRP, opExpr) of
|
case (foundRP, opExpr) of
|
||||||
(Just ReadPlan{relAggAlias}, OpExpr b (Is TriNull)) -> Right $ CoercibleStmnt $ CoercibleFilterNullEmbed b relAggAlias
|
(Just ReadPlan{relAggAlias}, OpExpr b (Is TriNull)) -> Right $ CoercibleStmnt $ CoercibleFilterNullEmbed b relAggAlias
|
||||||
@@ -726,7 +732,7 @@ 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 = maybe [] snd $ find (\(x, _) -> null x) qsOrder
|
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
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ where
|
|||||||
import qualified Data.ByteString.Lazy as LBS
|
import qualified Data.ByteString.Lazy as LBS
|
||||||
|
|
||||||
import PostgREST.ApiRequest.Preferences (PreferResolution)
|
import PostgREST.ApiRequest.Preferences (PreferResolution)
|
||||||
import PostgREST.ApiRequest.Types (OrderTerm)
|
|
||||||
import PostgREST.Plan.Types (CoercibleField,
|
import PostgREST.Plan.Types (CoercibleField,
|
||||||
CoercibleLogicTree)
|
CoercibleLogicTree,
|
||||||
|
CoercibleOrderTerm)
|
||||||
import PostgREST.RangeQuery (NonnegRange)
|
import PostgREST.RangeQuery (NonnegRange)
|
||||||
import PostgREST.SchemaCache.Identifiers (FieldName,
|
import PostgREST.SchemaCache.Identifiers (FieldName,
|
||||||
QualifiedIdentifier)
|
QualifiedIdentifier)
|
||||||
@@ -33,7 +33,7 @@ data MutatePlan
|
|||||||
, updBody :: Maybe LBS.ByteString
|
, updBody :: Maybe LBS.ByteString
|
||||||
, where_ :: [CoercibleLogicTree]
|
, where_ :: [CoercibleLogicTree]
|
||||||
, mutRange :: NonnegRange
|
, mutRange :: NonnegRange
|
||||||
, mutOrder :: [OrderTerm]
|
, mutOrder :: [CoercibleOrderTerm]
|
||||||
, returning :: [FieldName]
|
, returning :: [FieldName]
|
||||||
, applyDefs :: Bool
|
, applyDefs :: Bool
|
||||||
}
|
}
|
||||||
@@ -41,6 +41,6 @@ data MutatePlan
|
|||||||
{ in_ :: QualifiedIdentifier
|
{ in_ :: QualifiedIdentifier
|
||||||
, where_ :: [CoercibleLogicTree]
|
, where_ :: [CoercibleLogicTree]
|
||||||
, mutRange :: NonnegRange
|
, mutRange :: NonnegRange
|
||||||
, mutOrder :: [OrderTerm]
|
, mutOrder :: [CoercibleOrderTerm]
|
||||||
, returning :: [FieldName]
|
, returning :: [FieldName]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ module PostgREST.Plan.ReadPlan
|
|||||||
import Data.Tree (Tree (..))
|
import Data.Tree (Tree (..))
|
||||||
|
|
||||||
import PostgREST.ApiRequest.Types (Alias, Cast, Depth, Hint,
|
import PostgREST.ApiRequest.Types (Alias, Cast, Depth, Hint,
|
||||||
JoinType, NodeName,
|
JoinType, NodeName)
|
||||||
OrderTerm)
|
|
||||||
import PostgREST.Plan.Types (CoercibleField (..),
|
import PostgREST.Plan.Types (CoercibleField (..),
|
||||||
CoercibleLogicTree)
|
CoercibleLogicTree,
|
||||||
|
CoercibleOrderTerm)
|
||||||
import PostgREST.RangeQuery (NonnegRange)
|
import PostgREST.RangeQuery (NonnegRange)
|
||||||
import PostgREST.SchemaCache.Identifiers (FieldName,
|
import PostgREST.SchemaCache.Identifiers (FieldName,
|
||||||
QualifiedIdentifier)
|
QualifiedIdentifier)
|
||||||
@@ -32,7 +32,7 @@ data ReadPlan = ReadPlan
|
|||||||
, from :: QualifiedIdentifier
|
, from :: QualifiedIdentifier
|
||||||
, fromAlias :: Maybe Alias
|
, fromAlias :: Maybe Alias
|
||||||
, where_ :: [CoercibleLogicTree]
|
, where_ :: [CoercibleLogicTree]
|
||||||
, order :: [OrderTerm]
|
, order :: [CoercibleOrderTerm]
|
||||||
, range_ :: NonnegRange
|
, range_ :: NonnegRange
|
||||||
, relName :: NodeName
|
, relName :: NodeName
|
||||||
, relToParent :: Maybe Relationship
|
, relToParent :: Maybe Relationship
|
||||||
|
|||||||
@@ -4,9 +4,11 @@ module PostgREST.Plan.Types
|
|||||||
, CoercibleLogicTree(..)
|
, CoercibleLogicTree(..)
|
||||||
, CoercibleFilter(..)
|
, CoercibleFilter(..)
|
||||||
, TransformerProc
|
, TransformerProc
|
||||||
|
, CoercibleOrderTerm(..)
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import PostgREST.ApiRequest.Types (JsonPath, LogicOperator, OpExpr)
|
import PostgREST.ApiRequest.Types (Field, JsonPath, LogicOperator,
|
||||||
|
OpExpr, OrderDirection, OrderNulls)
|
||||||
|
|
||||||
import PostgREST.SchemaCache.Identifiers (FieldName)
|
import PostgREST.SchemaCache.Identifiers (FieldName)
|
||||||
|
|
||||||
@@ -28,13 +30,14 @@ type TransformerProc = Text
|
|||||||
data CoercibleField = CoercibleField
|
data CoercibleField = CoercibleField
|
||||||
{ cfName :: FieldName
|
{ cfName :: FieldName
|
||||||
, cfJsonPath :: JsonPath
|
, cfJsonPath :: JsonPath
|
||||||
|
, cfToJson :: Bool
|
||||||
, cfIRType :: Text -- ^ The native Postgres type of the field, the intermediate (IR) type before mapping.
|
, cfIRType :: Text -- ^ The native Postgres type of the field, the intermediate (IR) type before mapping.
|
||||||
, cfTransform :: Maybe TransformerProc -- ^ The optional mapping from irType -> targetType.
|
, cfTransform :: Maybe TransformerProc -- ^ The optional mapping from irType -> targetType.
|
||||||
, cfDefault :: Maybe Text
|
, cfDefault :: Maybe Text
|
||||||
} deriving (Eq, Show)
|
} deriving (Eq, Show)
|
||||||
|
|
||||||
unknownField :: FieldName -> JsonPath -> CoercibleField
|
unknownField :: FieldName -> JsonPath -> CoercibleField
|
||||||
unknownField name path = CoercibleField name path "" Nothing Nothing
|
unknownField name path = CoercibleField name path False "" Nothing Nothing
|
||||||
|
|
||||||
-- | Like an API request LogicTree, but with coercible field information.
|
-- | Like an API request LogicTree, but with coercible field information.
|
||||||
data CoercibleLogicTree
|
data CoercibleLogicTree
|
||||||
@@ -48,3 +51,17 @@ data CoercibleFilter = CoercibleFilter
|
|||||||
}
|
}
|
||||||
| CoercibleFilterNullEmbed Bool FieldName
|
| CoercibleFilterNullEmbed Bool FieldName
|
||||||
deriving (Eq, Show)
|
deriving (Eq, Show)
|
||||||
|
|
||||||
|
data CoercibleOrderTerm
|
||||||
|
= CoercibleOrderTerm
|
||||||
|
{ coField :: CoercibleField
|
||||||
|
, coDirection :: Maybe OrderDirection
|
||||||
|
, coNullOrder :: Maybe OrderNulls
|
||||||
|
}
|
||||||
|
| CoercibleOrderRelationTerm
|
||||||
|
{ coRelation :: FieldName
|
||||||
|
, coRelTerm :: Field
|
||||||
|
, coDirection :: Maybe OrderDirection
|
||||||
|
, coNullOrder :: Maybe OrderNulls
|
||||||
|
}
|
||||||
|
deriving (Eq, Show)
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ mutatePlanToQuery (Update mainQi uCols body logicForest range ordts returnings a
|
|||||||
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)
|
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)
|
rangeCols = intercalateSnippet ", " ((\col -> pgFmtIdent (cfName col) <> " = (SELECT " <> pgFmtIdent (cfName col) <> " FROM pgrst_update_body) ") <$> uCols)
|
||||||
(whereRangeIdF, rangeIdF) = mutRangeF mainQi (fst . otTerm <$> ordts)
|
(whereRangeIdF, rangeIdF) = mutRangeF mainQi (cfName . coField <$> ordts)
|
||||||
|
|
||||||
mutatePlanToQuery (Delete mainQi logicForest range ordts returnings)
|
mutatePlanToQuery (Delete mainQi logicForest range ordts returnings)
|
||||||
| range == allRange =
|
| range == allRange =
|
||||||
@@ -161,7 +161,7 @@ mutatePlanToQuery (Delete mainQi logicForest range ordts 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 (fst . otTerm <$> ordts)
|
(whereRangeIdF, rangeIdF) = mutRangeF mainQi (cfName . coField <$> ordts)
|
||||||
|
|
||||||
callPlanToQuery :: CallPlan -> PgVersion -> SQL.Snippet
|
callPlanToQuery :: CallPlan -> PgVersion -> SQL.Snippet
|
||||||
callPlanToQuery (FunctionCall qi params args returnsScalar returnsSetOfScalar returnsCompositeAlias returnings) pgVer =
|
callPlanToQuery (FunctionCall qi params args returnsScalar returnsSetOfScalar returnsCompositeAlias returnings) pgVer =
|
||||||
@@ -171,7 +171,7 @@ callPlanToQuery (FunctionCall qi params args returnsScalar returnsSetOfScalar re
|
|||||||
fromCall = case params of
|
fromCall = case params of
|
||||||
OnePosParam prm -> "FROM " <> callIt (singleParameter args $ encodeUtf8 $ ppType prm)
|
OnePosParam prm -> "FROM " <> callIt (singleParameter args $ encodeUtf8 $ ppType prm)
|
||||||
KeyParams [] -> "FROM " <> callIt mempty
|
KeyParams [] -> "FROM " <> callIt mempty
|
||||||
KeyParams prms -> fromJsonBodyF args ((\p -> CoercibleField (ppName p) mempty (ppType p) Nothing Nothing) <$> prms) False True False <> ", " <>
|
KeyParams prms -> fromJsonBodyF args ((\p -> CoercibleField (ppName p) mempty False (ppType p) Nothing Nothing) <$> prms) False True False <> ", " <>
|
||||||
"LATERAL " <> callIt (fmtParams prms)
|
"LATERAL " <> callIt (fmtParams prms)
|
||||||
|
|
||||||
callIt :: SQL.Snippet -> SQL.Snippet
|
callIt :: SQL.Snippet -> SQL.Snippet
|
||||||
|
|||||||
@@ -61,7 +61,6 @@ import PostgREST.ApiRequest.Types (Alias, Cast,
|
|||||||
Operation (..),
|
Operation (..),
|
||||||
OrderDirection (..),
|
OrderDirection (..),
|
||||||
OrderNulls (..),
|
OrderNulls (..),
|
||||||
OrderTerm (..),
|
|
||||||
QuantOperator (..),
|
QuantOperator (..),
|
||||||
SimpleOperator (..),
|
SimpleOperator (..),
|
||||||
TrileanVal (..))
|
TrileanVal (..))
|
||||||
@@ -71,6 +70,7 @@ import PostgREST.Plan.ReadPlan (JoinCondition (..))
|
|||||||
import PostgREST.Plan.Types (CoercibleField (..),
|
import PostgREST.Plan.Types (CoercibleField (..),
|
||||||
CoercibleFilter (..),
|
CoercibleFilter (..),
|
||||||
CoercibleLogicTree (..),
|
CoercibleLogicTree (..),
|
||||||
|
CoercibleOrderTerm (..),
|
||||||
unknownField)
|
unknownField)
|
||||||
import PostgREST.RangeQuery (NonnegRange, allRange,
|
import PostgREST.RangeQuery (NonnegRange, allRange,
|
||||||
rangeLimit, rangeOffset)
|
rangeLimit, rangeOffset)
|
||||||
@@ -241,10 +241,9 @@ pgFmtCallUnary :: Text -> SQL.Snippet -> SQL.Snippet
|
|||||||
pgFmtCallUnary f x = SQL.sql (encodeUtf8 f) <> "(" <> x <> ")"
|
pgFmtCallUnary f x = SQL.sql (encodeUtf8 f) <> "(" <> x <> ")"
|
||||||
|
|
||||||
pgFmtField :: QualifiedIdentifier -> CoercibleField -> SQL.Snippet
|
pgFmtField :: QualifiedIdentifier -> CoercibleField -> SQL.Snippet
|
||||||
pgFmtField table CoercibleField{cfName=fn, cfJsonPath=[]} = pgFmtColumn table fn
|
pgFmtField table CoercibleField{cfName=fn, cfJsonPath=[]} = pgFmtColumn table fn
|
||||||
-- Using to_jsonb instead of to_json to avoid missing operator errors when filtering:
|
pgFmtField table CoercibleField{cfName=fn, cfToJson=doToJson, cfJsonPath=jp} | doToJson = "to_jsonb(" <> pgFmtColumn table fn <> ")" <> pgFmtJsonPath jp
|
||||||
-- "operator does not exist: json = unknown"
|
| otherwise = pgFmtColumn table fn <> pgFmtJsonPath jp
|
||||||
pgFmtField table CoercibleField{cfName=fn, cfJsonPath=jp} = "to_jsonb(" <> pgFmtColumn table fn <> ")" <> pgFmtJsonPath jp
|
|
||||||
|
|
||||||
-- Select the value of a named element from a table, applying its optional coercion mapping if any.
|
-- Select the value of a named element from a table, applying its optional coercion mapping if any.
|
||||||
pgFmtTableCoerce :: QualifiedIdentifier -> CoercibleField -> SQL.Snippet
|
pgFmtTableCoerce :: QualifiedIdentifier -> CoercibleField -> SQL.Snippet
|
||||||
@@ -299,16 +298,16 @@ fromJsonBodyF body fields includeSelect includeLimitOne includeDefaults =
|
|||||||
else ("pgrst_uniform_json.val", "json_typeof", "json_build_array", "json_array_elements", "json_to_recordset")
|
else ("pgrst_uniform_json.val", "json_typeof", "json_build_array", "json_array_elements", "json_to_recordset")
|
||||||
jsonPlaceHolder = SQL.encoderAndParam (HE.nullable $ if includeDefaults then HE.jsonbLazyBytes else HE.jsonLazyBytes) body
|
jsonPlaceHolder = SQL.encoderAndParam (HE.nullable $ if includeDefaults then HE.jsonbLazyBytes else HE.jsonLazyBytes) body
|
||||||
|
|
||||||
pgFmtOrderTerm :: QualifiedIdentifier -> OrderTerm -> SQL.Snippet
|
pgFmtOrderTerm :: QualifiedIdentifier -> CoercibleOrderTerm -> SQL.Snippet
|
||||||
pgFmtOrderTerm qi ot =
|
pgFmtOrderTerm qi ot =
|
||||||
fmtOTerm ot <> " " <>
|
fmtOTerm ot <> " " <>
|
||||||
SQL.sql (BS.unwords [
|
SQL.sql (BS.unwords [
|
||||||
maybe mempty direction $ otDirection ot,
|
maybe mempty direction $ coDirection ot,
|
||||||
maybe mempty nullOrder $ otNullOrder ot])
|
maybe mempty nullOrder $ coNullOrder ot])
|
||||||
where
|
where
|
||||||
fmtOTerm = \case
|
fmtOTerm = \case
|
||||||
OrderTerm{otTerm=(fn, jp)} -> pgFmtField qi (unknownField fn jp)
|
CoercibleOrderTerm{coField=cof} -> pgFmtField qi cof
|
||||||
OrderRelationTerm{otRelation, otRelTerm=(fn, jp)} -> pgFmtField (QualifiedIdentifier mempty otRelation) (unknownField fn jp)
|
CoercibleOrderRelationTerm{coRelation, coRelTerm=(fn, jp)} -> pgFmtField (QualifiedIdentifier mempty coRelation) (unknownField fn jp)
|
||||||
|
|
||||||
direction OrderAsc = "ASC"
|
direction OrderAsc = "ASC"
|
||||||
direction OrderDesc = "DESC"
|
direction OrderDesc = "DESC"
|
||||||
@@ -446,7 +445,7 @@ mutRangeF mainQi rangeId =
|
|||||||
, intercalateSnippet ", " (pgFmtColumn mainQi <$> rangeId)
|
, intercalateSnippet ", " (pgFmtColumn mainQi <$> rangeId)
|
||||||
)
|
)
|
||||||
|
|
||||||
orderF :: QualifiedIdentifier -> [OrderTerm] -> 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)
|
||||||
|
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ spec actualPgVersion = describe "json and jsonb operators" $ do
|
|||||||
|
|
||||||
it "can get array of objects" $ do
|
it "can get array of objects" $ do
|
||||||
get "/json_arr?select=data->0->>a&id=in.(5,6)" `shouldRespondWith`
|
get "/json_arr?select=data->0->>a&id=in.(5,6)" `shouldRespondWith`
|
||||||
[json| [{"a":"A"}, {"a":"[1, 2, 3]"}] |]
|
[json|[{"a":"A"}, {"a":"[1,2,3]"}]|]
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
get "/json_arr?select=data->0->a->>2&id=in.(5,6)" `shouldRespondWith`
|
get "/json_arr?select=data->0->a->>2&id=in.(5,6)" `shouldRespondWith`
|
||||||
[json| [{"a":null}, {"a":"3"}] |]
|
[json| [{"a":null}, {"a":"3"}] |]
|
||||||
@@ -275,7 +275,7 @@ spec actualPgVersion = describe "json and jsonb operators" $ do
|
|||||||
[json| [{"data":8}, {"data":7}] |]
|
[json| [{"data":8}, {"data":7}] |]
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
get "/json_arr?select=data->-2->>a&id=in.(5,6)" `shouldRespondWith`
|
get "/json_arr?select=data->-2->>a&id=in.(5,6)" `shouldRespondWith`
|
||||||
[json| [{"a":"A"}, {"a":"[1, 2, 3]"}] |]
|
[json| [{"a":"A"}, {"a":"[1,2,3]"}] |]
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
it "can filter with negative indexes" $ do
|
it "can filter with negative indexes" $ do
|
||||||
|
|||||||
@@ -348,6 +348,43 @@ spec actualPgVersion = do
|
|||||||
liftIO $ do
|
liftIO $ do
|
||||||
resBody `shouldSatisfy` (\t -> not $ T.isInfixOf "getitemrange" (decodeUtf8 $ LBS.toStrict t))
|
resBody `shouldSatisfy` (\t -> not $ T.isInfixOf "getitemrange" (decodeUtf8 $ LBS.toStrict t))
|
||||||
|
|
||||||
|
context "index usage" $ do
|
||||||
|
it "should use an index for a json arrow operator filter" $ do
|
||||||
|
r <- request methodGet "/bets?data_json->>contractId=eq.1"
|
||||||
|
[(hAccept, "application/vnd.pgrst.plan")] ""
|
||||||
|
|
||||||
|
let resBody = simpleBody r
|
||||||
|
|
||||||
|
liftIO $ do
|
||||||
|
resBody `shouldSatisfy` (\t -> T.isInfixOf "Index Cond" (decodeUtf8 $ LBS.toStrict t))
|
||||||
|
|
||||||
|
it "should use an index for a jsonb arrow operator filter" $ do
|
||||||
|
r <- request methodGet "/bets?data_jsonb->>contractId=eq.1"
|
||||||
|
[(hAccept, "application/vnd.pgrst.plan")] ""
|
||||||
|
|
||||||
|
let resBody = simpleBody r
|
||||||
|
|
||||||
|
liftIO $ do
|
||||||
|
resBody `shouldSatisfy` (\t -> T.isInfixOf "Index" (decodeUtf8 $ LBS.toStrict t))
|
||||||
|
|
||||||
|
it "should use an index for ordering on a json arrow operator" $ do
|
||||||
|
r <- request methodGet "/bets?order=data_json->>contractId"
|
||||||
|
[(hAccept, "application/vnd.pgrst.plan")] ""
|
||||||
|
|
||||||
|
let resBody = simpleBody r
|
||||||
|
|
||||||
|
liftIO $ do
|
||||||
|
resBody `shouldSatisfy` (\t -> T.isInfixOf "Index" (decodeUtf8 $ LBS.toStrict t))
|
||||||
|
|
||||||
|
it "should use an index for ordering on a jsonb arrow operator" $ do
|
||||||
|
r <- request methodGet "/bets?order=data_jsonb->>contractId"
|
||||||
|
[(hAccept, "application/vnd.pgrst.plan")] ""
|
||||||
|
|
||||||
|
let resBody = simpleBody r
|
||||||
|
|
||||||
|
liftIO $ do
|
||||||
|
resBody `shouldSatisfy` (\t -> T.isInfixOf "Index" (decodeUtf8 $ LBS.toStrict t))
|
||||||
|
|
||||||
disabledSpec :: SpecWith ((), Application)
|
disabledSpec :: SpecWith ((), Application)
|
||||||
disabledSpec =
|
disabledSpec =
|
||||||
it "doesn't work if db-plan-enabled=false(the default)" $ do
|
it "doesn't work if db-plan-enabled=false(the default)" $ do
|
||||||
|
|||||||
Vendored
+9
@@ -3293,3 +3293,12 @@ create table evil_friends(
|
|||||||
id devil_int
|
id devil_int
|
||||||
, name text
|
, name text
|
||||||
);
|
);
|
||||||
|
|
||||||
|
create table bets (
|
||||||
|
id int
|
||||||
|
, data_json json
|
||||||
|
, data_jsonb jsonb
|
||||||
|
);
|
||||||
|
|
||||||
|
create index bets_data_json on bets ((data_json ->>'contractId'));
|
||||||
|
create index bets_data_jsonb on bets ((data_jsonb ->>'contractId'));
|
||||||
|
|||||||
Reference in New Issue
Block a user