From 50f2cc16abaac5a93705f2b6df6e85e9fb60fd3d Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Mon, 30 Sep 2019 13:31:14 -0500 Subject: [PATCH] refactor: rm schema arg from QueryBuilder funcs * Change TableName to QualifiedIdentifier in ReadQuery and MutateQuery. * Move removeSourceCTESchema to DbRequestBuilder. --- src/PostgREST/App.hs | 9 ++--- src/PostgREST/DbRequestBuilder.hs | 55 +++++++++++++++++---------- src/PostgREST/QueryBuilder.hs | 55 ++++++++++++--------------- src/PostgREST/QueryBuilder/Private.hs | 11 +----- src/PostgREST/Types.hs | 13 ++++--- 5 files changed, 72 insertions(+), 71 deletions(-) diff --git a/src/PostgREST/App.hs b/src/PostgREST/App.hs index 07b7803fe..4c67c3fbc 100644 --- a/src/PostgREST/App.hs +++ b/src/PostgREST/App.hs @@ -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 diff --git a/src/PostgREST/DbRequestBuilder.hs b/src/PostgREST/DbRequestBuilder.hs index 6dcc84d6a..f0e4a9418 100644 --- a/src/PostgREST/DbRequestBuilder.hs +++ b/src/PostgREST/DbRequestBuilder.hs @@ -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 diff --git a/src/PostgREST/QueryBuilder.hs b/src/PostgREST/QueryBuilder.hs index 269be4515..7e0061d7f 100644 --- a/src/PostgREST/QueryBuilder.hs +++ b/src/PostgREST/QueryBuilder.hs @@ -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 diff --git a/src/PostgREST/QueryBuilder/Private.hs b/src/PostgREST/QueryBuilder/Private.hs index b1d00539c..8cb9ec274 100644 --- a/src/PostgREST/QueryBuilder/Private.hs +++ b/src/PostgREST/QueryBuilder/Private.hs @@ -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 diff --git a/src/PostgREST/Types.hs b/src/PostgREST/Types.hs index 09f076ce9..02e165f85 100644 --- a/src/PostgREST/Types.hs +++ b/src/PostgREST/Types.hs @@ -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)