graphql-engine/server/src-lib/Hasura/RQL/DDL/Schema/Diff.hs

270 lines
10 KiB
Haskell
Raw Normal View History

2018-06-27 16:11:32 +03:00
module Hasura.RQL.DDL.Schema.Diff
( TableMeta(..)
, ComputedFieldMeta(..)
2018-06-27 16:11:32 +03:00
, fetchMeta
, getDifference
2018-06-27 16:11:32 +03:00
, TableDiff(..)
, getTableDiff
, getTableChangeDeps
, ComputedFieldDiff(..)
2018-06-27 16:11:32 +03:00
, SchemaDiff(..)
, getSchemaDiff
, getSchemaChangeDeps
, FunctionMeta(..)
, FunctionDiff(..)
, getFuncDiff
, getOverloadedFuncs
2018-06-27 16:11:32 +03:00
) where
import Hasura.Prelude
2018-06-27 16:11:32 +03:00
import qualified Data.HashMap.Strict as M
import qualified Data.HashSet as HS
import qualified Data.List.NonEmpty as NE
2018-06-27 16:11:32 +03:00
import Data.Aeson.TH
import Data.List.Extended (duplicates)
import qualified Hasura.SQL.AnyBackend as AB
2018-06-27 16:11:32 +03:00
import Hasura.Backends.Postgres.SQL.Types hiding (TableName)
import Hasura.RQL.DDL.Schema.Common
import Hasura.RQL.Types hiding (ConstraintName, fmFunction,
tmComputedFields, tmTable)
2018-06-27 16:11:32 +03:00
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)
2018-06-27 16:11:32 +03:00
= 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
2018-06-27 16:11:32 +03:00
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)
2018-06-27 16:11:32 +03:00
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)
2018-06-27 16:11:32 +03:00
= 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)
}
2018-06-27 16:11:32 +03:00
getTableDiff :: TableMeta ('Postgres 'Vanilla) -> TableMeta ('Postgres 'Vanilla) -> TableDiff ('Postgres 'Vanilla)
2018-06-27 16:11:32 +03:00
getTableDiff oldtm newtm =
TableDiff mNewName droppedCols addedCols alteredCols
droppedFKeyConstraints computedFieldDiff uniqueOrPrimaryCons mNewDesc
2018-06-27 16:11:32 +03:00
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)
2018-06-27 16:11:32 +03:00
mNewDesc = _ptmiDescription $ tmInfo newtm
2018-06-27 16:11:32 +03:00
droppedCols = map prciName $ getDifference prciPosition oldCols newCols
addedCols = getDifference prciPosition newCols oldCols
existingCols = getOverlap prciPosition oldCols newCols
alteredCols = filter (uncurry (/=)) existingCols
2018-06-27 16:11:32 +03:00
-- 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)
2018-06-27 16:11:32 +03:00
-- 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
2018-06-27 16:11:32 +03:00
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
2018-06-27 16:11:32 +03:00
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
2018-06-27 16:11:32 +03:00
return $ getDependentObjs sc objId
return $ droppedConsDeps <> droppedColDeps <> droppedComputedFieldDeps
2018-06-27 16:11:32 +03:00
where
TableDiff _ droppedCols _ _ droppedFKeyConstraints computedFieldDiff _ _ = tableDiff
droppedComputedFieldDeps =
map
(SOSourceObj source
. AB.mkAnyBackend
. SOITableObj @('Postgres 'Vanilla) tn
. TOComputedField)
$ _cfdDropped computedFieldDiff
2018-06-27 16:11:32 +03:00
data SchemaDiff (b :: BackendType)
2018-06-27 16:11:32 +03:00
= SchemaDiff
{ _sdDroppedTables :: ![QualifiedTable]
, _sdAlteredTables :: ![(QualifiedTable, TableDiff b)]
}
2018-06-27 16:11:32 +03:00
getSchemaDiff :: [TableMeta ('Postgres 'Vanilla)] -> [TableMeta ('Postgres 'Vanilla)] -> SchemaDiff ('Postgres 'Vanilla)
2018-06-27 16:11:32 +03:00
getSchemaDiff oldMeta newMeta =
SchemaDiff droppedTables survivingTables
where
droppedTables = map tmTable $ getDifference (_ptmiOid . tmInfo) oldMeta newMeta
2018-06-27 16:11:32 +03:00
survivingTables =
flip map (getOverlap (_ptmiOid . tmInfo) oldMeta newMeta) $ \(oldtm, newtm) ->
2018-06-27 16:11:32 +03:00
(tmTable oldtm, getTableDiff oldtm newtm)
getSchemaChangeDeps
:: (QErrM m, CacheRM m)
=> SourceName -> SchemaDiff ('Postgres 'Vanilla) -> m [SchemaObjId]
getSchemaChangeDeps source schemaDiff = do
2018-06-27 16:11:32 +03:00
-- Get schema cache
sc <- askSchemaCache
let tableIds =
map
(SOSourceObj source . AB.mkAnyBackend . SOITable @('Postgres 'Vanilla))
droppedTables
2018-06-27 16:11:32 +03:00
-- Get the dependent of the dropped tables
let tableDropDeps = concatMap (getDependentObjs sc) tableIds
tableModDeps <- concat <$> traverse (uncurry (getTableChangeDeps source)) alteredTables
2018-06-27 16:11:32 +03:00
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