fix postgres query error when computed fields included in mutation response, fix #4035

This commit is contained in:
rakeshkky 2020-03-10 18:15:47 +05:30
parent db724f719d
commit 9128c69a80
13 changed files with 169 additions and 38 deletions

View File

@ -457,7 +457,7 @@ insertMultipleObjects strfyNum role tn multiObjIns addCols mutOutput errP =
let affRows = sum $ map fst insResps let affRows = sum $ map fst insResps
columnValues = catMaybes $ map snd insResps columnValues = catMaybes $ map snd insResps
cteExp <- mkSelCTEFromColVals tn tableColInfos columnValues cteExp <- mkSelCTEFromColVals tn tableColInfos columnValues
let sql = toSQL $ RR.mkMutationOutputExp tn (Just affRows) cteExp mutOutput strfyNum let sql = toSQL $ RR.mkMutationOutputExp tn tableColInfos (Just affRows) cteExp mutOutput strfyNum
runIdentity . Q.getRow runIdentity . Q.getRow
<$> Q.rawQE dmlTxErrorHandler (Q.fromBuilder sql) [] False <$> Q.rawQE dmlTxErrorHandler (Q.fromBuilder sql) [] False

View File

@ -38,19 +38,19 @@ runMutation mut =
hasNestedFld $ _mOutput mut hasNestedFld $ _mOutput mut
mutateAndReturn :: Mutation -> Q.TxE QErr EncJSON mutateAndReturn :: Mutation -> Q.TxE QErr EncJSON
mutateAndReturn (Mutation qt (cte, p) mutationOutput _ strfyNum) = mutateAndReturn (Mutation qt (cte, p) mutationOutput allCols strfyNum) =
encJFromBS . runIdentity . Q.getRow encJFromBS . runIdentity . Q.getRow
<$> Q.rawQE dmlTxErrorHandler (Q.fromBuilder $ toSQL selWith) <$> Q.rawQE dmlTxErrorHandler (Q.fromBuilder $ toSQL selWith)
(toList p) True (toList p) True
where where
selWith = mkMutationOutputExp qt Nothing cte mutationOutput strfyNum selWith = mkMutationOutputExp qt allCols Nothing cte mutationOutput strfyNum
mutateAndSel :: Mutation -> Q.TxE QErr EncJSON mutateAndSel :: Mutation -> Q.TxE QErr EncJSON
mutateAndSel (Mutation qt q mutationOutput allCols strfyNum) = do mutateAndSel (Mutation qt q mutationOutput allCols strfyNum) = do
-- Perform mutation and fetch unique columns -- Perform mutation and fetch unique columns
MutateResp _ columnVals <- mutateAndFetchCols qt allCols q strfyNum MutateResp _ columnVals <- mutateAndFetchCols qt allCols q strfyNum
selCTE <- mkSelCTEFromColVals qt allCols columnVals selCTE <- mkSelCTEFromColVals qt allCols columnVals
let selWith = mkMutationOutputExp qt Nothing selCTE mutationOutput strfyNum let selWith = mkMutationOutputExp qt allCols Nothing selCTE mutationOutput strfyNum
-- Perform select query and fetch returning fields -- Perform select query and fetch returning fields
encJFromBS . runIdentity . Q.getRow encJFromBS . runIdentity . Q.getRow
<$> Q.rawQE dmlTxErrorHandler (Q.fromBuilder $ toSQL selWith) [] True <$> Q.rawQE dmlTxErrorHandler (Q.fromBuilder $ toSQL selWith) [] True
@ -108,8 +108,7 @@ mkSelCTEFromColVals qt allCols colVals =
where where
rowAlias = Iden "row" rowAlias = Iden "row"
extractor = S.selectStar' $ S.QualIden rowAlias $ Just $ S.TypeAnn $ toSQLTxt qt extractor = S.selectStar' $ S.QualIden rowAlias $ Just $ S.TypeAnn $ toSQLTxt qt
sortedCols = flip sortBy allCols $ \lCol rCol -> sortedCols = sortCols allCols
compare (pgiPosition lCol) (pgiPosition rCol)
mkTupsFromColVal colVal = mkTupsFromColVal colVal =
fmap S.TupleExp $ forM sortedCols $ \ci -> do fmap S.TupleExp $ forM sortedCols $ \ci -> do
let pgCol = pgiColumn ci let pgCol = pgiColumn ci

