2019-03-01 12:17:22 +03:00
|
|
|
module Hasura.RQL.DDL.Relationship
|
2019-03-25 11:56:29 +03:00
|
|
|
( validateObjRel
|
|
|
|
, objRelP2Setup
|
2019-03-01 12:17:22 +03:00
|
|
|
, objRelP2
|
2019-03-25 11:56:29 +03:00
|
|
|
, validateArrRel
|
2019-03-01 12:17:22 +03:00
|
|
|
, arrRelP2Setup
|
|
|
|
, arrRelP2
|
|
|
|
, delRelFromCatalog
|
|
|
|
, validateRelP1
|
|
|
|
, runCreateObjRel
|
|
|
|
, runCreateArrRel
|
|
|
|
, runDropRel
|
|
|
|
, runSetRelComment
|
|
|
|
, module Hasura.RQL.DDL.Relationship.Types
|
|
|
|
)
|
|
|
|
where
|
|
|
|
|
|
|
|
import qualified Database.PG.Query as Q
|
2019-03-18 19:22:21 +03:00
|
|
|
|
|
|
|
import Hasura.EncJSON
|
2018-11-23 16:02:46 +03:00
|
|
|
import Hasura.Prelude
|
2018-06-27 16:11:32 +03:00
|
|
|
import Hasura.RQL.DDL.Deps
|
2019-03-01 12:17:22 +03:00
|
|
|
import Hasura.RQL.DDL.Permission (purgePerm)
|
|
|
|
import Hasura.RQL.DDL.Relationship.Types
|
2018-06-27 16:11:32 +03:00
|
|
|
import Hasura.RQL.Types
|
|
|
|
import Hasura.SQL.Types
|
|
|
|
|
|
|
|
import Data.Aeson.Types
|
2019-03-01 12:17:22 +03:00
|
|
|
import qualified Data.HashMap.Strict as HM
|
|
|
|
import qualified Data.Map.Strict as M
|
|
|
|
import qualified Data.Text as T
|
|
|
|
import Data.Tuple (swap)
|
|
|
|
import Instances.TH.Lift ()
|
2018-06-27 16:11:32 +03:00
|
|
|
|
|
|
|
validateManualConfig
|
|
|
|
:: (QErrM m, CacheRM m)
|
|
|
|
=> FieldInfoMap
|
|
|
|
-> RelManualConfig
|
|
|
|
-> m ()
|
|
|
|
validateManualConfig fim rm = do
|
|
|
|
let colMapping = M.toList $ rmColumns rm
|
|
|
|
remoteQt = rmTable rm
|
|
|
|
remoteTabInfo <- askTabInfo remoteQt
|
|
|
|
let remoteFim = tiFieldInfoMap remoteTabInfo
|
|
|
|
forM_ colMapping $ \(lCol, rCol) -> do
|
|
|
|
assertPGCol fim "" lCol
|
|
|
|
assertPGCol remoteFim "" rCol
|
|
|
|
-- lColType <- askPGType fim lCol ""
|
|
|
|
-- rColType <- askPGType remoteFim rCol ""
|
|
|
|
-- when (lColType /= rColType) $
|
|
|
|
-- throw400 $ mconcat
|
|
|
|
-- [ "the types of columns " <> lCol <<> ", " <>> rCol
|
|
|
|
-- , " do not match"
|
|
|
|
-- ]
|
|
|
|
|
|
|
|
persistRel :: QualifiedTable
|
|
|
|
-> RelName
|
|
|
|
-> RelType
|
|
|
|
-> Value
|
|
|
|
-> Maybe T.Text
|
|
|
|
-> Q.TxE QErr ()
|
2019-01-25 06:31:54 +03:00
|
|
|
persistRel (QualifiedObject sn tn) rn relType relDef comment =
|
2018-06-27 16:11:32 +03:00
|
|
|
Q.unitQE defaultTxErrorHandler [Q.sql|
|
|
|
|
INSERT INTO
|
|
|
|
hdb_catalog.hdb_relationship
|
|
|
|
(table_schema, table_name, rel_name, rel_type, rel_def, comment)
|
|
|
|
VALUES ($1, $2, $3, $4, $5 :: jsonb, $6)
|
|
|
|
|] (sn, tn, rn, relTypeToTxt relType, Q.AltJ relDef, comment) True
|
|
|
|
|
|
|
|
checkForColConfilct
|
|
|
|
:: (MonadError QErr m)
|
|
|
|
=> TableInfo
|
|
|
|
-> FieldName
|
|
|
|
-> m ()
|
|
|
|
checkForColConfilct tabInfo f =
|
|
|
|
case HM.lookup f (tiFieldInfoMap tabInfo) of
|
|
|
|
Just _ -> throw400 AlreadyExists $ mconcat
|
|
|
|
[ "column/relationship " <>> f
|
|
|
|
, " of table " <>> tiName tabInfo
|
|
|
|
, " already exists"
|
|
|
|
]
|
|
|
|
Nothing -> return ()
|
|
|
|
|
2019-03-25 11:56:29 +03:00
|
|
|
validateObjRel
|
2018-06-27 16:11:32 +03:00
|
|
|
:: (QErrM m, CacheRM m)
|
2019-03-25 11:56:29 +03:00
|
|
|
=> QualifiedTable
|
2018-06-27 16:11:32 +03:00
|
|
|
-> ObjRelDef
|
|
|
|
-> m ()
|
2019-03-25 11:56:29 +03:00
|
|
|
validateObjRel qt (RelDef rn ru _) = do
|
|
|
|
tabInfo <- askTabInfo qt
|
2018-06-27 16:11:32 +03:00
|
|
|
checkForColConfilct tabInfo (fromRel rn)
|
|
|
|
let fim = tiFieldInfoMap tabInfo
|
|
|
|
case ru of
|
|
|
|
RUFKeyOn cn -> assertPGCol fim "" cn
|
|
|
|
RUManual (ObjRelManualConfig rm) -> validateManualConfig fim rm
|
|
|
|
|
|
|
|
createObjRelP1
|
2018-12-13 10:26:15 +03:00
|
|
|
:: (UserInfoM m, QErrM m, CacheRM m)
|
2018-06-27 16:11:32 +03:00
|
|
|
=> CreateObjRel
|
|
|
|
-> m ()
|
|
|
|
createObjRelP1 (WithTable qt rd) = do
|
|
|
|
adminOnly
|
2019-03-25 11:56:29 +03:00
|
|
|
validateObjRel qt rd
|
2018-06-27 16:11:32 +03:00
|
|
|
|
2018-11-23 16:02:46 +03:00
|
|
|
objRelP2Setup
|
|
|
|
:: (QErrM m, CacheRWM m, MonadTx m)
|
|
|
|
=> QualifiedTable -> RelDef ObjRelUsing -> m ()
|
2018-06-27 16:11:32 +03:00
|
|
|
objRelP2Setup qt (RelDef rn ru _) = do
|
2018-11-16 15:40:23 +03:00
|
|
|
(relInfo, deps) <- case ru of
|
2018-06-27 16:11:32 +03:00
|
|
|
RUManual (ObjRelManualConfig rm) -> do
|
|
|
|
let refqt = rmTable rm
|
|
|
|
(lCols, rCols) = unzip $ M.toList $ rmColumns rm
|
|
|
|
deps = map (\c -> SchemaDependency (SOTableObj qt $ TOCol c) "lcol") lCols
|
|
|
|
<> map (\c -> SchemaDependency (SOTableObj refqt $ TOCol c) "rcol") rCols
|
2018-11-16 15:40:23 +03:00
|
|
|
return (RelInfo rn ObjRel (zip lCols rCols) refqt True, deps)
|
2018-06-27 16:11:32 +03:00
|
|
|
RUFKeyOn cn -> do
|
2019-03-25 11:56:29 +03:00
|
|
|
-- TODO: validation should account for this too
|
2018-06-27 16:11:32 +03:00
|
|
|
res <- liftTx $ Q.catchE defaultTxErrorHandler $ fetchFKeyDetail cn
|
|
|
|
case mapMaybe processRes res of
|
|
|
|
[] -> throw400 ConstraintError
|
|
|
|
"no foreign constraint exists on the given column"
|
|
|
|
[(consName, refsn, reftn, colMapping)] -> do
|
|
|
|
let deps = [ SchemaDependency (SOTableObj qt $ TOCons consName) "fkey"
|
|
|
|
, SchemaDependency (SOTableObj qt $ TOCol cn) "using_col"
|
2019-01-24 16:26:13 +03:00
|
|
|
-- this needs to be added explicitly to handle the remote table
|
|
|
|
-- being untracked. In this case, neither the using_col nor
|
|
|
|
-- the constraint name will help.
|
|
|
|
, SchemaDependency (SOTable refqt) "remote_table"
|
2018-06-27 16:11:32 +03:00
|
|
|
]
|
2019-01-25 06:31:54 +03:00
|
|
|
refqt = QualifiedObject refsn reftn
|
2018-08-03 12:34:37 +03:00
|
|
|
void $ askTabInfo refqt
|
2018-11-16 15:40:23 +03:00
|
|
|
return (RelInfo rn ObjRel colMapping refqt False, deps)
|
2018-06-27 16:11:32 +03:00
|
|
|
_ -> throw400 ConstraintError
|
|
|
|
"more than one foreign key constraint exists on the given column"
|
2018-11-16 15:40:23 +03:00
|
|
|
addRelToCache rn relInfo deps qt
|
2018-06-27 16:11:32 +03:00
|
|
|
where
|
2019-01-25 06:31:54 +03:00
|
|
|
QualifiedObject sn tn = qt
|
2018-06-27 16:11:32 +03:00
|
|
|
fetchFKeyDetail cn =
|
|
|
|
Q.listQ [Q.sql|
|
|
|
|
SELECT constraint_name, ref_table_table_schema, ref_table, column_mapping
|
|
|
|
FROM hdb_catalog.hdb_foreign_key_constraint
|
|
|
|
WHERE table_schema = $1
|
|
|
|
AND table_name = $2
|
2018-07-17 19:56:47 +03:00
|
|
|
AND (column_mapping ->> $3) IS NOT NULL
|
2018-06-27 16:11:32 +03:00
|
|
|
|] (sn, tn, cn) False
|
|
|
|
processRes (consn, refsn, reftn, mapping) =
|
|
|
|
case M.toList (Q.getAltJ mapping) of
|
|
|
|
m@[_] -> Just (consn, refsn, reftn, m)
|
|
|
|
_ -> Nothing
|
|
|
|
|
2018-11-23 16:02:46 +03:00
|
|
|
objRelP2
|
|
|
|
:: ( QErrM m
|
|
|
|
, CacheRWM m
|
|
|
|
, MonadTx m
|
|
|
|
)
|
|
|
|
=> QualifiedTable
|
|
|
|
-> ObjRelDef
|
|
|
|
-> m ()
|
2018-06-27 16:11:32 +03:00
|
|
|
objRelP2 qt rd@(RelDef rn ru comment) = do
|
|
|
|
objRelP2Setup qt rd
|
|
|
|
liftTx $ persistRel qt rn ObjRel (toJSON ru) comment
|
|
|
|
|
2018-12-13 10:26:15 +03:00
|
|
|
createObjRelP2
|
2019-03-18 19:22:21 +03:00
|
|
|
:: (QErrM m, CacheRWM m, MonadTx m) => CreateObjRel -> m EncJSON
|
2018-06-27 16:11:32 +03:00
|
|
|
createObjRelP2 (WithTable qt rd) = do
|
|
|
|
objRelP2 qt rd
|
|
|
|
return successMsg
|
|
|
|
|
2018-12-13 10:26:15 +03:00
|
|
|
runCreateObjRel
|
|
|
|
:: (QErrM m, CacheRWM m, MonadTx m , UserInfoM m)
|
2019-03-18 19:22:21 +03:00
|
|
|
=> CreateObjRel -> m EncJSON
|
2018-12-13 10:26:15 +03:00
|
|
|
runCreateObjRel defn = do
|
|
|
|
createObjRelP1 defn
|
|
|
|
createObjRelP2 defn
|
2018-06-27 16:11:32 +03:00
|
|
|
|
2018-12-13 10:26:15 +03:00
|
|
|
createArrRelP1 :: (UserInfoM m, QErrM m, CacheRM m) => CreateArrRel -> m ()
|
2018-06-27 16:11:32 +03:00
|
|
|
createArrRelP1 (WithTable qt rd) = do
|
|
|
|
adminOnly
|
2019-03-25 11:56:29 +03:00
|
|
|
validateArrRel qt rd
|
2018-06-27 16:11:32 +03:00
|
|
|
|
2019-03-25 11:56:29 +03:00
|
|
|
validateArrRel
|
2018-06-27 16:11:32 +03:00
|
|
|
:: (QErrM m, CacheRM m)
|
2019-03-25 11:56:29 +03:00
|
|
|
=> QualifiedTable -> ArrRelDef -> m ()
|
|
|
|
validateArrRel qt (RelDef rn ru _) = do
|
|
|
|
tabInfo <- askTabInfo qt
|
2018-06-27 16:11:32 +03:00
|
|
|
checkForColConfilct tabInfo (fromRel rn)
|
|
|
|
let fim = tiFieldInfoMap tabInfo
|
|
|
|
case ru of
|
|
|
|
RUFKeyOn (ArrRelUsingFKeyOn remoteQt rcn) -> do
|
|
|
|
remoteTabInfo <- askTabInfo remoteQt
|
|
|
|
let rfim = tiFieldInfoMap remoteTabInfo
|
|
|
|
-- Check if 'using' column exists
|
|
|
|
assertPGCol rfim "" rcn
|
|
|
|
RUManual (ArrRelManualConfig rm) ->
|
|
|
|
validateManualConfig fim rm
|
|
|
|
|
2018-11-23 16:02:46 +03:00
|
|
|
arrRelP2Setup
|
|
|
|
:: (QErrM m, CacheRWM m, MonadTx m)
|
|
|
|
=> QualifiedTable -> ArrRelDef -> m ()
|
2018-06-27 16:11:32 +03:00
|
|
|
arrRelP2Setup qt (RelDef rn ru _) = do
|
2018-11-16 15:40:23 +03:00
|
|
|
(relInfo, deps) <- case ru of
|
2018-06-27 16:11:32 +03:00
|
|
|
RUManual (ArrRelManualConfig rm) -> do
|
|
|
|
let refqt = rmTable rm
|
|
|
|
(lCols, rCols) = unzip $ M.toList $ rmColumns rm
|
|
|
|
deps = map (\c -> SchemaDependency (SOTableObj qt $ TOCol c) "lcol") lCols
|
|
|
|
<> map (\c -> SchemaDependency (SOTableObj refqt $ TOCol c) "rcol") rCols
|
2018-11-16 15:40:23 +03:00
|
|
|
return (RelInfo rn ArrRel (zip lCols rCols) refqt True, deps)
|
2018-06-27 16:11:32 +03:00
|
|
|
RUFKeyOn (ArrRelUsingFKeyOn refqt refCol) -> do
|
2019-01-25 06:31:54 +03:00
|
|
|
let QualifiedObject refSn refTn = refqt
|
2019-03-25 11:56:29 +03:00
|
|
|
-- TODO: validation should account for this too
|
2018-06-27 16:11:32 +03:00
|
|
|
res <- liftTx $ Q.catchE defaultTxErrorHandler $
|
|
|
|
fetchFKeyDetail refSn refTn refCol
|
|
|
|
case mapMaybe processRes res of
|
|
|
|
[] -> throw400 ConstraintError
|
|
|
|
"no foreign constraint exists on the given column"
|
|
|
|
[(consName, mapping)] -> do
|
|
|
|
let deps = [ SchemaDependency (SOTableObj refqt $ TOCons consName) "remote_fkey"
|
|
|
|
, SchemaDependency (SOTableObj refqt $ TOCol refCol) "using_col"
|
2019-01-24 16:26:13 +03:00
|
|
|
-- we don't need to necessarily track the remote table like we did in
|
|
|
|
-- case of obj relationships as the remote table is indirectly
|
|
|
|
-- tracked by tracking the constraint name and 'using_col'
|
|
|
|
, SchemaDependency (SOTable refqt) "remote_table"
|
2018-06-27 16:11:32 +03:00
|
|
|
]
|
2018-11-16 15:40:23 +03:00
|
|
|
return (RelInfo rn ArrRel (map swap mapping) refqt False, deps)
|
2018-06-27 16:11:32 +03:00
|
|
|
_ -> throw400 ConstraintError
|
|
|
|
"more than one foreign key constraint exists on the given column"
|
2018-11-16 15:40:23 +03:00
|
|
|
addRelToCache rn relInfo deps qt
|
2018-06-27 16:11:32 +03:00
|
|
|
where
|
2019-01-25 06:31:54 +03:00
|
|
|
QualifiedObject sn tn = qt
|
2018-06-27 16:11:32 +03:00
|
|
|
fetchFKeyDetail refsn reftn refcn = Q.listQ [Q.sql|
|
|
|
|
SELECT constraint_name, column_mapping
|
|
|
|
FROM hdb_catalog.hdb_foreign_key_constraint
|
|
|
|
WHERE table_schema = $1
|
|
|
|
AND table_name = $2
|
2018-07-17 19:56:47 +03:00
|
|
|
AND (column_mapping -> $3) IS NOT NULL
|
2018-06-27 16:11:32 +03:00
|
|
|
AND ref_table_table_schema = $4
|
|
|
|
AND ref_table = $5
|
|
|
|
|] (refsn, reftn, refcn, sn, tn) False
|
|
|
|
processRes (consn, mapping) =
|
|
|
|
case M.toList (Q.getAltJ mapping) of
|
|
|
|
m@[_] -> Just (consn, m)
|
|
|
|
_ -> Nothing
|
|
|
|
|
2018-11-23 16:02:46 +03:00
|
|
|
arrRelP2
|
|
|
|
:: (QErrM m, CacheRWM m, MonadTx m)
|
|
|
|
=> QualifiedTable -> ArrRelDef -> m ()
|
2018-06-27 16:11:32 +03:00
|
|
|
arrRelP2 qt rd@(RelDef rn u comment) = do
|
|
|
|
arrRelP2Setup qt rd
|
|
|
|
liftTx $ persistRel qt rn ArrRel (toJSON u) comment
|
|
|
|
|
2018-12-13 10:26:15 +03:00
|
|
|
createArrRelP2
|
2019-03-18 19:22:21 +03:00
|
|
|
:: (QErrM m, CacheRWM m, MonadTx m) => CreateArrRel -> m EncJSON
|
2018-06-27 16:11:32 +03:00
|
|
|
createArrRelP2 (WithTable qt rd) = do
|
|
|
|
arrRelP2 qt rd
|
|
|
|
return successMsg
|
|
|
|
|
2018-12-13 10:26:15 +03:00
|
|
|
runCreateArrRel
|
|
|
|
:: (QErrM m, CacheRWM m, MonadTx m , UserInfoM m)
|
2019-03-18 19:22:21 +03:00
|
|
|
=> CreateArrRel -> m EncJSON
|
2018-12-13 10:26:15 +03:00
|
|
|
runCreateArrRel defn = do
|
|
|
|
createArrRelP1 defn
|
|
|
|
createArrRelP2 defn
|
2018-06-27 16:11:32 +03:00
|
|
|
|
2018-12-13 10:26:15 +03:00
|
|
|
dropRelP1 :: (UserInfoM m, QErrM m, CacheRM m) => DropRel -> m [SchemaObjId]
|
2018-06-27 16:11:32 +03:00
|
|
|
dropRelP1 (DropRel qt rn cascade) = do
|
|
|
|
adminOnly
|
|
|
|
tabInfo <- askTabInfo qt
|
|
|
|
_ <- askRelType (tiFieldInfoMap tabInfo) rn ""
|
|
|
|
sc <- askSchemaCache
|
|
|
|
let depObjs = getDependentObjs sc relObjId
|
|
|
|
when (depObjs /= [] && not (or cascade)) $ reportDeps depObjs
|
|
|
|
return depObjs
|
|
|
|
where
|
|
|
|
relObjId = SOTableObj qt $ TORel rn
|
|
|
|
|
2018-12-13 10:26:15 +03:00
|
|
|
purgeRelDep
|
|
|
|
:: (CacheRWM m, MonadTx m) => SchemaObjId -> m ()
|
2018-06-27 16:11:32 +03:00
|
|
|
purgeRelDep (SOTableObj tn (TOPerm rn pt)) =
|
|
|
|
purgePerm tn rn pt
|
|
|
|
purgeRelDep d = throw500 $ "unexpected dependency of relationship : "
|
|
|
|
<> reportSchemaObj d
|
|
|
|
|
2018-12-13 10:26:15 +03:00
|
|
|
dropRelP2
|
|
|
|
:: (QErrM m, CacheRWM m, MonadTx m)
|
2019-03-18 19:22:21 +03:00
|
|
|
=> DropRel -> [SchemaObjId] -> m EncJSON
|
2018-06-27 16:11:32 +03:00
|
|
|
dropRelP2 (DropRel qt rn _) depObjs = do
|
|
|
|
mapM_ purgeRelDep depObjs
|
2018-11-16 15:40:23 +03:00
|
|
|
delRelFromCache rn qt
|
2018-06-27 16:11:32 +03:00
|
|
|
liftTx $ delRelFromCatalog qt rn
|
|
|
|
return successMsg
|
|
|
|
|
2018-12-13 10:26:15 +03:00
|
|
|
runDropRel
|
|
|
|
:: (QErrM m, CacheRWM m, MonadTx m , UserInfoM m)
|
2019-03-18 19:22:21 +03:00
|
|
|
=> DropRel -> m EncJSON
|
2018-12-13 10:26:15 +03:00
|
|
|
runDropRel defn = do
|
|
|
|
depObjs <- dropRelP1 defn
|
|
|
|
dropRelP2 defn depObjs
|
|
|
|
|
|
|
|
delRelFromCatalog
|
|
|
|
:: QualifiedTable
|
|
|
|
-> RelName
|
|
|
|
-> Q.TxE QErr ()
|
2019-01-25 06:31:54 +03:00
|
|
|
delRelFromCatalog (QualifiedObject sn tn) rn =
|
2018-06-27 16:11:32 +03:00
|
|
|
Q.unitQE defaultTxErrorHandler [Q.sql|
|
|
|
|
DELETE FROM
|
|
|
|
hdb_catalog.hdb_relationship
|
|
|
|
WHERE table_schema = $1
|
|
|
|
AND table_name = $2
|
|
|
|
AND rel_name = $3
|
|
|
|
|] (sn, tn, rn) True
|
|
|
|
|
2019-03-01 12:17:22 +03:00
|
|
|
validateRelP1
|
|
|
|
:: (UserInfoM m, QErrM m, CacheRM m)
|
|
|
|
=> QualifiedTable -> RelName -> m RelInfo
|
|
|
|
validateRelP1 qt rn = do
|
2018-06-27 16:11:32 +03:00
|
|
|
adminOnly
|
|
|
|
tabInfo <- askTabInfo qt
|
2019-03-01 12:17:22 +03:00
|
|
|
askRelType (tiFieldInfoMap tabInfo) rn ""
|
2018-06-27 16:11:32 +03:00
|
|
|
|
2018-12-13 10:26:15 +03:00
|
|
|
setRelCommentP2
|
|
|
|
:: (QErrM m, MonadTx m)
|
2019-03-18 19:22:21 +03:00
|
|
|
=> SetRelComment -> m EncJSON
|
2018-06-27 16:11:32 +03:00
|
|
|
setRelCommentP2 arc = do
|
|
|
|
liftTx $ setRelComment arc
|
|
|
|
return successMsg
|
|
|
|
|
2018-12-13 10:26:15 +03:00
|
|
|
runSetRelComment
|
|
|
|
:: (QErrM m, CacheRWM m, MonadTx m , UserInfoM m)
|
2019-03-18 19:22:21 +03:00
|
|
|
=> SetRelComment -> m EncJSON
|
2018-12-13 10:26:15 +03:00
|
|
|
runSetRelComment defn = do
|
2019-03-01 12:17:22 +03:00
|
|
|
void $ validateRelP1 qt rn
|
2018-12-13 10:26:15 +03:00
|
|
|
setRelCommentP2 defn
|
2019-03-01 12:17:22 +03:00
|
|
|
where
|
|
|
|
SetRelComment qt rn _ = defn
|
2018-06-27 16:11:32 +03:00
|
|
|
|
|
|
|
setRelComment :: SetRelComment
|
|
|
|
-> Q.TxE QErr ()
|
2019-01-25 06:31:54 +03:00
|
|
|
setRelComment (SetRelComment (QualifiedObject sn tn) rn comment) =
|
2018-06-27 16:11:32 +03:00
|
|
|
Q.unitQE defaultTxErrorHandler [Q.sql|
|
|
|
|
UPDATE hdb_catalog.hdb_relationship
|
|
|
|
SET comment = $1
|
|
|
|
WHERE table_schema = $2
|
|
|
|
AND table_name = $3
|
|
|
|
AND rel_name = $4
|
|
|
|
|] (comment, sn, tn, rn) True
|