graphql-engine/server/src-lib/Hasura/RQL/DDL/Schema/Diff.hs
Antoine Leblanc 2152911e24 server: introduce Hasura.Base (take 2)
GitOrigin-RevId: 0dd10f1ccd338b1cf382ebff59b6ee7f209d39a1
2021-05-11 15:19:33 +00:00

272 lines
10 KiB
Haskell

module Hasura.RQL.DDL.Schema.Diff
( TableMeta(..)
, ComputedFieldMeta(..)
, fetchMeta
, getDifference
, TableDiff(..)
, getTableDiff
, getTableChangeDeps
, ComputedFieldDiff(..)
, SchemaDiff(..)
, getSchemaDiff
, getSchemaChangeDeps
, FunctionMeta(..)
, FunctionDiff(..)
, getFuncDiff
, getOverloadedFuncs
) where
import Hasura.Prelude
import qualified Data.HashMap.Strict as M
import qualified Data.HashSet as HS
import qualified Data.List.NonEmpty as NE
import Data.Aeson.TH
import Data.List.Extended (duplicates)
import qualified Hasura.SQL.AnyBackend as AB
import Hasura.Backends.Postgres.SQL.Types hiding (TableName)
import Hasura.Base.Error
import Hasura.RQL.DDL.Schema.Common
import Hasura.RQL.Types hiding (ConstraintName, fmFunction,
tmComputedFields, tmTable)
data FunctionMeta
= FunctionMeta
{ fmOid :: !OID
, fmFunction :: !QualifiedFunction
, fmType :: !FunctionVolatility
} deriving (Show, Eq)
$(deriveJSON hasuraJSON ''FunctionMeta)
data ComputedFieldMeta
= ComputedFieldMeta
{ ccmName :: !ComputedFieldName
, ccmFunctionMeta :: !FunctionMeta
} deriving (Show, Eq)
$(deriveJSON hasuraJSON{omitNothingFields=True} ''ComputedFieldMeta)
data TableMeta (b :: BackendType)
= TableMeta
{ tmTable :: !QualifiedTable
, tmInfo :: !(DBTableMetadata b)
, tmComputedFields :: ![ComputedFieldMeta]
} deriving (Show, Eq)
fetchMeta
:: (MonadTx m)
=> TableCache ('Postgres 'Vanilla)
-> FunctionCache ('Postgres 'Vanilla)
-> m ([TableMeta ('Postgres 'Vanilla)], [FunctionMeta])
fetchMeta tables functions = do
tableMetaInfos <- fetchTableMetadata
functionMetaInfos <- fetchFunctionMetadata
let getFunctionMetas function =
let mkFunctionMeta rawInfo =
FunctionMeta (rfiOid rawInfo) function (rfiFunctionType rawInfo)
in maybe [] (map mkFunctionMeta) $ M.lookup function functionMetaInfos
mkComputedFieldMeta computedField =
let function = _cffName $ _cfiFunction computedField
in map (ComputedFieldMeta (_cfiName computedField)) $ getFunctionMetas function
tableMetas = flip map (M.toList tableMetaInfos) $ \(table, tableMetaInfo) ->
TableMeta table tableMetaInfo $ fromMaybe [] $
M.lookup table tables <&> \tableInfo ->
let tableCoreInfo = _tiCoreInfo tableInfo
computedFields = getComputedFieldInfos $ _tciFieldInfoMap tableCoreInfo
in concatMap mkComputedFieldMeta computedFields
functionMetas = concatMap getFunctionMetas $ M.keys functions
pure (tableMetas, functionMetas)
getOverlap :: (Eq k, Hashable k) => (v -> k) -> [v] -> [v] -> [(v, v)]
getOverlap getKey left right =
M.elems $ M.intersectionWith (,) (mkMap left) (mkMap right)
where
mkMap = M.fromList . map (\v -> (getKey v, v))
getDifference :: (Eq k, Hashable k) => (v -> k) -> [v] -> [v] -> [v]
getDifference getKey left right =
M.elems $ M.difference (mkMap left) (mkMap right)
where
mkMap = M.fromList . map (\v -> (getKey v, v))
data ComputedFieldDiff
= ComputedFieldDiff
{ _cfdDropped :: [ComputedFieldName]
, _cfdAltered :: [(ComputedFieldMeta, ComputedFieldMeta)]
, _cfdOverloaded :: [(ComputedFieldName, QualifiedFunction)]
} deriving (Show, Eq)
data TableDiff (b :: BackendType)
= TableDiff
{ _tdNewName :: !(Maybe QualifiedTable)
, _tdDroppedCols :: ![Column b]
, _tdAddedCols :: ![RawColumnInfo b]
, _tdAlteredCols :: ![(RawColumnInfo b, RawColumnInfo b)]
, _tdDroppedFKeyCons :: ![ConstraintName]
, _tdComputedFields :: !ComputedFieldDiff
-- The final list of uniq/primary constraint names
-- used for generating types on_conflict clauses
-- TODO: this ideally should't be part of TableDiff
, _tdUniqOrPriCons :: ![ConstraintName]
, _tdNewDescription :: !(Maybe PGDescription)
}
getTableDiff :: TableMeta ('Postgres 'Vanilla) -> TableMeta ('Postgres 'Vanilla) -> TableDiff ('Postgres 'Vanilla)
getTableDiff oldtm newtm =
TableDiff mNewName droppedCols addedCols alteredCols
droppedFKeyConstraints computedFieldDiff uniqueOrPrimaryCons mNewDesc
where
mNewName = bool (Just $ tmTable newtm) Nothing $ tmTable oldtm == tmTable newtm
oldCols = _ptmiColumns $ tmInfo oldtm
newCols = _ptmiColumns $ tmInfo newtm
uniqueOrPrimaryCons = map _cName $
maybeToList (_pkConstraint <$> _ptmiPrimaryKey (tmInfo newtm))
<> toList (_ptmiUniqueConstraints $ tmInfo newtm)
mNewDesc = _ptmiDescription $ tmInfo newtm
droppedCols = map prciName $ getDifference prciPosition oldCols newCols
addedCols = getDifference prciPosition newCols oldCols
existingCols = getOverlap prciPosition oldCols newCols
alteredCols = filter (uncurry (/=)) existingCols
-- foreign keys are considered dropped only if their oid
-- and (ref-table, column mapping) are changed
droppedFKeyConstraints = map (_cName . _fkConstraint) $ HS.toList $
droppedFKeysWithOid `HS.intersection` droppedFKeysWithUniq
tmForeignKeys = fmap unForeignKeyMetadata . toList . _ptmiForeignKeys . tmInfo
droppedFKeysWithOid = HS.fromList $
(getDifference (_cOid . _fkConstraint) `on` tmForeignKeys) oldtm newtm
droppedFKeysWithUniq = HS.fromList $
(getDifference mkFKeyUniqId `on` tmForeignKeys) oldtm newtm
mkFKeyUniqId (ForeignKey _ reftn colMap) = (reftn, colMap)
-- calculate computed field diff
oldComputedFieldMeta = tmComputedFields oldtm
newComputedFieldMeta = tmComputedFields newtm
droppedComputedFields = map ccmName $
getDifference (fmOid . ccmFunctionMeta) oldComputedFieldMeta newComputedFieldMeta
alteredComputedFields =
getOverlap (fmOid . ccmFunctionMeta) oldComputedFieldMeta newComputedFieldMeta
overloadedComputedFieldFunctions =
let getFunction = fmFunction . ccmFunctionMeta
getSecondElement (_ NE.:| list) = listToMaybe list
in mapMaybe (fmap ((&&&) ccmName getFunction) . getSecondElement) $
flip NE.groupBy newComputedFieldMeta $ \l r ->
ccmName l == ccmName r && getFunction l == getFunction r
computedFieldDiff = ComputedFieldDiff droppedComputedFields alteredComputedFields
overloadedComputedFieldFunctions
getTableChangeDeps
:: (QErrM m, CacheRM m)
=> SourceName -> QualifiedTable -> TableDiff ('Postgres 'Vanilla) -> m [SchemaObjId]
getTableChangeDeps source tn tableDiff = do
sc <- askSchemaCache
-- for all the dropped columns
droppedColDeps <- fmap concat $ forM droppedCols $ \droppedCol -> do
let objId = SOSourceObj source
$ AB.mkAnyBackend
$ SOITableObj @('Postgres 'Vanilla) tn
$ TOCol @('Postgres 'Vanilla) droppedCol
return $ getDependentObjs sc objId
-- for all dropped constraints
droppedConsDeps <- fmap concat $ forM droppedFKeyConstraints $ \droppedCons -> do
let objId = SOSourceObj source
$ AB.mkAnyBackend
$ SOITableObj @('Postgres 'Vanilla) tn
$ TOForeignKey @('Postgres 'Vanilla) droppedCons
return $ getDependentObjs sc objId
return $ droppedConsDeps <> droppedColDeps <> droppedComputedFieldDeps
where
TableDiff _ droppedCols _ _ droppedFKeyConstraints computedFieldDiff _ _ = tableDiff
droppedComputedFieldDeps =
map
(SOSourceObj source
. AB.mkAnyBackend
. SOITableObj @('Postgres 'Vanilla) tn
. TOComputedField)
$ _cfdDropped computedFieldDiff
data SchemaDiff (b :: BackendType)
= SchemaDiff
{ _sdDroppedTables :: ![QualifiedTable]
, _sdAlteredTables :: ![(QualifiedTable, TableDiff b)]
}
getSchemaDiff :: [TableMeta ('Postgres 'Vanilla)] -> [TableMeta ('Postgres 'Vanilla)] -> SchemaDiff ('Postgres 'Vanilla)
getSchemaDiff oldMeta newMeta =
SchemaDiff droppedTables survivingTables
where
droppedTables = map tmTable $ getDifference (_ptmiOid . tmInfo) oldMeta newMeta
survivingTables =
flip map (getOverlap (_ptmiOid . tmInfo) oldMeta newMeta) $ \(oldtm, newtm) ->
(tmTable oldtm, getTableDiff oldtm newtm)
getSchemaChangeDeps
:: (QErrM m, CacheRM m)
=> SourceName -> SchemaDiff ('Postgres 'Vanilla) -> m [SchemaObjId]
getSchemaChangeDeps source schemaDiff = do
-- Get schema cache
sc <- askSchemaCache
let tableIds =
map
(SOSourceObj source . AB.mkAnyBackend . SOITable @('Postgres 'Vanilla))
droppedTables
-- Get the dependent of the dropped tables
let tableDropDeps = concatMap (getDependentObjs sc) tableIds
tableModDeps <- concat <$> traverse (uncurry (getTableChangeDeps source)) alteredTables
return $ filter (not . isDirectDep) $
HS.toList $ HS.fromList $ tableDropDeps <> tableModDeps
where
SchemaDiff droppedTables alteredTables = schemaDiff
isDirectDep (SOSourceObj s exists) =
case AB.unpackAnyBackend @('Postgres 'Vanilla) exists of
Just (SOITableObj pgTable _) ->
s == source && pgTable `HS.member` HS.fromList droppedTables
_ -> False
isDirectDep _ = False
data FunctionDiff
= FunctionDiff
{ fdDropped :: ![QualifiedFunction]
, fdAltered :: ![(QualifiedFunction, FunctionVolatility)]
} deriving (Show, Eq)
getFuncDiff :: [FunctionMeta] -> [FunctionMeta] -> FunctionDiff
getFuncDiff oldMeta newMeta =
FunctionDiff droppedFuncs alteredFuncs
where
droppedFuncs = map fmFunction $ getDifference fmOid oldMeta newMeta
alteredFuncs = mapMaybe mkAltered $ getOverlap fmOid oldMeta newMeta
mkAltered (oldfm, newfm) =
let isTypeAltered = fmType oldfm /= fmType newfm
alteredFunc = (fmFunction oldfm, fmType newfm)
in bool Nothing (Just alteredFunc) $ isTypeAltered
getOverloadedFuncs
:: [QualifiedFunction] -> [FunctionMeta] -> [QualifiedFunction]
getOverloadedFuncs trackedFuncs newFuncMeta =
toList $ duplicates $ map fmFunction trackedMeta
where
trackedMeta = flip filter newFuncMeta $ \fm ->
fmFunction fm `elem` trackedFuncs