View File

@ -93,12 +93,8 @@ mkDefaultMutFlds = MOutMultirowFields . \case
where where
mutFlds = [("affected_rows", MCount)] mutFlds = [("affected_rows", MCount)]
qualTableToAliasIden :: QualifiedTable -> Iden mkMutFldExp :: Iden -> Maybe Int -> Bool -> MutFld -> S.SQLExp
qualTableToAliasIden qt = mkMutFldExp cteAlias preCalAffRows strfyNum = \case
Iden $ snakeCaseTable qt <> "__mutation_result_alias"
mkMutFldExp :: QualifiedTable -> Maybe Int -> Bool -> MutFld -> S.SQLExp
mkMutFldExp qt preCalAffRows strfyNum = \case
MCount -> MCount ->
let countExp = S.SESelect $ let countExp = S.SESelect $
S.mkSelect S.mkSelect
@ -112,28 +108,70 @@ mkMutFldExp qt preCalAffRows strfyNum = \case
tabPerm = TablePerm annBoolExpTrue Nothing tabPerm = TablePerm annBoolExpTrue Nothing
in S.SESelect $ mkSQLSelect JASMultipleRows $ in S.SESelect $ mkSQLSelect JASMultipleRows $
AnnSelG selFlds tabFrom tabPerm noTableArgs strfyNum AnnSelG selFlds tabFrom tabPerm noTableArgs strfyNum
where
cteAlias = qualTableToAliasIden qt
{- Note [Mutation output expression]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
An example output expression for INSERT mutation:
WITH "<table-name>__mutation_result_alias" AS (
INSERT INTO <table-name> (<insert-column>[..])
VALUES
(<insert-value-row>[..])
ON CONFLICT ON CONSTRAINT "<table-constraint-name>" DO NOTHING RETURNING *,
-- An extra column expression which performs the 'CHECK' validation
CASE
WHEN (<CHECK Condition>) THEN NULL
ELSE "hdb_catalog"."check_violation"('insert check constraint failed')
END
),
"<table-name>__all_columns_alias" AS (
-- Only extract columns from mutated rows. Columns sorted by ordinal position so that
-- resulted rows can be casted to table type.
SELECT (<table-column>[..])
FROM
"<table-name>__mutation_result_alias"
)
<SELECT statement to generate mutation response using '<table-name>__all_columns_alias' as FROM>
-}
-- | Generate mutation output expression with given mutation CTE statement.
-- See Note [Mutation output expression].
mkMutationOutputExp mkMutationOutputExp
:: QualifiedTable -> Maybe Int -> S.CTE -> MutationOutput -> Bool -> S.SelectWith :: QualifiedTable
mkMutationOutputExp qt preCalAffRows cte mutOutput strfyNum = -> [PGColumnInfo]
S.SelectWith [(S.Alias cteAlias, cte)] sel -> Maybe Int
-> S.CTE
-> MutationOutput
-> Bool
-> S.SelectWith
mkMutationOutputExp qt allCols preCalAffRows cte mutOutput strfyNum =
S.SelectWith [ (S.Alias mutationResultAlias, cte)
, (S.Alias allColumnsAlias, allColumnsSelect)
] sel
where where
cteAlias = qualTableToAliasIden qt mutationResultAlias = Iden $ snakeCaseQualObject qt <> "__mutation_result_alias"
allColumnsAlias = Iden $ snakeCaseQualObject qt <> "__all_columns_alias"
allColumnsSelect = S.CTESelect $ S.mkSelect
{ S.selExtr = map S.mkExtr $ map pgiColumn $ sortCols allCols
, S.selFrom = Just $ S.mkIdenFromExp mutationResultAlias
}
sel = S.mkSelect { S.selExtr = [S.Extractor extrExp Nothing] } sel = S.mkSelect { S.selExtr = [S.Extractor extrExp Nothing] }
where
extrExp = case mutOutput of
MOutMultirowFields mutFlds ->
let jsonBuildObjArgs = flip concatMap mutFlds $
\(FieldName k, mutFld) -> [ S.SELit k
, mkMutFldExp allColumnsAlias preCalAffRows strfyNum mutFld
]
in S.SEFnApp "json_build_object" jsonBuildObjArgs Nothing
extrExp = case mutOutput of MOutSinglerowObject annFlds ->
MOutMultirowFields mutFlds -> let tabFrom = FromIden allColumnsAlias
let jsonBuildObjArgs = flip concatMap mutFlds $ tabPerm = TablePerm annBoolExpTrue Nothing
\(FieldName k, mutFld) -> [S.SELit k, mkMutFldExp qt preCalAffRows strfyNum mutFld] in S.SESelect $ mkSQLSelect JASSingleObject $
in S.SEFnApp "json_build_object" jsonBuildObjArgs Nothing AnnSelG annFlds tabFrom tabPerm noTableArgs strfyNum
MOutSinglerowObject annFlds ->
let tabFrom = FromIden cteAlias
tabPerm = TablePerm annBoolExpTrue Nothing
in S.SESelect $ mkSQLSelect JASSingleObject $
AnnSelG annFlds tabFrom tabPerm noTableArgs strfyNum
checkRetCols checkRetCols

