refactor: rm schema arg from QueryBuilder funcs

* Change TableName to QualifiedIdentifier in ReadQuery
  and MutateQuery.

* Move removeSourceCTESchema to DbRequestBuilder.
This commit is contained in:
steve-chavez
2019-10-08 12:41:39 -05:00
committed by Steve Chávez
parent eebe319bfd
commit 50f2cc16ab
5 changed files with 72 additions and 71 deletions
+4 -5
View File
@@ -333,24 +333,23 @@ app dbStructure proc cols conf apiRequest =
topLevelRange = iTopLevelRange apiRequest
returnsScalar = maybe False procReturnsScalar proc
selectQuery = readRequestToQuery schema False
countQuery = readRequestToCountQuery schema
selectQuery = readRequestToQuery False
readSqlParts tableName =
let
readReq = readRequest schema tableName maxRows (dbRelations dbStructure) apiRequest
in
(,,) <$>
(selectQuery <$> readReq) <*>
(countQuery <$> readReq) <*>
(readRequestToCountQuery <$> readReq) <*>
(binaryField contentType rawContentTypes returnsScalar =<< readReq)
mutateSqlParts s t =
let
readReq = readRequest s t maxRows (dbRelations dbStructure) apiRequest
mutReq = mutateRequest apiRequest t cols (tablePKCols dbStructure s t) =<< readReq
mutReq = mutateRequest s t apiRequest cols (tablePKCols dbStructure s t) =<< readReq
in
(,) <$>
(selectQuery <$> readReq) <*>
(mutateRequestToQuery s <$> mutReq)
(mutateRequestToQuery <$> mutReq)
responseContentTypeOrError :: [ContentType] -> [ContentType] -> Action -> Target -> Either Response ContentType
responseContentTypeOrError accepts rawContentTypes action target = serves contentTypesForRequest accepts
+34 -21
View File
@@ -50,15 +50,15 @@ readRequest schema rootTableName maxRows allRels apiRequest =
(initReadRequest rootName <$> pRequestSelect sel)
where
sel = fromMaybe "*" $ iSelect apiRequest -- default to all columns requested (SELECT *) for a non existent ?select querystring param
(rootName, rootRels) = rootWithRelations rootTableName allRels (iAction apiRequest)
(rootName, rootRels) = rootWithRelations schema rootTableName allRels (iAction apiRequest)
-- Get the root table name with its relations according to the Action type.
-- This is done because of the shape of the final SQL Query. The mutation cases are wrapped in a WITH {sourceCTEName}(see Statements.hs).
-- So we need a FROM {sourceCTEName} instead of FROM {tableName}.
rootWithRelations :: TableName -> [Relation] -> Action -> (TableName, [Relation])
rootWithRelations rootTableName allRels action = case action of
ActionRead _ -> (rootTableName, allRels) -- normal read case
_ -> (sourceCTEName, mapMaybe toSourceRelation allRels ++ allRels) -- mutation cases and calling proc
rootWithRelations :: Schema -> TableName -> [Relation] -> Action -> (QualifiedIdentifier, [Relation])
rootWithRelations schema rootTableName allRels action = case action of
ActionRead _ -> (QualifiedIdentifier schema rootTableName, allRels) -- normal read case
_ -> (QualifiedIdentifier mempty sourceCTEName, mapMaybe toSourceRelation allRels ++ allRels) -- mutation cases and calling proc
where
-- To enable embedding in the sourceCTEName cases we need to replace the foreign key tableName in the Relation
-- with {sourceCTEName}. This way findRelation can find Relations with sourceCTEName.
@@ -69,19 +69,24 @@ rootWithRelations rootTableName allRels action = case action of
-- Build the initial tree with a Depth attribute so when a self join occurs we can differentiate the parent and child tables by having
-- an alias like "table_depth", this is related to http://github.com/PostgREST/postgrest/issues/987.
initReadRequest :: TableName -> [Tree SelectItem] -> ReadRequest
initReadRequest rootTableName =
initReadRequest :: QualifiedIdentifier -> [Tree SelectItem] -> ReadRequest
initReadRequest rootQi =
foldr (treeEntry rootDepth) initial
where
rootDepth = 0
initial = Node (Select [] rootTableName Nothing [] [] [] [] allRange, (rootTableName, Nothing, Nothing, Nothing, rootDepth)) []
rootSchema = qiSchema rootQi
rootName = qiName rootQi
initial = Node (Select [] rootQi Nothing [] [] [] [] allRange, (rootName, Nothing, Nothing, Nothing, rootDepth)) []
treeEntry :: Depth -> Tree SelectItem -> ReadRequest -> ReadRequest
treeEntry depth (Node fld@((fn, _),_,alias,relationDetail) fldForest) (Node (q, i) rForest) =
let nxtDepth = succ depth in
case fldForest of
[] -> Node (q {select=fld:select q}, i) rForest
_ -> Node (q, i) $
foldr (treeEntry nxtDepth) (Node (Select [] fn Nothing [] [] [] [] allRange, (fn, Nothing, alias, relationDetail, nxtDepth)) []) fldForest:rForest
foldr (treeEntry nxtDepth)
(Node (Select [] (QualifiedIdentifier rootSchema fn) Nothing [] [] [] [] allRange,
(fn, Nothing, alias, relationDetail, nxtDepth)) [])
fldForest:rForest
treeRestrictRange :: Maybe Integer -> ReadRequest -> Either ApiRequestError ReadRequest
treeRestrictRange maxRows request = pure $ nodeRestrictRange maxRows <$> request
@@ -97,9 +102,10 @@ augumentRequestWithJoin schema allRels request =
addRelations :: Schema -> [Relation] -> Maybe ReadRequest -> ReadRequest -> Either ApiRequestError ReadRequest
addRelations schema allRelations parentNode (Node (query@Select{from=tbl}, (nodeName, _, alias, relationDetail, depth)) forest) =
case parentNode of
Just (Node (Select{from=parentNodeTable}, _) _) ->
let newFrom r = if tbl == nodeName then tableName (relTable r) else tbl
Just (Node (Select{from=parentNodeQi}, _) _) ->
let newFrom r = if qiName tbl == nodeName then tableQi (relTable r) else tbl
newReadNode = (\r -> (query{from=newFrom r}, (nodeName, Just r, alias, Nothing, depth))) <$> rel
parentNodeTable = qiName parentNodeQi
rel :: Either ApiRequestError Relation
rel = note (NoRelationBetween parentNodeTable nodeName) $
findRelation schema allRelations nodeName parentNodeTable relationDetail in
@@ -201,12 +207,12 @@ addJoinConditions schema previousAlias (Node node@(query@Select{from=tbl}, nodeP
Just rel@Relation{relType=Child} -> Node (augmentQuery rel, nodeProps) <$> updatedForest
Just rel@Relation{relType=Many, relLinkTable=(Just linkTable)} ->
let rq = augmentQuery rel in
Node (rq{implicitJoins=tableName linkTable:implicitJoins rq}, nodeProps) <$> updatedForest
Node (rq{implicitJoins=tableQi linkTable:implicitJoins rq}, nodeProps) <$> updatedForest
_ -> Left UnknownRelation
where
newAlias = case isSelfJoin <$> relation of
Just True
| depth /= 0 -> Just (tbl <> "_" <> show depth) -- root node doesn't get aliased
| depth /= 0 -> Just (qiName tbl <> "_" <> show depth) -- root node doesn't get aliased
| otherwise -> Nothing
_ -> Nothing
augmentQuery rel =
@@ -231,11 +237,17 @@ getJoinConditions previousAlias newAlias (Relation Table{tableSchema=tSchema, ta
where
toJoinCondition :: Text -> Text -> Column -> Column -> JoinCondition
toJoinCondition tb ftb c fc =
let qi1 = QualifiedIdentifier tSchema tb
qi2 = QualifiedIdentifier tSchema ftb in
let qi1 = removeSourceCTESchema tSchema tb
qi2 = removeSourceCTESchema tSchema ftb in
JoinCondition (maybe qi1 (QualifiedIdentifier mempty) newAlias, colName c)
(maybe qi2 (QualifiedIdentifier mempty) previousAlias, colName fc)
-- On mutation and calling proc cases we wrap the target table in a WITH {sourceCTEName}
-- if this happens remove the schema `FROM "schema"."{sourceCTEName}"` and use only the
-- `FROM "{sourceCTEName}"`. If the schema remains the FROM would be invalid.
removeSourceCTESchema :: Schema -> TableName -> QualifiedIdentifier
removeSourceCTESchema schema tbl = QualifiedIdentifier (if tbl == sourceCTEName then mempty else schema) tbl
addFiltersOrdersRanges :: ApiRequest -> Either ApiRequestError (ReadRequest -> ReadRequest)
addFiltersOrdersRanges apiRequest = foldr1 (liftA2 (.)) [
flip (foldr addFilter) <$> filters,
@@ -297,11 +309,11 @@ addProperty f (targetNodeName:remainingPath, a) (Node rn forest) =
where
pathNode = find (\(Node (_,(nodeName,_,alias,_,_)) _) -> nodeName == targetNodeName || alias == Just targetNodeName) forest
mutateRequest :: ApiRequest -> TableName -> S.Set FieldName -> [FieldName] -> ReadRequest -> Either Response MutateRequest
mutateRequest apiRequest tName cols pkCols readReq = mapLeft errorResponseFor $
mutateRequest :: Schema -> TableName -> ApiRequest -> S.Set FieldName -> [FieldName] -> ReadRequest -> Either Response MutateRequest
mutateRequest schema tName apiRequest cols pkCols readReq = mapLeft errorResponseFor $
case action of
ActionCreate -> Right $ Insert tName cols ((,) <$> iPreferResolution apiRequest <*> Just pkCols) [] returnings
ActionUpdate -> Update tName cols <$> combinedLogic <*> pure returnings
ActionCreate -> Right $ Insert qi cols ((,) <$> iPreferResolution apiRequest <*> Just pkCols) [] returnings
ActionUpdate -> Update qi cols <$> combinedLogic <*> pure returnings
ActionSingleUpsert ->
(\flts ->
if null (iLogic apiRequest) &&
@@ -310,12 +322,13 @@ mutateRequest apiRequest tName cols pkCols readReq = mapLeft errorResponseFor $
all (\case
Filter _ (OpExpr False (Op "eq" _)) -> True
_ -> False) flts
then Insert tName cols (Just (MergeDuplicates, pkCols)) <$> combinedLogic <*> pure returnings
then Insert qi cols (Just (MergeDuplicates, pkCols)) <$> combinedLogic <*> pure returnings
else
Left InvalidFilters) =<< filters
ActionDelete -> Delete tName <$> combinedLogic <*> pure returnings
ActionDelete -> Delete qi <$> combinedLogic <*> pure returnings
_ -> Left UnsupportedVerb
where
qi = QualifiedIdentifier schema tName
action = iAction apiRequest
returnings =
if iPreferRepresentation apiRequest == None
+24 -31
View File
@@ -7,7 +7,7 @@ Module : PostgREST.QueryBuilder
Description : PostgREST SQL queries generating functions.
This module provides functions to consume data types that
represent database objects (e.g. Relation, Schema) and SqlFragment
represent database queries (e.g. ReadRequest, MutateRequest) and SqlFragment
to produce SqlQuery type outputs.
-}
module PostgREST.QueryBuilder (
@@ -34,8 +34,8 @@ import PostgREST.Types
import Protolude hiding (cast, intercalate,
replace)
readRequestToQuery :: Schema -> Bool -> ReadRequest -> SqlQuery
readRequestToQuery schema isParent (Node (Select colSelects tbl tblAlias implJoins logicForest joinConditions_ ordts range, _) forest) =
readRequestToQuery :: Bool -> ReadRequest -> SqlQuery
readRequestToQuery isParent (Node (Select colSelects mainQi tblAlias implJoins logicForest joinConditions_ ordts range, _) forest) =
unwords [
"SELECT " <> intercalate ", " (map (pgFmtSelectItem qi) colSelects ++ selects),
"FROM " <> intercalate ", " (tabl : implJs),
@@ -46,8 +46,7 @@ readRequestToQuery schema isParent (Node (Select colSelects tbl tblAlias implJoi
("LIMIT " <> maybe "ALL" show (rangeLimit range) <> " OFFSET " <> show (rangeOffset range)) `emptyOnFalse` (isParent || range == allRange) ]
where
implJs = fromQi . QualifiedIdentifier schema <$> implJoins
mainQi = removeSourceCTESchema schema tbl
implJs = fromQi <$> implJoins
tabl = fromQi mainQi <> maybe mempty (\a -> " AS " <> pgFmtIdent a) tblAlias
qi = maybe mainQi (QualifiedIdentifier mempty) tblAlias
@@ -60,37 +59,37 @@ readRequestToQuery schema isParent (Node (Select colSelects tbl tblAlias implJoi
<> "SELECT json_agg(" <> pgFmtIdent table <> ".*) "
<> "FROM (" <> subquery <> ") " <> pgFmtIdent table
<> "), '[]') AS " <> pgFmtIdent (fromMaybe name alias)
where subquery = readRequestToQuery schema False (Node n forst)
where subquery = readRequestToQuery False (Node n forst)
getQueryParts (Node n@(_, (name, Just Relation{relType=Parent,relTable=Table{tableName=table}}, alias, _, _)) forst) (j,s) = (joi:j,sel:s)
where
aliasOrName = fromMaybe name alias
localTableName = pgFmtIdent $ table <> "_" <> aliasOrName
sel = "row_to_json(" <> localTableName <> ".*) AS " <> pgFmtIdent aliasOrName
joi = " LEFT JOIN LATERAL( " <> subquery <> " ) AS " <> localTableName <> " ON TRUE "
where subquery = readRequestToQuery schema True (Node n forst)
where subquery = readRequestToQuery True (Node n forst)
getQueryParts (Node n@(_, (name, Just Relation{relType=Many,relTable=Table{tableName=table}}, alias, _, _)) forst) (j,s) = (j,sel:s)
where
sel = "COALESCE (("
<> "SELECT json_agg(" <> pgFmtIdent table <> ".*) "
<> "FROM (" <> subquery <> ") " <> pgFmtIdent table
<> "), '[]') AS " <> pgFmtIdent (fromMaybe name alias)
where subquery = readRequestToQuery schema False (Node n forst)
where subquery = readRequestToQuery False (Node n forst)
--the following is just to remove the warning
--getQueryParts is not total but readRequestToQuery is called only after addJoinConditions which ensures the only
--posible relations are Child Parent Many
getQueryParts _ _ = witness
mutateRequestToQuery :: Schema -> MutateRequest -> SqlQuery
mutateRequestToQuery schema (Insert mainTbl iCols onConflct putConditions returnings) =
mutateRequestToQuery :: MutateRequest -> SqlQuery
mutateRequestToQuery (Insert mainQi iCols onConflct putConditions returnings) =
unwords [
"WITH " <> normalizedBody,
"INSERT INTO ", fromQi qi, if S.null iCols then " " else "(" <> cols <> ")",
"INSERT INTO ", fromQi mainQi, if S.null iCols then " " else "(" <> cols <> ")",
unwords [
"SELECT " <> cols <> " FROM",
"json_populate_recordset", "(null::", fromQi qi, ", " <> selectBody <> ") _",
"json_populate_recordset", "(null::", fromQi mainQi, ", " <> selectBody <> ") _",
-- Only used for PUT
("WHERE " <> intercalate " AND " (pgFmtLogicTree (QualifiedIdentifier "" "_") <$> putConditions)) `emptyOnFalse` null putConditions],
("WHERE " <> intercalate " AND " (pgFmtLogicTree (QualifiedIdentifier mempty "_") <$> putConditions)) `emptyOnFalse` null putConditions],
maybe "" (\(oncDo, oncCols) -> (
"ON CONFLICT(" <> intercalate ", " (pgFmtIdent <$> oncCols) <> ") " <> case oncDo of
IgnoreDuplicates ->
@@ -100,33 +99,29 @@ mutateRequestToQuery schema (Insert mainTbl iCols onConflct putConditions return
then "DO NOTHING"
else "DO UPDATE SET " <> intercalate ", " (pgFmtIdent <> const " = EXCLUDED." <> pgFmtIdent <$> S.toList iCols)
) `emptyOnFalse` null oncCols) onConflct,
("RETURNING " <> intercalate ", " (map (pgFmtColumn qi) returnings)) `emptyOnFalse` null returnings]
("RETURNING " <> intercalate ", " (map (pgFmtColumn mainQi) returnings)) `emptyOnFalse` null returnings]
where
qi = QualifiedIdentifier schema mainTbl
cols = intercalate ", " $ pgFmtIdent <$> S.toList iCols
mutateRequestToQuery schema (Update mainTbl uCols logicForest returnings) =
mutateRequestToQuery (Update mainQi uCols logicForest returnings) =
if S.null uCols
then "WITH " <> ignoredBody <> "SELECT null WHERE false" -- if there are no columns we cannot do UPDATE table SET {empty}, it'd be invalid syntax
else
unwords [
"WITH " <> normalizedBody,
"UPDATE " <> fromQi qi <> " SET " <> cols,
"FROM (SELECT * FROM json_populate_recordset", "(null::", fromQi qi, ", " <> selectBody <> ")) _ ",
("WHERE " <> intercalate " AND " (pgFmtLogicTree qi <$> logicForest)) `emptyOnFalse` null logicForest,
("RETURNING " <> intercalate ", " (pgFmtColumn qi <$> returnings)) `emptyOnFalse` null returnings
"UPDATE " <> fromQi mainQi <> " SET " <> cols,
"FROM (SELECT * FROM json_populate_recordset", "(null::", fromQi mainQi, ", " <> selectBody <> ")) _ ",
("WHERE " <> intercalate " AND " (pgFmtLogicTree mainQi <$> logicForest)) `emptyOnFalse` null logicForest,
("RETURNING " <> intercalate ", " (pgFmtColumn mainQi <$> returnings)) `emptyOnFalse` null returnings
]
where
qi = QualifiedIdentifier schema mainTbl
cols = intercalate ", " (pgFmtIdent <> const " = _." <> pgFmtIdent <$> S.toList uCols)
mutateRequestToQuery schema (Delete mainTbl logicForest returnings) =
mutateRequestToQuery (Delete mainQi logicForest returnings) =
unwords [
"WITH " <> ignoredBody,
"DELETE FROM ", fromQi qi,
("WHERE " <> intercalate " AND " (map (pgFmtLogicTree qi) logicForest)) `emptyOnFalse` null logicForest,
("RETURNING " <> intercalate ", " (map (pgFmtColumn qi) returnings)) `emptyOnFalse` null returnings
"DELETE FROM ", fromQi mainQi,
("WHERE " <> intercalate " AND " (map (pgFmtLogicTree mainQi) logicForest)) `emptyOnFalse` null logicForest,
("RETURNING " <> intercalate ", " (map (pgFmtColumn mainQi) returnings)) `emptyOnFalse` null returnings
]
where
qi = QualifiedIdentifier schema mainTbl
requestToCallProcQuery :: QualifiedIdentifier -> [PgArg] -> Bool -> Maybe PreferParameters -> SqlQuery
requestToCallProcQuery qi pgArgs returnsScalar preferParams =
@@ -176,15 +171,13 @@ requestToCallProcQuery qi pgArgs returnsScalar preferParams =
-- It only takes WHERE into account and doesn't include LIMIT/OFFSET because it would reduce the COUNT.
-- SELECT 1 is done instead of SELECT * to prevent doing expensive operations(like functions based on the columns)
-- inside the FROM target.
readRequestToCountQuery :: Schema -> ReadRequest -> SqlQuery
readRequestToCountQuery schema (Node (Select{where_=logicForest}, (mainTbl, _, _, _, _)) _) =
readRequestToCountQuery :: ReadRequest -> SqlQuery
readRequestToCountQuery (Node (Select{from=qi, where_=logicForest}, _) _) =
unwords [
"SELECT 1",
"FROM " <> fromQi qi,
("WHERE " <> intercalate " AND " (map (pgFmtLogicTree qi) logicForest)) `emptyOnFalse` null logicForest
]
where
qi = removeSourceCTESchema schema mainTbl
limitedQuery :: SqlQuery -> Maybe Integer -> SqlQuery
limitedQuery query maxRows = query <> maybe mempty (\x -> " LIMIT " <> show x) maxRows
+2 -9
View File
@@ -150,9 +150,8 @@ pgFmtFilter table (Filter fld (OpExpr hasNot oper)) = notOp <> " " <> case oper
(find ((==) . toLower $ v) ["null","true","false"])
pgFmtJoinCondition :: JoinCondition -> SqlFragment
pgFmtJoinCondition (JoinCondition (QualifiedIdentifier schema1 tName, col1) (QualifiedIdentifier schema2 ftName, col2)) =
pgFmtColumn (removeSourceCTESchema schema1 tName) col1 <> " = " <>
pgFmtColumn (removeSourceCTESchema schema2 ftName) col2
pgFmtJoinCondition (JoinCondition (qi1, col1) (qi2, col2)) =
pgFmtColumn qi1 col1 <> " = " <> pgFmtColumn qi2 col2
pgFmtLogicTree :: QualifiedIdentifier -> LogicTree -> SqlFragment
pgFmtLogicTree qi (Expr hasNot op forest) = notOp <> " (" <> intercalate (" " <> show op <> " ") (pgFmtLogicTree qi <$> forest) <> ")"
@@ -183,12 +182,6 @@ pgFmtAs _ _ (Just alias) = " AS " <> pgFmtIdent alias
trimNullChars :: Text -> Text
trimNullChars = T.takeWhile (/= '\x0')
-- On mutation and calling proc cases we wrap the target table in a WITH {sourceCTEName}
-- if this happens remove the schema `FROM "schema"."{sourceCTEName}"` and use only the
-- `FROM "{sourceCTEName}"`. If the schema remains the FROM would be invalid.
removeSourceCTESchema :: Schema -> TableName -> QualifiedIdentifier
removeSourceCTESchema schema tbl = QualifiedIdentifier (if tbl == sourceCTEName then mempty else schema) tbl
countF :: SqlQuery -> Bool -> (SqlFragment, SqlFragment)
countF countQuery shouldCount =
if shouldCount
+8 -5
View File
@@ -188,6 +188,9 @@ data Table = Table {
instance Eq Table where
Table{tableSchema=s1,tableName=n1} == Table{tableSchema=s2,tableName=n2} = s1 == s2 && n1 == n2
tableQi :: Table -> QualifiedIdentifier
tableQi Table{tableSchema=s, tableName=n} = QualifiedIdentifier s n
newtype ForeignKey = ForeignKey { fkCol :: Column } deriving (Show, Eq, Ord)
data Column =
@@ -397,11 +400,11 @@ data JoinCondition = JoinCondition (QualifiedIdentifier, FieldName)
data ReadQuery = Select {
select :: [SelectItem]
, from :: TableName
, from :: QualifiedIdentifier
-- | A table alias is used in case of self joins
, fromAlias :: Maybe Alias
-- | Only used for Many to Many joins. Parent and Child joins use explicit joins.
, implicitJoins :: [TableName]
, implicitJoins :: [QualifiedIdentifier]
, where_ :: [LogicTree]
, joinConditions :: [JoinCondition]
, order :: [OrderTerm]
@@ -410,20 +413,20 @@ data ReadQuery = Select {
data MutateQuery =
Insert {
in_ :: TableName
in_ :: QualifiedIdentifier
, insCols :: S.Set FieldName
, onConflict :: Maybe (PreferResolution, [FieldName])
, where_ :: [LogicTree]
, returning :: [FieldName]
}|
Update {
in_ :: TableName
in_ :: QualifiedIdentifier
, updCols :: S.Set FieldName
, where_ :: [LogicTree]
, returning :: [FieldName]
}|
Delete {
in_ :: TableName
in_ :: QualifiedIdentifier
, where_ :: [LogicTree]
, returning :: [FieldName]
} deriving (Show, Eq)