remove hdb_views for inserts (#3598)

* WIP: Remove hdb_views for inserts

* Show failing row in check constraint error

* Revert "Show failing row in check constraint error"

This reverts commit dd2cac29d0.

* Use the better query plan

* Simplify things

* fix cli test

* Update downgrading.rst

* remove 1.1 asset for cli
This commit is contained in:
Phil Freeman 2020-01-15 20:53:28 -08:00 committed by Shahidh K Muhammed
parent f6a43fe3ba
commit 9ed8f717a7
23 changed files with 161 additions and 348 deletions

View File

@ -103,6 +103,12 @@ You can downgrade the catalogue from a particular version to its previous versio
:backlinks: none
:depth: 1
:local:
From 30 to 29
"""""""""""""
.. code-block:: plpgsql
DROP FUNCTION hdb_catalog.check_violation();
From 27 to 26
"""""""""""""

View File

@ -280,7 +280,6 @@ library
, Hasura.RQL.DDL.Relationship
, Hasura.RQL.DDL.Deps
, Hasura.RQL.DDL.Permission.Internal
, Hasura.RQL.DDL.Permission.Triggers
, Hasura.RQL.DDL.Permission
, Hasura.RQL.DDL.Relationship.Rename
, Hasura.RQL.DDL.Relationship.Types

View File

@ -29,7 +29,10 @@ import Hasura.GraphQL.Resolve.Mutation
import Hasura.GraphQL.Resolve.Select
import Hasura.GraphQL.Validate.Field
import Hasura.GraphQL.Validate.Types
import Hasura.RQL.DML.Internal (convPartialSQLExp, dmlTxErrorHandler,
import Hasura.RQL.DML.Insert (insertCheckExpr)
import Hasura.RQL.DML.Internal (convPartialSQLExp,
convAnnBoolExpPartialSQL,
dmlTxErrorHandler,
sessVarFromCurrentSetting)
import Hasura.RQL.DML.Mutation
import Hasura.RQL.GBoolExp (toSQLBoolExp)
@ -47,7 +50,7 @@ data AnnIns a
= AnnIns
{ _aiInsObj :: !a
, _aiConflictClause :: !(Maybe RI.ConflictClauseP1)
, _aiView :: !QualifiedTable
, _aiCheckCond :: AnnBoolExpPartialSQL
, _aiTableCols :: ![PGColumnInfo]
, _aiDefVals :: !(Map.HashMap PGCol S.SQLExp)
} deriving (Show, Eq, Functor, Foldable, Traversable)
@ -131,7 +134,7 @@ traverseInsObj rim allColMap (gName, annVal) defVal@(AnnInsObj cols objRels arrR
throw500 $ "relation " <> relName <<> " not found"
let rTable = riRTable relInfo
InsCtx rtView rtColMap rtDefVals rtRelInfoMap rtUpdPerm <- getInsCtx rTable
InsCtx rtColMap checkCond rtDefVals rtRelInfoMap rtUpdPerm <- getInsCtx rTable
let rtCols = Map.elems rtColMap
rtDefValsRes <- mapM (convPartialSQLExp sessVarFromCurrentSetting) rtDefVals
@ -140,7 +143,7 @@ traverseInsObj rim allColMap (gName, annVal) defVal@(AnnInsObj cols objRels arrR
dataObj <- asObject dataVal
annDataObj <- mkAnnInsObj rtRelInfoMap rtColMap dataObj
ccM <- forM onConflictM $ parseOnConflict rTable rtUpdPerm rtColMap
let singleObjIns = AnnIns annDataObj ccM rtView rtCols rtDefValsRes
let singleObjIns = AnnIns annDataObj ccM checkCond rtCols rtDefValsRes
objRelIns = RelIns singleObjIns relInfo
return (AnnInsObj cols (objRelIns:objRels) arrRels)
@ -151,8 +154,7 @@ traverseInsObj rim allColMap (gName, annVal) defVal@(AnnInsObj cols objRels arrR
dataObj <- asObject arrDataVal
mkAnnInsObj rtRelInfoMap rtColMap dataObj
ccM <- forM onConflictM $ parseOnConflict rTable rtUpdPerm rtColMap
let multiObjIns = AnnIns annDataObjs ccM rtView
rtCols rtDefValsRes
let multiObjIns = AnnIns annDataObjs ccM checkCond rtCols rtDefValsRes
arrRelIns = RelIns multiObjIns relInfo
return (AnnInsObj cols objRels (arrRelIns:arrRels))
-- if array relation insert input data has empty objects
@ -224,20 +226,26 @@ mkInsertQ
-> [PGColWithValue]
-> Map.HashMap PGCol S.SQLExp
-> RoleName
-> m (CTEExp, Maybe RI.ConflictCtx)
mkInsertQ vn onConflictM insCols defVals role = do
-> AnnBoolExpSQL
-> m CTEExp
mkInsertQ tn onConflictM insCols defVals role checkCond = do
(givenCols, args) <- flip runStateT Seq.Empty $ toSQLExps insCols
let sqlConflict = RI.toSQLConflict <$> onConflictM
sqlExps = mkSQLRow defVals givenCols
valueExp = S.ValuesExp [S.TupleExp sqlExps]
tableCols = Map.keys defVals
sqlInsert =
S.SQLInsert vn tableCols valueExp sqlConflict $ Just S.returningStar
adminIns = return (CTEExp (S.CTEInsert sqlInsert) args, Nothing)
S.SQLInsert tn tableCols valueExp sqlConflict
. Just
$ S.RetExp
[ S.selectStar
, insertCheckExpr (toSQLBoolExp (S.QualTable tn) checkCond)
]
adminIns = return (CTEExp (S.CTEInsert sqlInsert) args)
nonAdminInsert = do
ccM <- mapM RI.extractConflictCtx onConflictM
let cteIns = S.CTEInsert sqlInsert{S.siConflict=Nothing}
return (CTEExp cteIns args, ccM)
let cteIns = S.CTEInsert sqlInsert
return (CTEExp cteIns args)
bool nonAdminInsert adminIns $ isAdmin role
@ -400,10 +408,11 @@ insertObj strfyNum role tn singleObjIns addCols = do
finalInsCols = cols <> objRelDeterminedCols <> addCols
-- prepare insert query as with expression
(CTEExp cte insPArgs, ccM) <-
mkInsertQ vn onConflictM finalInsCols defVals role
checkExpr <- convAnnBoolExpPartialSQL sessVarFromCurrentSetting checkCond
CTEExp cte insPArgs <-
mkInsertQ tn onConflictM finalInsCols defVals role checkExpr
RI.setConflictCtx ccM
MutateResp affRows colVals <- mutateAndFetchCols tn allCols (cte, insPArgs) strfyNum
colValM <- asSingleObject colVals
cteExp <- mkSelCTE tn allCols colValM
@ -413,7 +422,7 @@ insertObj strfyNum role tn singleObjIns addCols = do
return (totAffRows, cteExp)
where
AnnIns annObj onConflictM vn allCols defVals = singleObjIns
AnnIns annObj onConflictM checkCond allCols defVals = singleObjIns
AnnInsObj cols objRels arrRels = annObj
arrRelDepCols = flip getColInfos allCols $
@ -445,7 +454,7 @@ insertMultipleObjects
insertMultipleObjects strfyNum role tn multiObjIns addCols mutFlds errP =
bool withoutRelsInsert withRelsInsert anyRelsToInsert
where
AnnIns insObjs onConflictM vn tableColInfos defVals = multiObjIns
AnnIns insObjs onConflictM checkCond tableColInfos defVals = multiObjIns
singleObjInserts = multiToSingles multiObjIns
insCols = map _aioColumns insObjs
allInsObjRels = concatMap _aioObjRels insObjs
@ -465,10 +474,13 @@ insertMultipleObjects strfyNum role tn multiObjIns addCols mutFlds errP =
(sqlRows, prepArgs) <- flip runStateT Seq.Empty $ do
rowsWithCol <- mapM toSQLExps withAddCols
return $ map (mkSQLRow defVals) rowsWithCol
let insQP1 = RI.InsertQueryP1 tn vn tableCols sqlRows onConflictM mutFlds tableColInfos
checkExpr <- convAnnBoolExpPartialSQL sessVarFromCurrentSetting checkCond
let insQP1 = RI.InsertQueryP1 tn tableCols sqlRows onConflictM
(Just checkExpr) mutFlds tableColInfos
p1 = (insQP1, prepArgs)
bool (RI.nonAdminInsert strfyNum p1) (RI.insertP2 strfyNum p1) $ isAdmin role
RI.insertP2 strfyNum p1
-- insert each object with relations
withRelsInsert = withErrPath $ do
@ -513,14 +525,14 @@ convertInsert role tn fld = prefixErrPath fld $ do
(withEmptyObjs mutFldsRes) $ null annVals
where
withNonEmptyObjs annVals mutFlds = do
InsCtx vn tableColMap defValMap relInfoMap updPerm <- getInsCtx tn
InsCtx tableColMap checkCond defValMap relInfoMap updPerm <- getInsCtx tn
annObjs <- mapM asObject annVals
annInsObjs <- forM annObjs $ mkAnnInsObj relInfoMap tableColMap
conflictClauseM <- forM onConflictM $ parseOnConflict tn updPerm tableColMap
defValMapRes <- mapM (convPartialSQLExp sessVarFromCurrentSetting)
defValMap
let multiObjIns = AnnIns annInsObjs conflictClauseM
vn tableCols defValMapRes
let multiObjIns = AnnIns annInsObjs conflictClauseM checkCond
tableCols defValMapRes
tableCols = Map.elems tableColMap
strfyNum <- stringifyNum <$> asks getter
return $ prefixErrPath fld $ insertMultipleObjects strfyNum role tn

View File

@ -179,8 +179,8 @@ data UpdPermForIns
data InsCtx
= InsCtx
{ icView :: !QualifiedTable
, icAllCols :: !PGColGNameMap
{ icAllCols :: !PGColGNameMap
, icCheck :: !AnnBoolExpPartialSQL
, icSet :: !PreSetColsPartial
, icRelations :: !RelationInfoMap
, icUpdPerm :: !(Maybe UpdPermForIns)

View File

@ -509,13 +509,13 @@ mkInsCtx role tableCache fields insPermInfo updPermM = do
isInsertable insPermM viewInfoM && isValidRel relName remoteTable
let relInfoMap = Map.fromList $ catMaybes relTupsM
return $ InsCtx iView gNamePGColMap setCols relInfoMap updPermForIns
return $ InsCtx gNamePGColMap checkCond setCols relInfoMap updPermForIns
where
gNamePGColMap = mkPGColGNameMap allCols
allCols = getCols fields
rels = getValidRels fields
iView = ipiView insPermInfo
setCols = ipiSet insPermInfo
checkCond = ipiCheck insPermInfo
updPermForIns = mkUpdPermForIns <$> updPermM
mkUpdPermForIns upi = UpdPermForIns (toList $ upiCols upi)
(upiFilter upi) (upiSet upi)
@ -525,11 +525,10 @@ mkInsCtx role tableCache fields insPermInfo updPermM = do
mkAdminInsCtx
:: MonadError QErr m
=> QualifiedTable
-> TableCache
=> TableCache
-> FieldInfoMap FieldInfo
-> m InsCtx
mkAdminInsCtx tn tc fields = do
mkAdminInsCtx tc fields = do
relTupsM <- forM rels $ \relInfo -> do
let remoteTable = riRTable relInfo
relName = riName relInfo
@ -541,7 +540,7 @@ mkAdminInsCtx tn tc fields = do
let relInfoMap = Map.fromList $ catMaybes relTupsM
updPerm = UpdPermForIns updCols noFilter Map.empty
return $ InsCtx tn colGNameMap Map.empty relInfoMap (Just updPerm)
return $ InsCtx colGNameMap noFilter Map.empty relInfoMap (Just updPerm)
where
allCols = getCols fields
colGNameMap = mkPGColGNameMap allCols
@ -667,7 +666,7 @@ mkGCtxMapTable tableCache funcCache tabInfo = do
m <- flip Map.traverseWithKey rolePerms $
mkGCtxRole tableCache tn descM fields primaryKey validConstraints
tabFuncs viewInfo customConfig
adminInsCtx <- mkAdminInsCtx tn tableCache fields
adminInsCtx <- mkAdminInsCtx tableCache fields
adminSelFlds <- mkAdminSelFlds fields tableCache
let adminCtx = mkGCtxRole' tn descM (Just (cols, icRelations adminInsCtx))
(Just (True, adminSelFlds)) (Just cols) (Just ())

View File

@ -7,37 +7,26 @@ module Hasura.RQL.DDL.Permission
, InsPerm(..)
, InsPermDef
, CreateInsPerm
, clearInsInfra
, buildInsInfra
, buildInsPermInfo
, DropInsPerm
, dropInsPermP2
, SelPerm(..)
, SelPermDef
, CreateSelPerm
, buildSelPermInfo
, DropSelPerm
, dropSelPermP2
, UpdPerm(..)
, UpdPermDef
, CreateUpdPerm
, buildUpdPermInfo
, DropUpdPerm
, dropUpdPermP2
, DelPerm(..)
, DelPermDef
, CreateDelPerm
, buildDelPermInfo
, DropDelPerm
, dropDelPermP2
, IsPerm(..)
, addPermP2
, dropView
, DropPerm
, runDropPerm
@ -51,21 +40,17 @@ import Hasura.EncJSON
import Hasura.Incremental (Cacheable)
import Hasura.Prelude
import Hasura.RQL.DDL.Permission.Internal
import Hasura.RQL.DDL.Permission.Triggers
import Hasura.RQL.DML.Internal hiding (askPermInfo)
import Hasura.RQL.GBoolExp
import Hasura.RQL.Types
import Hasura.SQL.Types
import qualified Database.PG.Query as Q
import qualified Hasura.SQL.DML as S
import Data.Aeson
import Data.Aeson.Casing
import Data.Aeson.TH
import Language.Haskell.TH.Syntax (Lift)
import qualified Crypto.Hash as CH
import qualified Data.HashMap.Strict as HM
import qualified Data.HashSet as HS
import qualified Data.Text as T
@ -83,31 +68,6 @@ $(deriveJSON (aesonDrop 2 snakeCase){omitNothingFields=True} ''InsPerm)
type InsPermDef = PermDef InsPerm
type CreateInsPerm = CreatePerm InsPerm
buildViewName :: QualifiedTable -> RoleName -> PermType -> QualifiedTable
buildViewName qt rn pt = QualifiedObject hdbViewsSchema tableName
where
-- Generate a unique hash for view name from role name, permission type and qualified table.
-- See Note [Postgres identifier length limitations].
-- Black2b_224 generates 56 character hash. See Note [Blake2b faster than SHA-256].
-- Refer https://github.com/hasura/graphql-engine/issues/3444.
tableName = TableName $ T.pack $ show hash
hash :: CH.Digest CH.Blake2b_224 =
CH.hash $ txtToBs $ roleNameToTxt rn <> "__" <> T.pack (show pt) <> "__" <> qualObjectToText qt
buildView :: QualifiedTable -> QualifiedTable -> Q.Query
buildView tn vn =
Q.fromBuilder $ mconcat
[ "CREATE VIEW " <> toSQL vn
, " AS SELECT * FROM " <> toSQL tn
]
dropView :: QualifiedTable -> Q.Tx ()
dropView vn =
Q.unitQ dropViewS () False
where
dropViewS = Q.fromBuilder $
"DROP VIEW IF EXISTS " <> toSQL vn
procSetObj
:: (QErrM m)
=> QualifiedTable
@ -136,7 +96,7 @@ buildInsPermInfo
-> FieldInfoMap FieldInfo
-> PermDef InsPerm
-> m (WithDeps InsPermInfo)
buildInsPermInfo tn fieldInfoMap (PermDef rn (InsPerm chk set mCols) _) =
buildInsPermInfo tn fieldInfoMap (PermDef _rn (InsPerm chk set mCols) _) =
withPathK "permission" $ do
(be, beDeps) <- withPathK "check" $ procBoolExp tn fieldInfoMap chk
(setColsSQL, setHdrs, setColDeps) <- procSetObj tn fieldInfoMap set
@ -147,56 +107,19 @@ buildInsPermInfo tn fieldInfoMap (PermDef rn (InsPerm chk set mCols) _) =
insColDeps = map (mkColDep DRUntyped tn) insCols
deps = mkParentDep tn : beDeps ++ setColDeps ++ insColDeps
insColsWithoutPresets = insCols \\ HM.keys setColsSQL
return (InsPermInfo (HS.fromList insColsWithoutPresets) vn be setColsSQL reqHdrs, deps)
return (InsPermInfo (HS.fromList insColsWithoutPresets) be setColsSQL reqHdrs, deps)
where
vn = buildViewName tn rn PTInsert
allCols = map pgiColumn $ getCols fieldInfoMap
insCols = fromMaybe allCols $ convColSpec fieldInfoMap <$> mCols
buildInsInfra :: QualifiedTable -> InsPermInfo -> Q.TxE QErr ()
buildInsInfra tn (InsPermInfo _ vn be _ _) = do
resolvedBoolExp <- {-# SCC "buildInsInfra/convAnnBoolExpPartialSQL" #-} convAnnBoolExpPartialSQL sessVarFromCurrentSetting be
let trigFnQ = {-# SCC "buildInsInfra/buildInsTrigFn" #-} buildInsTrigFn vn tn $ toSQLBoolExp (S.QualVar "NEW") resolvedBoolExp
{-# SCC "buildInsInfra/execute" #-} Q.catchE defaultTxErrorHandler $ do
-- Create the view
dropView vn
Q.unitQ (buildView tn vn) () False
-- Inject defaults on the view
Q.discardQ (injectDefaults vn tn) () False
-- Construct a trigger function
Q.unitQ trigFnQ () False
-- Add trigger for check expression
Q.unitQ (buildInsTrig vn) () False
clearInsInfra :: QualifiedTable -> Q.TxE QErr ()
clearInsInfra vn =
Q.catchE defaultTxErrorHandler $ do
dropView vn
Q.unitQ (dropInsTrigFn vn) () False
type DropInsPerm = DropPerm InsPerm
dropInsPermP2 :: (MonadTx m) => DropInsPerm -> QualifiedTable -> m ()
dropInsPermP2 = dropPermP2
type instance PermInfo InsPerm = InsPermInfo
instance IsPerm InsPerm where
type DropPermP1Res InsPerm = QualifiedTable
permAccessor = PAInsert
buildPermInfo = buildInsPermInfo
addPermP2Setup qt _ = liftTx . buildInsInfra qt
buildDropPermP1Res dp =
ipiView <$> dropPermP1 dp
dropPermP2Setup _ vn =
liftTx $ clearInsInfra vn
-- Select constraint
data SelPerm
= SelPerm
@ -263,29 +186,16 @@ buildSelPermInfo tn fieldInfoMap sp = withPathK "permission" $ do
type SelPermDef = PermDef SelPerm
type CreateSelPerm = CreatePerm SelPerm
type DropSelPerm = DropPerm SelPerm
type instance PermInfo SelPerm = SelPermInfo
dropSelPermP2 :: (MonadTx m) => DropSelPerm -> m ()
dropSelPermP2 dp = dropPermP2 dp ()
instance IsPerm SelPerm where
type DropPermP1Res SelPerm = ()
permAccessor = PASelect
buildPermInfo tn fieldInfoMap (PermDef _ a _) =
buildSelPermInfo tn fieldInfoMap a
buildDropPermP1Res =
void . dropPermP1
addPermP2Setup _ _ _ = return ()
dropPermP2Setup _ _ = return ()
-- Update constraint
data UpdPerm
= UpdPerm
@ -330,27 +240,13 @@ buildUpdPermInfo tn fieldInfoMap (UpdPerm colSpec set fltr) = do
type instance PermInfo UpdPerm = UpdPermInfo
type DropUpdPerm = DropPerm UpdPerm
dropUpdPermP2 :: (MonadTx m) => DropUpdPerm -> m ()
dropUpdPermP2 dp = dropPermP2 dp ()
instance IsPerm UpdPerm where
type DropPermP1Res UpdPerm = ()
permAccessor = PAUpdate
buildPermInfo tn fieldInfoMap (PermDef _ a _) =
buildUpdPermInfo tn fieldInfoMap a
addPermP2Setup _ _ _ = return ()
buildDropPermP1Res =
void . dropPermP1
dropPermP2Setup _ _ = return ()
-- Delete permission
data DelPerm
= DelPerm { dcFilter :: !BoolExp }
@ -374,29 +270,15 @@ buildDelPermInfo tn fieldInfoMap (DelPerm fltr) = do
depHeaders = getDependentHeaders fltr
return (DelPermInfo tn be depHeaders, deps)
type DropDelPerm = DropPerm DelPerm
dropDelPermP2 :: (MonadTx m) => DropDelPerm -> m ()
dropDelPermP2 dp = dropPermP2 dp ()
type instance PermInfo DelPerm = DelPermInfo
instance IsPerm DelPerm where
type DropPermP1Res DelPerm = ()
permAccessor = PADelete
buildPermInfo tn fieldInfoMap (PermDef _ a _) =
buildDelPermInfo tn fieldInfoMap a
addPermP2Setup _ _ _ = return ()
buildDropPermP1Res =
void . dropPermP1
dropPermP2Setup _ _ = return ()
data SetPermComment
= SetPermComment
{ apTable :: !QualifiedTable
@ -443,13 +325,13 @@ setPermCommentTx (SetPermComment (QualifiedObject sn tn) rn pt comment) =
AND perm_type = $5
|] (comment, sn, tn, rn, permTypeToCode pt) True
purgePerm :: (MonadTx m) => QualifiedTable -> RoleName -> PermType -> m ()
purgePerm qt rn pt =
case pt of
PTInsert -> dropInsPermP2 dp $ buildViewName qt rn PTInsert
PTSelect -> dropSelPermP2 dp
PTUpdate -> dropUpdPermP2 dp
PTDelete -> dropDelPermP2 dp
purgePerm :: MonadTx m => QualifiedTable -> RoleName -> PermType -> m ()
purgePerm qt rn pt =
case pt of
PTInsert -> dropPermP2 @InsPerm dp
PTSelect -> dropPermP2 @SelPerm dp
PTUpdate -> dropPermP2 @UpdPerm dp
PTDelete -> dropPermP2 @DelPerm dp
where
dp :: DropPerm a
dp = DropPerm qt rn

View File

@ -239,8 +239,6 @@ type family PermInfo a = r | r -> a
class (ToJSON a) => IsPerm a where
type DropPermP1Res a
permAccessor
:: PermAccessor (PermInfo a)
@ -251,16 +249,6 @@ class (ToJSON a) => IsPerm a where
-> PermDef a
-> m (WithDeps (PermInfo a))
addPermP2Setup
:: (MonadTx m) => QualifiedTable -> PermDef a -> PermInfo a -> m ()
buildDropPermP1Res
:: (QErrM m, CacheRM m, UserInfoM m)
=> DropPerm a
-> m (DropPermP1Res a)
dropPermP2Setup :: (MonadTx m) => DropPerm a -> DropPermP1Res a -> m ()
getPermAcc1
:: PermDef a -> PermAccessor (PermInfo a)
getPermAcc1 _ = permAccessor
@ -268,7 +256,7 @@ class (ToJSON a) => IsPerm a where
getPermAcc2
:: DropPerm a -> PermAccessor (PermInfo a)
getPermAcc2 _ = permAccessor
addPermP2 :: (IsPerm a, MonadTx m, HasSystemDefined m) => QualifiedTable -> PermDef a -> m ()
addPermP2 tn pd = do
let pt = permAccToType $ getPermAcc1 pd
@ -291,9 +279,8 @@ dropPermP1 dp@(DropPerm tn rn) = do
tabInfo <- askTabInfo tn
askPermInfo tabInfo rn $ getPermAcc2 dp
dropPermP2 :: (MonadTx m, IsPerm a) => DropPerm a -> DropPermP1Res a -> m ()
dropPermP2 dp@(DropPerm tn rn) p1Res = do
dropPermP2Setup dp p1Res
dropPermP2 :: forall a m. (MonadTx m, IsPerm a) => DropPerm a -> m ()
dropPermP2 dp@(DropPerm tn rn) = do
liftTx $ dropPermFromCatalog tn rn pt
where
pa = getPermAcc2 dp
@ -303,7 +290,7 @@ runDropPerm
:: (IsPerm a, UserInfoM m, CacheRWM m, MonadTx m)
=> DropPerm a -> m EncJSON
runDropPerm defn = do
permInfo <- buildDropPermP1Res defn
dropPermP2 defn permInfo
dropPermP1 defn
dropPermP2 defn
withNewInconsistentObjsCheck buildSchemaCache
return successMsg

View File

@ -1,34 +0,0 @@
module Hasura.RQL.DDL.Permission.Triggers
( buildInsTrig
, dropInsTrigFn
, buildInsTrigFn
) where
import Hasura.Prelude
import Hasura.SQL.Types
import qualified Database.PG.Query as Q
import qualified Hasura.SQL.DML as S
import qualified Data.Text.Lazy as TL
import qualified Text.Shakespeare.Text as ST
buildInsTrig :: QualifiedTable -> Q.Query
buildInsTrig qt@(QualifiedObject _ tn) =
Q.fromBuilder $ mconcat
[ "CREATE TRIGGER " <> toSQL tn
, " INSTEAD OF INSERT ON " <> toSQL qt
, " FOR EACH ROW EXECUTE PROCEDURE "
, toSQL qt <> "();"
]
dropInsTrigFn :: QualifiedTable -> Q.Query
dropInsTrigFn fn =
Q.fromBuilder $ "DROP FUNCTION " <> toSQL fn <> "()"
buildInsTrigFn :: QualifiedTable -> QualifiedTable -> S.BoolExp -> Q.Query
buildInsTrigFn fn tn be = Q.fromText . TL.toStrict $
let functionName = toSQLTxt fn
tableName = toSQLTxt tn
checkExpression = toSQLTxt be
in $(ST.stextFile "src-rsr/insert_trigger.sql.shakespeare")

View File

@ -25,7 +25,7 @@ import Hasura.SQL.Types
buildTablePermissions
:: ( ArrowChoice arr, Inc.ArrowDistribute arr, Inc.ArrowCache m arr
, ArrowWriter (Seq CollectedInfo) arr, MonadTx m, MonadReader BuildReason m )
, ArrowWriter (Seq CollectedInfo) arr, MonadTx m )
=> ( Inc.Dependency TableCoreCache
, QualifiedTable
, FieldInfoMap FieldInfo
@ -80,8 +80,9 @@ withPermission f = proc (e, (permission, s)) -> do
buildPermission
:: ( ArrowChoice arr, Inc.ArrowCache m arr
, ArrowWriter (Seq CollectedInfo) arr, MonadTx m, MonadReader BuildReason m
, Inc.Cacheable a, IsPerm a, FromJSON a, Inc.Cacheable (PermInfo a) )
, ArrowWriter (Seq CollectedInfo) arr
, MonadTx m, IsPerm a, FromJSON a
)
=> ( Inc.Dependency TableCoreCache
, QualifiedTable
, FieldInfoMap FieldInfo
@ -98,16 +99,6 @@ buildPermission = Inc.cache proc (tableCache, tableName, tableFields, permission
(info, dependencies) <- liftEitherA <<< Inc.bindDepend -< runExceptT $
runTableCoreCacheRT (buildPermInfo tableName tableFields permDef) tableCache
tellA -< Seq.fromList dependencies
rebuildViewsIfNeeded -< (tableName, permDef, info)
returnA -< info)
|) permission) |)
>-> (\info -> join info >- returnA)
rebuildViewsIfNeeded
:: ( Inc.ArrowCache m arr, MonadTx m, MonadReader BuildReason m
, Inc.Cacheable a, IsPerm a, Inc.Cacheable (PermInfo a) )
=> (QualifiedTable, PermDef a, PermInfo a) `arr` ()
rebuildViewsIfNeeded = Inc.cache $ arrM \(tableName, permDef, info) -> do
buildReason <- ask
when (buildReason == CatalogUpdate) $
addPermP2Setup tableName permDef info

View File

@ -56,7 +56,7 @@ mkSQLCount (CountQueryP1 tn (permFltr, mWc) mDistCols) =
, S.selExtr = extrs
}
Nothing -> S.mkSelect
{ S.selExtr = [S.Extractor S.SEStar Nothing] }
{ S.selExtr = [S.Extractor (S.SEStar Nothing) Nothing] }
-- SELECT count(*) FROM (SELECT DISTINCT c1, .. cn FROM .. WHERE ..) r;
-- SELECT count(*) FROM (SELECT * FROM .. WHERE ..) r;

View File

@ -3,7 +3,6 @@ module Hasura.RQL.DML.Insert where
import Data.Aeson.Types
import Instances.TH.Lift ()
import qualified Data.Aeson.Extended as J
import qualified Data.HashMap.Strict as HM
import qualified Data.HashSet as HS
import qualified Data.Sequence as DS
@ -33,22 +32,31 @@ data ConflictClauseP1
data InsertQueryP1
= InsertQueryP1
{ iqp1Table :: !QualifiedTable
, iqp1View :: !QualifiedTable
, iqp1Cols :: ![PGCol]
, iqp1Tuples :: ![[S.SQLExp]]
, iqp1Conflict :: !(Maybe ConflictClauseP1)
, iqp1MutFlds :: !MutFlds
, iqp1AllCols :: ![PGColumnInfo]
{ iqp1Table :: !QualifiedTable
, iqp1Cols :: ![PGCol]
, iqp1Tuples :: ![[S.SQLExp]]
, iqp1Conflict :: !(Maybe ConflictClauseP1)
, iqp1CheckCond :: !(Maybe AnnBoolExpSQL)
, iqp1MutFlds :: !MutFlds
, iqp1AllCols :: ![PGColumnInfo]
} deriving (Show, Eq)
mkInsertCTE :: InsertQueryP1 -> S.CTE
mkInsertCTE (InsertQueryP1 _ vn cols vals c _ _) =
S.CTEInsert insert
mkInsertCTE (InsertQueryP1 tn cols vals c checkCond _ _) =
S.CTEInsert insert
where
tupVals = S.ValuesExp $ map S.TupleExp vals
insert =
S.SQLInsert vn cols tupVals (toSQLConflict <$> c) $ Just S.returningStar
S.SQLInsert tn cols tupVals (toSQLConflict <$> c)
. Just
. S.RetExp
$ maybe
[S.selectStar]
(\e ->
[ S.selectStar
, insertCheckExpr (toSQLBoolExp (S.QualTable tn) e)
])
checkCond
toSQLConflict :: ConflictClauseP1 -> S.SQLConflict
toSQLConflict conflict = case conflict of
@ -199,7 +207,6 @@ convInsertQuery objsParser sessVarBldr prepFn (InsertQuery tableName val oC mRet
map pgiColumn $ getCols fieldInfoMap
allCols = getCols fieldInfoMap
insCols = HM.keys defInsVals
insView = ipiView insPerm
resolvedPreSet <- mapM (convPartialSQLExp sessVarBldr) setInsVals
@ -208,16 +215,17 @@ convInsertQuery objsParser sessVarBldr prepFn (InsertQuery tableName val oC mRet
let sqlExps = map snd insTuples
inpCols = HS.toList $ HS.fromList $ concatMap fst insTuples
checkExpr <- convAnnBoolExpPartialSQL sessVarFromCurrentSetting (ipiCheck insPerm)
conflictClause <- withPathK "on_conflict" $ forM oC $ \c -> do
roleName <- askCurRole
unless (isTabUpdatable roleName tableInfo) $ throw400 PermissionDenied $
"upsert is not allowed for role " <> roleName
<<> " since update permissions are not defined"
buildConflictClause sessVarBldr tableInfo inpCols c
return $ InsertQueryP1 tableName insView insCols sqlExps
conflictClause mutFlds allCols
return $ InsertQueryP1 tableName insCols sqlExps
conflictClause (Just checkExpr) mutFlds allCols
where
selNecessaryMsg =
"; \"returning\" can only be used if the role has "
@ -241,53 +249,38 @@ convInsQ =
insertP2 :: Bool -> (InsertQueryP1, DS.Seq Q.PrepArg) -> Q.TxE QErr EncJSON
insertP2 strfyNum (u, p) =
runMutation $ Mutation (iqp1Table u) (insertCTE, p)
runMutation
$ Mutation (iqp1Table u) (insertCTE, p)
(iqp1MutFlds u) (iqp1AllCols u) strfyNum
where
insertCTE = mkInsertCTE u
data ConflictCtx
= CCUpdate !ConstraintName ![PGCol] !PreSetCols !S.BoolExp
| CCDoNothing !(Maybe ConstraintName)
deriving (Show, Eq)
nonAdminInsert :: Bool -> (InsertQueryP1, DS.Seq Q.PrepArg) -> Q.TxE QErr EncJSON
nonAdminInsert strfyNum (insQueryP1, args) = do
conflictCtxM <- mapM extractConflictCtx conflictClauseP1
setConflictCtx conflictCtxM
insertP2 strfyNum (withoutConflictClause, args)
where
withoutConflictClause = insQueryP1{iqp1Conflict=Nothing}
conflictClauseP1 = iqp1Conflict insQueryP1
extractConflictCtx :: (MonadError QErr m) => ConflictClauseP1 -> m ConflictCtx
extractConflictCtx cp =
case cp of
(CP1DoNothing mConflictTar) -> do
mConstraintName <- mapM extractConstraintName mConflictTar
return $ CCDoNothing mConstraintName
(CP1Update conflictTar inpCols preSet filtr) -> do
constraintName <- extractConstraintName conflictTar
return $ CCUpdate constraintName inpCols preSet filtr
where
extractConstraintName (CTConstraint cn) = return cn
extractConstraintName _ = throw400 NotSupported
"\"constraint_on\" not supported for non admin insert. use \"constraint\" instead"
setConflictCtx :: Maybe ConflictCtx -> Q.TxE QErr ()
setConflictCtx conflictCtxM = do
let t = maybe "null" conflictCtxToJSON conflictCtxM
setVal = toSQL $ S.SELit t
setVar = "SET LOCAL hasura.conflict_clause = "
q = Q.fromBuilder $ setVar <> setVal
Q.unitQE defaultTxErrorHandler q () False
where
conflictCtxToJSON (CCDoNothing constrM) =
J.encodeToStrictText $ InsertTxConflictCtx CAIgnore constrM Nothing
conflictCtxToJSON (CCUpdate constr updCols preSet filtr) =
J.encodeToStrictText $ InsertTxConflictCtx CAUpdate (Just constr) $
Just $ toSQLTxt (S.buildUpsertSetExp updCols preSet)
<> " " <> toSQLTxt (S.WhereFrag filtr)
-- | Create an expression which will fail with a check constraint violation error
-- if the condition is not met on any of the inserted rows.
--
-- The resulting SQL will look something like this:
--
-- > INSERT INTO
-- > ...
-- > RETURNING
-- > *,
-- > CASE WHEN {cond}
-- > THEN NULL
-- > ELSE hdb_catalog.check_violation('insert check constraint failed')
-- > END
insertCheckExpr
:: S.BoolExp
-> S.Extractor
insertCheckExpr condExpr =
S.Extractor
(S.SECond condExpr S.SENull
(S.SEFunction
(S.FunctionExp
(QualifiedObject (SchemaName "hdb_catalog") (FunctionName "check_violation"))
(S.FunctionArgs [S.SELit "insert check constraint failed"] mempty)
Nothing)
))
Nothing
runInsert
:: (QErrM m, UserInfoM m, CacheRM m, MonadTx m, HasSQLGenCtx m)
@ -295,6 +288,5 @@ runInsert
-> m EncJSON
runInsert q = do
res <- convInsQ q
role <- userRole <$> askUserInfo
strfyNum <- stringifyNum <$> askSQLGenCtx
liftTx $ bool (nonAdminInsert strfyNum res) (insertP2 strfyNum res) $ isAdmin role
liftTx $ insertP2 strfyNum res

View File

@ -38,7 +38,7 @@ mkAdminRolePermInfo ti =
getComputedFieldInfos fields
tn = _tciName ti
i = InsPermInfo (HS.fromList pgCols) tn annBoolExpTrue M.empty []
i = InsPermInfo (HS.fromList pgCols) annBoolExpTrue M.empty []
s = SelPermInfo (HS.fromList pgCols) (HS.fromList scalarComputedFields) tn annBoolExpTrue
Nothing True []
u = UpdPermInfo (HS.fromList pgCols) tn annBoolExpTrue M.empty []

View File

@ -705,7 +705,7 @@ baseNodeToSel joinCond baseNode =
= baseNode
-- this is the table which is aliased as "pfx.base"
baseSel = S.mkSelect
{ S.selExtr = [S.Extractor S.SEStar Nothing]
{ S.selExtr = [S.Extractor (S.SEStar Nothing) Nothing]
, S.selFrom = Just $ S.FromExp [fromItem]
, S.selWhere = Just $ injectJoinCond joinCond whr
}

View File

@ -208,7 +208,6 @@ isPGColInfo _ = False
data InsPermInfo
= InsPermInfo
{ ipiCols :: !(HS.HashSet PGCol)
, ipiView :: !QualifiedTable
, ipiCheck :: !AnnBoolExpPartialSQL
, ipiSet :: !PreSetColsPartial
, ipiRequiredHeaders :: ![T.Text]

View File

@ -283,7 +283,8 @@ data SQLExp
| SELit !T.Text
| SEUnsafe !T.Text
| SESelect !Select
| SEStar
| SEStar !(Maybe Qual)
-- ^ all fields (@*@) or all fields from relation (@iden.*@)
| SEIden !Iden
-- iden and row identifier are distinguished for easier rewrite rules
| SERowIden !Iden
@ -336,8 +337,10 @@ instance ToSQL SQLExp where
TB.text t
toSQL (SESelect se) =
paren $ toSQL se
toSQL SEStar =
toSQL (SEStar Nothing) =
TB.char '*'
toSQL (SEStar (Just qual)) =
mconcat [toSQL qual, TB.char '.', TB.char '*']
toSQL (SEIden iden) =
toSQL iden
toSQL (SERowIden iden) =
@ -725,7 +728,10 @@ newtype RetExp = RetExp [Extractor]
deriving (Show, Eq)
selectStar :: Extractor
selectStar = Extractor SEStar Nothing
selectStar = Extractor (SEStar Nothing) Nothing
selectStar' :: Qual -> Extractor
selectStar' q = Extractor (SEStar (Just q)) Nothing
returningStar :: RetExp
returningStar = RetExp [selectStar]
@ -804,15 +810,14 @@ data SQLInsert = SQLInsert
instance ToSQL SQLInsert where
toSQL si =
let insConflict = maybe "" toSQL
in "INSERT INTO"
<-> toSQL (siTable si)
<-> "("
<-> (", " <+> siCols si)
<-> ")"
<-> toSQL (siValues si)
<-> insConflict (siConflict si)
<-> toSQL (siRet si)
"INSERT INTO"
<-> toSQL (siTable si)
<-> "("
<-> (", " <+> siCols si)
<-> ")"
<-> toSQL (siValues si)
<-> maybe "" toSQL (siConflict si)
<-> toSQL (siRet si)
data CTE
= CTESelect !Select

View File

@ -161,7 +161,7 @@ uSqlExp = restoringIdens . \case
S.SELit t -> return $ S.SELit t
S.SEUnsafe t -> return $ S.SEUnsafe t
S.SESelect s -> S.SESelect <$> uSelect s
S.SEStar -> return S.SEStar
S.SEStar qual -> S.SEStar <$> traverse uQual qual
-- this is for row expressions
-- todo: check if this is always okay
S.SEIden iden -> return $ S.SEIden iden

View File

@ -40,7 +40,6 @@ module Hasura.SQL.Types
, SchemaName(..)
, publicSchema
, hdbViewsSchema
, TableName(..)
, FunctionName(..)
@ -239,9 +238,6 @@ newtype SchemaName
publicSchema :: SchemaName
publicSchema = SchemaName "public"
hdbViewsSchema :: SchemaName
hdbViewsSchema = SchemaName "hdb_views"
instance IsIden SchemaName where
toIden (SchemaName t) = Iden t

View File

@ -12,7 +12,7 @@ import Hasura.Prelude
import qualified Data.Text as T
latestCatalogVersion :: Integer
latestCatalogVersion = 29
latestCatalogVersion = 30
latestCatalogVersionString :: T.Text
latestCatalogVersionString = T.pack $ show latestCatalogVersion

View File

@ -61,10 +61,10 @@ data RQLQueryV1
| RQCreateUpdatePermission !CreateUpdPerm
| RQCreateDeletePermission !CreateDelPerm
| RQDropInsertPermission !DropInsPerm
| RQDropSelectPermission !DropSelPerm
| RQDropUpdatePermission !DropUpdPerm
| RQDropDeletePermission !DropDelPerm
| RQDropInsertPermission !(DropPerm InsPerm)
| RQDropSelectPermission !(DropPerm SelPerm)
| RQDropUpdatePermission !(DropPerm UpdPerm)
| RQDropDeletePermission !(DropPerm DelPerm)
| RQSetPermissionComment !SetPermComment
| RQGetInconsistentMetadata !GetInconsistentMetadata

View File

@ -656,3 +656,10 @@ CREATE VIEW hdb_catalog.hdb_computed_field_function AS
END AS function_schema
FROM hdb_catalog.hdb_computed_field
);
CREATE OR REPLACE FUNCTION hdb_catalog.check_violation(msg text) RETURNS bool AS
$$
BEGIN
RAISE check_violation USING message=msg;
END;
$$ LANGUAGE plpgsql;

View File

@ -1,34 +0,0 @@
CREATE OR REPLACE FUNCTION #{functionName}() RETURNS trigger LANGUAGE plpgsql AS $$
DECLARE r #{tableName}%ROWTYPE;
DECLARE conflict_clause jsonb;
DECLARE action text;
DECLARE constraint_name text;
DECLARE set_expression text;
BEGIN
conflict_clause = current_setting('hasura.conflict_clause')::jsonb;
IF (#{checkExpression}) THEN
CASE
WHEN conflict_clause = 'null'::jsonb THEN INSERT INTO #{tableName} VALUES (NEW.*) RETURNING * INTO r;
ELSE
action = conflict_clause ->> 'action';
constraint_name = quote_ident(conflict_clause ->> 'constraint');
set_expression = conflict_clause ->> 'set_expression';
IF action is NOT NULL THEN
CASE
WHEN action = 'ignore'::text AND constraint_name IS NULL THEN
INSERT INTO #{tableName} VALUES (NEW.*) ON CONFLICT DO NOTHING RETURNING * INTO r;
WHEN action = 'ignore'::text AND constraint_name is NOT NULL THEN
EXECUTE 'INSERT INTO #{tableName} VALUES ($1.*) ON CONFLICT ON CONSTRAINT ' || constraint_name ||
' DO NOTHING RETURNING *' INTO r USING NEW;
ELSE
EXECUTE 'INSERT INTO #{tableName} VALUES ($1.*) ON CONFLICT ON CONSTRAINT ' || constraint_name ||
' DO UPDATE ' || set_expression || ' RETURNING *' INTO r USING NEW;
END CASE;
ELSE
RAISE internal_error using message = 'action is not found'; RETURN NULL;
END IF;
END CASE;
IF r IS NULL THEN RETURN null; ELSE RETURN r; END IF;
ELSE RAISE check_violation using message = 'insert check constraint failed'; RETURN NULL;
END IF;
END $$;

View File

@ -128,4 +128,4 @@ CREATE VIEW hdb_catalog.hdb_table_info_agg AS
) foreign_key_constraints ON true
-- all these identify table-like things
WHERE "table".relkind IN ('r', 't', 'v', 'm', 'f', 'p');
WHERE "table".relkind IN ('r', 't', 'v', 'm', 'f', 'p');

View File

@ -0,0 +1,6 @@
CREATE OR REPLACE FUNCTION hdb_catalog.check_violation(msg text) RETURNS bool AS
$$
BEGIN
RAISE check_violation USING message=msg;
END;
$$ LANGUAGE plpgsql;