View File

@ -337,7 +337,7 @@ convColRhs tableQual = \case
curVarNum <- get curVarNum <- get
put $ curVarNum + 1 put $ curVarNum + 1
let newIden = Iden $ "_be_" <> T.pack (show curVarNum) <> "_" let newIden = Iden $ "_be_" <> T.pack (show curVarNum) <> "_"
<> snakeCaseTable relTN <> snakeCaseQualObject relTN
newIdenQ = S.QualIden newIden Nothing newIdenQ = S.QualIden newIden Nothing
annRelBoolExp <- convBoolRhs' newIdenQ nesAnn annRelBoolExp <- convBoolRhs' newIdenQ nesAnn
let backCompExp = foldr (S.BEBin S.AndOp) (S.BELit True) $ let backCompExp = foldr (S.BEBin S.AndOp) (S.BELit True) $

View File

@ -44,6 +44,7 @@ module Hasura.RQL.Types.Table
, getFieldInfoM , getFieldInfoM
, getPGColumnInfoM , getPGColumnInfoM
, getCols , getCols
, sortCols
, getRels , getRels
, getComputedFieldInfos , getComputedFieldInfos
@ -193,6 +194,10 @@ fieldInfoGraphQLNames = \case
getCols :: FieldInfoMap FieldInfo -> [PGColumnInfo] getCols :: FieldInfoMap FieldInfo -> [PGColumnInfo]
getCols = mapMaybe (^? _FIColumn) . M.elems getCols = mapMaybe (^? _FIColumn) . M.elems
-- | Sort columns based on their ordinal position
sortCols :: [PGColumnInfo] -> [PGColumnInfo]
sortCols = sortBy (\l r -> compare (pgiPosition l) (pgiPosition r))
getRels :: FieldInfoMap FieldInfo -> [RelInfo] getRels :: FieldInfoMap FieldInfo -> [RelInfo]
getRels = mapMaybe (^? _FIRelationship) . M.elems getRels = mapMaybe (^? _FIRelationship) . M.elems

View File

@ -11,7 +11,6 @@ module Hasura.SQL.Types
, isView , isView
, QualifiedTable , QualifiedTable
, snakeCaseTable
, QualifiedFunction , QualifiedFunction
, PGDescription(..) , PGDescription(..)
@ -300,10 +299,6 @@ snakeCaseQualObject (QualifiedObject sn o)
type QualifiedTable = QualifiedObject TableName type QualifiedTable = QualifiedObject TableName
snakeCaseTable :: QualifiedObject TableName -> T.Text
snakeCaseTable (QualifiedObject sn tn) =
getSchemaTxt sn <> "_" <> getTableTxt tn
type QualifiedFunction = QualifiedObject FunctionName type QualifiedFunction = QualifiedObject FunctionName
newtype PGDescription newtype PGDescription

View File

@ -30,6 +30,11 @@ query:
id id
name name
is_registered is_registered
get_articles(args: {search: "Article"}){
id
title
content
}
} }
} }
} }

View File

@ -27,12 +27,31 @@ args:
author_id INTEGER REFERENCES author(id), author_id INTEGER REFERENCES author(id),
is_published BOOLEAN, is_published BOOLEAN,
published_on TIMESTAMP published_on TIMESTAMP
) );
CREATE FUNCTION fetch_articles(search text, author_row author)
RETURNS SETOF article AS $$
SELECT *
FROM article
WHERE
( title ilike ('%' || search || '%')
OR content ilike ('%' || search || '%')
) AND author_id = author_row.id
$$ LANGUAGE sql STABLE;
- type: track_table - type: track_table
args: args:
schema: public schema: public
name: article name: article
- type: add_computed_field
args:
table: author
name: get_articles
definition:
function: fetch_articles
table_argument: author_row
#Create resident table #Create resident table
- type: run_sql - type: run_sql
args: args:

View File

@ -13,6 +13,7 @@ args:
sql: | sql: |
drop table address; drop table address;
drop table resident; drop table resident;
drop function fetch_articles(text,author);
drop table article; drop table article;
drop table blog; drop table blog;
drop table author; drop table author;

View File

@ -27,12 +27,30 @@ args:
is_published BOOLEAN, is_published BOOLEAN,
published_on TIMESTAMP, published_on TIMESTAMP,
PRIMARY KEY (id,version) PRIMARY KEY (id,version)
) );
CREATE FUNCTION fetch_articles(search text, author_row author)
RETURNS SETOF article AS $$
SELECT *
FROM article
WHERE
( title ilike ('%' || search || '%')
OR content ilike ('%' || search || '%')
) AND author_id = author_row.id
$$ LANGUAGE sql STABLE;
- type: track_table - type: track_table
args: args:
schema: public schema: public
name: article name: article
- type: add_computed_field
args:
table: author
name: get_articles
definition:
function: fetch_articles
table_argument: author_row
#Object relationship #Object relationship
- type: create_object_relationship - type: create_object_relationship
@ -77,6 +95,17 @@ args:
filter: filter:
id: X-HASURA-USER-ID id: X-HASURA-USER-ID
#Author update permission for user
- type: create_update_permission
args:
table: author
role: user
permission:
columns:
- name
filter:
id: X-Hasura-User-Id
#Article select permission for user #Article select permission for user
- type: create_select_permission - type: create_select_permission
args: args:

View File

@ -4,6 +4,7 @@ args:
- type: run_sql - type: run_sql
args: args:
sql: | sql: |
DROP function fetch_articles(text,author);
DROP table article; DROP table article;
DROP table author; DROP table author;
DROP table person; DROP table person;

View File

@ -0,0 +1,36 @@
description: Update name of an author
url: /v1/graphql
status: 200
headers:
X-Hasura-Role: user
X-Hasura-User-Id: '1'
response:
data:
update_author:
affected_rows: 1
returning:
- id: 1
name: Author 1 updated
get_articles:
- id: 2
title: Article 1
content: Sample article 1, content version 1.0.1
query:
query: |
mutation {
update_author(
_set: {name: "Author 1 updated"}
where: {id: {_eq: 1}}
){
affected_rows
returning{
id
name
get_articles(args: {search: "1.0.1"}){
id
title
content
}
}
}
}

View File

@ -380,6 +380,9 @@ class TestGraphqlUpdateJsonB:
@use_mutation_fixtures @use_mutation_fixtures
class TestGraphqlUpdatePermissions: class TestGraphqlUpdatePermissions:
def test_user_update_author(self, hge_ctx, transport):
check_query_f(hge_ctx, self.dir() + "/user_update_author.yaml", transport)
def test_user_can_update_unpublished_article(self, hge_ctx, transport): def test_user_can_update_unpublished_article(self, hge_ctx, transport):
check_query_f(hge_ctx, self.dir() + "/user_can_update_unpublished_article.yaml") check_query_f(hge_ctx, self.dir() + "/user_can_update_unpublished_article.yaml")
@ -388,7 +391,7 @@ class TestGraphqlUpdatePermissions:
def test_user_cannot_update_another_users_article(self, hge_ctx, transport): def test_user_cannot_update_another_users_article(self, hge_ctx, transport):
check_query_f(hge_ctx, self.dir() + "/user_cannot_update_another_users_article.yaml") check_query_f(hge_ctx, self.dir() + "/user_cannot_update_another_users_article.yaml")
def test_user_cannot_publish(self, hge_ctx, transport): def test_user_cannot_publish(self, hge_ctx, transport):
check_query_f(hge_ctx, self.dir() + "/user_cannot_publish.yaml") check_query_f(hge_ctx, self.dir() + "/user_cannot_publish.yaml")