mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-14 08:02:15 +03:00
server: mssql: apply schema changes by mssql_run_sql DDL on metadata (fix #779)
Co-authored-by: Antoine Leblanc <1618949+nicuveo@users.noreply.github.com> GitOrigin-RevId: 6905d5914c8a698445c0ef03d6a8303747701e1c
This commit is contained in:
parent
8ebf5eef2b
commit
e43d0273e0
@ -19,6 +19,7 @@
|
||||
|
||||
### Bug fixes and improvements
|
||||
|
||||
- server: detect and apply metadata changes by `mssql_run_sql` API if required
|
||||
- server: fix bug with creation of new cron events when cron trigger is imported via metadata
|
||||
- server: log warning for deprecated environment variables.
|
||||
- server: initialise `hdb_catalog` tables only when required, and only run the event loop for sources where it is required
|
||||
|
@ -490,9 +490,9 @@ library
|
||||
, Hasura.RQL.DDL.Schema.Cache.Fields
|
||||
, Hasura.RQL.DDL.Schema.Cache.Permission
|
||||
, Hasura.RQL.DDL.Schema.Catalog
|
||||
, Hasura.RQL.DDL.Schema.Diff
|
||||
, Hasura.RQL.DDL.Schema.LegacyCatalog
|
||||
, Hasura.RQL.DDL.Schema.Common
|
||||
, Hasura.RQL.DDL.Schema.Diff
|
||||
, Hasura.RQL.DDL.Schema.Enum
|
||||
, Hasura.RQL.DDL.Schema.Function
|
||||
, Hasura.RQL.DDL.Schema.Rename
|
||||
|
@ -2,17 +2,19 @@ module Data.List.Extended
|
||||
( duplicates
|
||||
, uniques
|
||||
, getDifference
|
||||
, getDifferenceOn
|
||||
, getOverlapWith
|
||||
, module L
|
||||
) where
|
||||
|
||||
import Data.Hashable (Hashable)
|
||||
import Data.Function (on)
|
||||
import Data.Function (on)
|
||||
import Data.Hashable (Hashable)
|
||||
import Prelude
|
||||
|
||||
import qualified Data.HashMap.Strict as Map
|
||||
import qualified Data.HashSet as Set
|
||||
import qualified Data.List as L
|
||||
import qualified Data.List.NonEmpty as NE
|
||||
import qualified Data.HashMap.Strict.Extended as Map
|
||||
import qualified Data.HashSet as Set
|
||||
import qualified Data.List as L
|
||||
import qualified Data.List.NonEmpty as NE
|
||||
|
||||
duplicates :: (Eq a, Hashable a) => [a] -> Set.HashSet a
|
||||
duplicates =
|
||||
@ -23,3 +25,12 @@ uniques = map NE.head . NE.group
|
||||
|
||||
getDifference :: (Eq a, Hashable a) => [a] -> [a] -> Set.HashSet a
|
||||
getDifference = Set.difference `on` Set.fromList
|
||||
|
||||
getDifferenceOn :: (Eq k, Hashable k) => (v -> k) -> [v] -> [v] -> [v]
|
||||
getDifferenceOn f l = Map.elems . Map.differenceOn f l
|
||||
|
||||
getOverlapWith :: (Eq k, Hashable k) => (v -> k) -> [v] -> [v] -> [(v, v)]
|
||||
getOverlapWith getKey left right =
|
||||
Map.elems $ Map.intersectionWith (,) (mkMap left) (mkMap right)
|
||||
where
|
||||
mkMap = Map.fromList . map (\v -> (getKey v, v))
|
||||
|
@ -150,7 +150,7 @@ odbcExceptionToJSONValue =
|
||||
runJSONPathQuery
|
||||
:: (MonadError QErr m, MonadIO m)
|
||||
=> MSSQLPool -> ODBC.Query -> m Text
|
||||
runJSONPathQuery pool query = do
|
||||
runJSONPathQuery pool query =
|
||||
mconcat <$> withMSSQLPool pool (`ODBC.query` query)
|
||||
|
||||
withMSSQLPool
|
||||
|
@ -1,23 +1,32 @@
|
||||
{-# LANGUAGE ViewPatterns #-}
|
||||
|
||||
module Hasura.Backends.MSSQL.DDL.RunSQL
|
||||
( runSQL
|
||||
, MSSQLRunSQL
|
||||
, MSSQLRunSQL(..)
|
||||
, sqlContainsDDLKeyword
|
||||
)
|
||||
where
|
||||
|
||||
import Hasura.Prelude
|
||||
|
||||
import qualified Data.Aeson as J
|
||||
import qualified Data.HashMap.Strict as M
|
||||
import qualified Data.HashSet as HS
|
||||
import qualified Data.Text as T
|
||||
import qualified Database.ODBC.Internal as ODBC
|
||||
import qualified Text.Regex.TDFA as TDFA
|
||||
|
||||
import Data.Aeson.TH
|
||||
import Data.String (fromString)
|
||||
|
||||
import Hasura.Backends.MSSQL.Connection
|
||||
import Hasura.Backends.MSSQL.Meta
|
||||
import Hasura.Base.Error
|
||||
import Hasura.EncJSON
|
||||
import Hasura.RQL.DDL.Schema (RunSQLRes (..))
|
||||
import Hasura.RQL.Types
|
||||
import Hasura.RQL.DDL.Schema
|
||||
import Hasura.RQL.DDL.Schema.Diff
|
||||
import Hasura.RQL.Types hiding (TableName, tmTable)
|
||||
import Hasura.Server.Utils (quoteRegex)
|
||||
|
||||
|
||||
odbcValueToJValue :: ODBC.Value -> J.Value
|
||||
@ -44,11 +53,42 @@ $(deriveJSON hasuraJSON ''MSSQLRunSQL)
|
||||
|
||||
runSQL
|
||||
:: (MonadIO m, CacheRWM m, MonadError QErr m, MetadataM m)
|
||||
=> MSSQLRunSQL -> m EncJSON
|
||||
=> MSSQLRunSQL
|
||||
-> m EncJSON
|
||||
runSQL (MSSQLRunSQL sqlText source) = do
|
||||
pool <- _mscConnectionPool <$> askSourceConfig @'MSSQL source
|
||||
results <- withMSSQLPool pool $ \conn -> ODBC.query conn $ fromString $ T.unpack sqlText
|
||||
SourceInfo _ tableCache _ sourceConfig <- askSourceInfo @'MSSQL source
|
||||
let pool = _mscConnectionPool sourceConfig
|
||||
results <- if sqlContainsDDLKeyword sqlText then withMetadataCheck tableCache pool else runSQLQuery pool
|
||||
pure $ encJFromJValue $ toResult results
|
||||
where
|
||||
runSQLQuery pool = withMSSQLPool pool $ \conn ->
|
||||
ODBC.query conn $ fromString $ T.unpack sqlText
|
||||
|
||||
toTableMeta dbTablesMeta = M.toList dbTablesMeta <&> \(table, dbTableMeta) ->
|
||||
TableMeta table dbTableMeta [] -- No computed fields
|
||||
|
||||
withMetadataCheck tableCache pool = do
|
||||
-- If the SQL modifies the schema of the database then check for any metadata changes
|
||||
preActionTablesMeta <- toTableMeta <$> loadDBMetadata pool
|
||||
results <- runSQLQuery pool
|
||||
postActionTablesMeta <- toTableMeta <$> loadDBMetadata pool
|
||||
let trackedTablesMeta = filter (flip M.member tableCache . tmTable) preActionTablesMeta
|
||||
schemaDiff = getSchemaDiff trackedTablesMeta postActionTablesMeta
|
||||
metadataUpdater <- execWriterT $ processSchemaDiff source tableCache schemaDiff
|
||||
-- Build schema cache with updated metadata
|
||||
withNewInconsistentObjsCheck $
|
||||
buildSchemaCacheWithInvalidations mempty{ciSources = HS.singleton source} metadataUpdater
|
||||
pure results
|
||||
|
||||
sqlContainsDDLKeyword :: Text -> Bool
|
||||
sqlContainsDDLKeyword = TDFA.match $$(quoteRegex
|
||||
TDFA.defaultCompOpt
|
||||
{ TDFA.caseSensitive = False
|
||||
, TDFA.multiline = True
|
||||
, TDFA.lastStarGreedy = True }
|
||||
TDFA.defaultExecOpt
|
||||
{ TDFA.captureGroups = False }
|
||||
"\\balter\\b|\\bdrop\\b|\\bsp_rename\\b")
|
||||
|
||||
toResult :: [[(ODBC.Column, ODBC.Value)]] -> RunSQLRes
|
||||
toResult result = case result of
|
||||
|
@ -25,7 +25,6 @@ import Hasura.RQL.Types.Common (OID (..))
|
||||
import Hasura.RQL.Types.Table
|
||||
import Hasura.SQL.Backend
|
||||
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Loader
|
||||
|
||||
@ -39,7 +38,6 @@ loadDBMetadata pool = do
|
||||
Left e -> throw500 $ T.pack $ "error loading sql server database schema: " <> e
|
||||
Right sysTables -> pure $ HM.fromList $ map transformTable sysTables
|
||||
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Local types
|
||||
|
||||
|
@ -1,62 +1,110 @@
|
||||
module Hasura.Backends.Postgres.DDL.RunSQL
|
||||
(withMetadataCheck) where
|
||||
( runRunSQL
|
||||
, RunSQL(..)
|
||||
, isSchemaCacheBuildRequiredRunSQL
|
||||
) where
|
||||
|
||||
import Hasura.Prelude
|
||||
|
||||
import qualified Data.HashMap.Strict as M
|
||||
import qualified Data.HashMap.Strict.InsOrd as OMap
|
||||
import qualified Data.HashSet as HS
|
||||
import qualified Data.List.NonEmpty as NE
|
||||
import qualified Database.PG.Query as Q
|
||||
import qualified Text.Regex.TDFA as TDFA
|
||||
|
||||
import Control.Lens ((.~))
|
||||
import Control.Monad.Trans.Control (MonadBaseControl)
|
||||
import Data.Aeson.TH
|
||||
import Data.List.Extended (duplicates)
|
||||
import Data.Aeson
|
||||
import Data.Text.Extended
|
||||
|
||||
import qualified Hasura.SQL.AnyBackend as AB
|
||||
|
||||
import Hasura.Backends.Postgres.DDL.Source (ToMetadataFetchQuery, fetchTableMetadata)
|
||||
import Hasura.Backends.Postgres.DDL.Source (ToMetadataFetchQuery, fetchFunctionMetadata,
|
||||
fetchTableMetadata)
|
||||
import Hasura.Backends.Postgres.DDL.Table
|
||||
import Hasura.Backends.Postgres.SQL.Types hiding (TableName)
|
||||
import Hasura.Base.Error
|
||||
import Hasura.EncJSON
|
||||
import Hasura.RQL.DDL.Deps (reportDepsExt)
|
||||
import Hasura.RQL.DDL.Schema
|
||||
import Hasura.RQL.DDL.Schema.Common
|
||||
import Hasura.RQL.DDL.Schema.Function
|
||||
import Hasura.RQL.DDL.Schema.Rename
|
||||
import Hasura.RQL.DDL.Schema.Table
|
||||
import Hasura.RQL.DDL.Schema.Diff
|
||||
import Hasura.RQL.Types hiding (ConstraintName, fmFunction,
|
||||
tmComputedFields, tmTable)
|
||||
import Hasura.Server.Utils (quoteRegex)
|
||||
|
||||
|
||||
data FunctionMeta
|
||||
= FunctionMeta
|
||||
{ fmOid :: !OID
|
||||
, fmFunction :: !QualifiedFunction
|
||||
, fmType :: !FunctionVolatility
|
||||
data RunSQL
|
||||
= RunSQL
|
||||
{ rSql :: Text
|
||||
, rSource :: !SourceName
|
||||
, rCascade :: !Bool
|
||||
, rCheckMetadataConsistency :: !(Maybe Bool)
|
||||
, rTxAccessMode :: !Q.TxAccess
|
||||
} deriving (Show, Eq)
|
||||
$(deriveJSON hasuraJSON ''FunctionMeta)
|
||||
|
||||
data ComputedFieldMeta
|
||||
= ComputedFieldMeta
|
||||
{ ccmName :: !ComputedFieldName
|
||||
, ccmFunctionMeta :: !FunctionMeta
|
||||
} deriving (Show, Eq)
|
||||
$(deriveJSON hasuraJSON{omitNothingFields=True} ''ComputedFieldMeta)
|
||||
instance FromJSON RunSQL where
|
||||
parseJSON = withObject "RunSQL" $ \o -> do
|
||||
rSql <- o .: "sql"
|
||||
rSource <- o .:? "source" .!= defaultSource
|
||||
rCascade <- o .:? "cascade" .!= False
|
||||
rCheckMetadataConsistency <- o .:? "check_metadata_consistency"
|
||||
isReadOnly <- o .:? "read_only" .!= False
|
||||
let rTxAccessMode = if isReadOnly then Q.ReadOnly else Q.ReadWrite
|
||||
pure RunSQL{..}
|
||||
|
||||
data TableMeta (b :: BackendType)
|
||||
= TableMeta
|
||||
{ tmTable :: !QualifiedTable
|
||||
, tmInfo :: !(DBTableMetadata b)
|
||||
, tmComputedFields :: ![ComputedFieldMeta]
|
||||
} deriving (Show, Eq)
|
||||
instance ToJSON RunSQL where
|
||||
toJSON RunSQL {..} =
|
||||
object
|
||||
[ "sql" .= rSql
|
||||
, "source" .= rSource
|
||||
, "cascade" .= rCascade
|
||||
, "check_metadata_consistency" .= rCheckMetadataConsistency
|
||||
, "read_only" .=
|
||||
case rTxAccessMode of
|
||||
Q.ReadOnly -> True
|
||||
Q.ReadWrite -> False
|
||||
]
|
||||
|
||||
-- | see Note [Checking metadata consistency in run_sql]
|
||||
isSchemaCacheBuildRequiredRunSQL :: RunSQL -> Bool
|
||||
isSchemaCacheBuildRequiredRunSQL RunSQL {..} =
|
||||
case rTxAccessMode of
|
||||
Q.ReadOnly -> False
|
||||
Q.ReadWrite -> fromMaybe (containsDDLKeyword rSql) rCheckMetadataConsistency
|
||||
where
|
||||
containsDDLKeyword = TDFA.match $$(quoteRegex
|
||||
TDFA.defaultCompOpt
|
||||
{ TDFA.caseSensitive = False
|
||||
, TDFA.multiline = True
|
||||
, TDFA.lastStarGreedy = True }
|
||||
TDFA.defaultExecOpt
|
||||
{ TDFA.captureGroups = False }
|
||||
"\\balter\\b|\\bdrop\\b|\\breplace\\b|\\bcreate function\\b|\\bcomment on\\b")
|
||||
|
||||
|
||||
{- Note [Checking metadata consistency in run_sql]
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
SQL queries executed by run_sql may change the Postgres schema in arbitrary
|
||||
ways. We attempt to automatically update the metadata to reflect those changes
|
||||
as much as possible---for example, if a table is renamed, we want to update the
|
||||
metadata to track the table under its new name instead of its old one. This
|
||||
schema diffing (plus some integrity checking) is handled by withMetadataCheck.
|
||||
|
||||
But this process has overhead---it involves reloading the metadata, diffing it,
|
||||
and rebuilding the schema cache---so we don’t want to do it if it isn’t
|
||||
necessary. The user can explicitly disable the check via the
|
||||
check_metadata_consistency option, and we also skip it if the current
|
||||
transaction is in READ ONLY mode, since the schema can’t be modified in that
|
||||
case, anyway.
|
||||
|
||||
However, even if neither read_only or check_metadata_consistency is passed, lots
|
||||
of queries may not modify the schema at all. As a (fairly stupid) heuristic, we
|
||||
check if the query contains any keywords for DDL operations, and if not, we skip
|
||||
the metadata check as well. -}
|
||||
|
||||
fetchMeta
|
||||
:: (ToMetadataFetchQuery pgKind, BackendMetadata ('Postgres pgKind), MonadTx m)
|
||||
=> TableCache ('Postgres pgKind)
|
||||
-> FunctionCache ('Postgres pgKind)
|
||||
-> m ([TableMeta ('Postgres pgKind)], [FunctionMeta])
|
||||
-> m ([TableMeta ('Postgres pgKind)], [FunctionMeta ('Postgres pgKind)])
|
||||
fetchMeta tables functions = do
|
||||
tableMetaInfos <- fetchTableMetadata
|
||||
functionMetaInfos <- fetchFunctionMetadata
|
||||
@ -81,206 +129,41 @@ fetchMeta tables functions = do
|
||||
|
||||
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)
|
||||
runRunSQL
|
||||
:: forall (pgKind :: PostgresKind) m
|
||||
. ( BackendMetadata ('Postgres pgKind)
|
||||
, ToMetadataFetchQuery pgKind
|
||||
, CacheRWM m
|
||||
, HasServerConfigCtx m
|
||||
, MetadataM m
|
||||
, MonadBaseControl IO m
|
||||
, MonadError QErr m
|
||||
, MonadIO m
|
||||
)
|
||||
=> RunSQL
|
||||
-> m EncJSON
|
||||
runRunSQL q@RunSQL {..}
|
||||
-- see Note [Checking metadata consistency in run_sql]
|
||||
| isSchemaCacheBuildRequiredRunSQL q
|
||||
= withMetadataCheck @pgKind rSource rCascade rTxAccessMode $ execRawSQL rSql
|
||||
| otherwise
|
||||
= askSourceConfig @('Postgres pgKind) rSource >>= \sourceConfig ->
|
||||
liftEitherM $ runExceptT $
|
||||
runLazyTx (_pscExecCtx sourceConfig) rTxAccessMode $ execRawSQL rSql
|
||||
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
|
||||
:: (Backend ('Postgres pgKind), BackendMetadata ('Postgres pgKind))
|
||||
=> TableMeta ('Postgres pgKind)
|
||||
-> TableMeta ('Postgres pgKind)
|
||||
-> TableDiff ('Postgres pgKind)
|
||||
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
|
||||
:: forall pgKind m. (Backend ('Postgres pgKind), QErrM m, CacheRM m)
|
||||
=> SourceName -> QualifiedTable -> TableDiff ('Postgres pgKind) -> 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 pgKind) tn
|
||||
$ TOCol @('Postgres pgKind) droppedCol
|
||||
return $ getDependentObjs sc objId
|
||||
-- for all dropped constraints
|
||||
droppedConsDeps <- fmap concat $ forM droppedFKeyConstraints $ \droppedCons -> do
|
||||
let objId = SOSourceObj source
|
||||
$ AB.mkAnyBackend
|
||||
$ SOITableObj @('Postgres pgKind) tn
|
||||
$ TOForeignKey @('Postgres pgKind) droppedCons
|
||||
return $ getDependentObjs sc objId
|
||||
return $ droppedConsDeps <> droppedColDeps <> droppedComputedFieldDeps
|
||||
where
|
||||
TableDiff _ droppedCols _ _ droppedFKeyConstraints computedFieldDiff _ _ = tableDiff
|
||||
droppedComputedFieldDeps =
|
||||
map
|
||||
(SOSourceObj source
|
||||
. AB.mkAnyBackend
|
||||
. SOITableObj @('Postgres pgKind) tn
|
||||
. TOComputedField)
|
||||
$ _cfdDropped computedFieldDiff
|
||||
|
||||
data SchemaDiff (b :: BackendType)
|
||||
= SchemaDiff
|
||||
{ _sdDroppedTables :: ![QualifiedTable]
|
||||
, _sdAlteredTables :: ![(QualifiedTable, TableDiff b)]
|
||||
}
|
||||
|
||||
getSchemaDiff
|
||||
:: BackendMetadata ('Postgres pgKind)
|
||||
=> [TableMeta ('Postgres pgKind)]
|
||||
-> [TableMeta ('Postgres pgKind)]
|
||||
-> SchemaDiff ('Postgres pgKind)
|
||||
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
|
||||
:: forall pgKind m. (Backend ('Postgres pgKind), QErrM m, CacheRM m)
|
||||
=> SourceName -> SchemaDiff ('Postgres pgKind) -> m [SourceObjId ('Postgres pgKind)]
|
||||
getSchemaChangeDeps source schemaDiff = do
|
||||
-- Get schema cache
|
||||
sc <- askSchemaCache
|
||||
let tableIds =
|
||||
map
|
||||
(SOSourceObj source . AB.mkAnyBackend . SOITable @('Postgres pgKind))
|
||||
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) $
|
||||
return $ mapMaybe getIndirectDep $
|
||||
HS.toList $ HS.fromList $ tableDropDeps <> tableModDeps
|
||||
where
|
||||
SchemaDiff droppedTables alteredTables = schemaDiff
|
||||
|
||||
getIndirectDep :: SchemaObjId -> Maybe (SourceObjId ('Postgres pgKind))
|
||||
getIndirectDep (SOSourceObj s exists) =
|
||||
AB.unpackAnyBackend exists >>= \case
|
||||
srcObjId@(SOITableObj tn _) ->
|
||||
-- Indirect dependancy shouldn't be of same source and not among dropped tables
|
||||
if not (s == source && tn `HS.member` HS.fromList droppedTables)
|
||||
then Just srcObjId
|
||||
else Nothing
|
||||
srcObjId -> Just srcObjId
|
||||
getIndirectDep _ = Nothing
|
||||
|
||||
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
|
||||
execRawSQL :: (MonadTx n) => Text -> n EncJSON
|
||||
execRawSQL =
|
||||
fmap (encJFromJValue @RunSQLRes) . liftTx . Q.multiQE rawSqlErrHandler . Q.fromText
|
||||
where
|
||||
rawSqlErrHandler txe =
|
||||
(err400 PostgresError "query execution failed") { qeInternal = Just $ toJSON txe }
|
||||
|
||||
-- | @'withMetadataCheck' cascade action@ runs @action@ and checks if the schema changed as a
|
||||
-- result. If it did, it checks to ensure the changes do not violate any integrity constraints, and
|
||||
-- if not, incorporates them into the schema cache.
|
||||
withMetadataCheck
|
||||
:: forall (pgKind :: PostgresKind) a m
|
||||
. ( Backend ('Postgres pgKind)
|
||||
, BackendMetadata ('Postgres pgKind)
|
||||
. ( BackendMetadata ('Postgres pgKind)
|
||||
, ToMetadataFetchQuery pgKind
|
||||
, CacheRWM m
|
||||
, HasServerConfigCtx m
|
||||
@ -345,7 +228,7 @@ withMetadataCheck source cascade txAccess action = do
|
||||
"type of function " <> qf <<> " is altered to \"VOLATILE\" which is not supported now"
|
||||
|
||||
-- update the metadata with the changes
|
||||
processSchemaChanges preActionTables schemaDiff
|
||||
processSchemaDiff source preActionTables schemaDiff
|
||||
|
||||
pure (actionResult, metadataUpdater)
|
||||
|
||||
@ -367,97 +250,3 @@ withMetadataCheck source cascade txAccess action = do
|
||||
flip runReaderT serverConfigCtx $ mkAllTriggersQ triggerName table columns opsDefinition
|
||||
|
||||
pure actionResult
|
||||
where
|
||||
processSchemaChanges
|
||||
:: ( MonadError QErr m'
|
||||
, CacheRM m'
|
||||
, MonadWriter MetadataModifier m'
|
||||
)
|
||||
=> TableCache ('Postgres pgKind) -> SchemaDiff ('Postgres pgKind) -> m' ()
|
||||
processSchemaChanges preActionTables schemaDiff = do
|
||||
-- Purge the dropped tables
|
||||
forM_ droppedTables $
|
||||
\tn -> tell $ MetadataModifier $ metaSources.ix source.(toSourceMetadata @('Postgres pgKind)).smTables %~ OMap.delete tn
|
||||
|
||||
for_ alteredTables $ \(oldQtn, tableDiff) -> do
|
||||
ti <- onNothing
|
||||
(M.lookup oldQtn preActionTables)
|
||||
(throw500 $ "old table metadata not found in cache : " <>> oldQtn)
|
||||
processTableChanges source (_tiCoreInfo ti) tableDiff
|
||||
where
|
||||
SchemaDiff droppedTables alteredTables = schemaDiff
|
||||
|
||||
processTableChanges
|
||||
:: forall pgKind m
|
||||
. ( Backend ('Postgres pgKind)
|
||||
, BackendMetadata ('Postgres pgKind)
|
||||
, MonadError QErr m
|
||||
, CacheRM m
|
||||
, MonadWriter MetadataModifier m
|
||||
)
|
||||
=> SourceName -> TableCoreInfo ('Postgres pgKind) -> TableDiff ('Postgres pgKind) -> m ()
|
||||
processTableChanges source ti tableDiff = do
|
||||
-- If table rename occurs then don't replace constraints and
|
||||
-- process dropped/added columns, because schema reload happens eventually
|
||||
sc <- askSchemaCache
|
||||
let tn = _tciName ti
|
||||
withOldTabName = do
|
||||
procAlteredCols sc tn
|
||||
|
||||
withNewTabName newTN = do
|
||||
let tnGQL = snakeCaseQualifiedObject newTN
|
||||
-- check for GraphQL schema conflicts on new name
|
||||
checkConflictingNode sc tnGQL
|
||||
procAlteredCols sc tn
|
||||
-- update new table in metadata
|
||||
renameTableInMetadata @('Postgres pgKind) source newTN tn
|
||||
|
||||
-- Process computed field diff
|
||||
processComputedFieldDiff tn
|
||||
-- Drop custom column names for dropped columns
|
||||
possiblyDropCustomColumnNames tn
|
||||
maybe withOldTabName withNewTabName mNewName
|
||||
where
|
||||
TableDiff mNewName droppedCols _ alteredCols _ computedFieldDiff _ _ = tableDiff
|
||||
|
||||
possiblyDropCustomColumnNames tn = do
|
||||
let TableConfig customFields customColumnNames customName = _tciCustomConfig ti
|
||||
modifiedCustomColumnNames = foldl' (flip M.delete) customColumnNames droppedCols
|
||||
when (modifiedCustomColumnNames /= customColumnNames) $
|
||||
tell $ MetadataModifier $
|
||||
tableMetadataSetter @('Postgres pgKind) source tn.tmConfiguration .~ TableConfig customFields modifiedCustomColumnNames customName
|
||||
|
||||
procAlteredCols sc tn = for_ alteredCols $
|
||||
\( RawColumnInfo oldName _ oldType _ _
|
||||
, RawColumnInfo newName _ newType _ _ ) -> do
|
||||
if | oldName /= newName ->
|
||||
renameColumnInMetadata oldName newName source tn (_tciFieldInfoMap ti)
|
||||
|
||||
| oldType /= newType -> do
|
||||
let colId =
|
||||
SOSourceObj source
|
||||
$ AB.mkAnyBackend
|
||||
$ SOITableObj @('Postgres pgKind) tn
|
||||
$ TOCol @('Postgres pgKind) oldName
|
||||
typeDepObjs = getDependentObjsWith (== DROnType) sc colId
|
||||
|
||||
unless (null typeDepObjs) $ throw400 DependencyError $
|
||||
"cannot change type of column " <> oldName <<> " in table "
|
||||
<> tn <<> " because of the following dependencies : " <>
|
||||
reportSchemaObjs typeDepObjs
|
||||
|
||||
| otherwise -> pure ()
|
||||
|
||||
processComputedFieldDiff table = do
|
||||
let ComputedFieldDiff _ altered overloaded = computedFieldDiff
|
||||
getFunction = fmFunction . ccmFunctionMeta
|
||||
forM_ overloaded $ \(columnName, function) ->
|
||||
throw400 NotSupported $ "The function " <> function
|
||||
<<> " associated with computed field" <> columnName
|
||||
<<> " of table " <> table <<> " is being overloaded"
|
||||
forM_ altered $ \(old, new) ->
|
||||
if | (fmType . ccmFunctionMeta) new == FTVOLATILE ->
|
||||
throw400 NotSupported $ "The type of function " <> getFunction old
|
||||
<<> " associated with computed field " <> ccmName old
|
||||
<<> " of table " <> table <<> " is being altered to \"VOLATILE\""
|
||||
| otherwise -> pure ()
|
||||
|
@ -1,8 +1,8 @@
|
||||
module Hasura.Backends.Postgres.DDL.Source
|
||||
( ToMetadataFetchQuery
|
||||
, fetchFunctionMetadata
|
||||
, fetchPgScalars
|
||||
, fetchTableMetadata
|
||||
, fetchFunctionMetadata
|
||||
, initCatalogForSource
|
||||
, postDropSourceHook
|
||||
, resolveDatabaseMetadata
|
||||
|
@ -22,142 +22,27 @@ load and modify the Hasura catalog and schema cache.
|
||||
-}
|
||||
|
||||
module Hasura.RQL.DDL.Schema
|
||||
( module Hasura.RQL.DDL.Schema.Cache
|
||||
, module Hasura.RQL.DDL.Schema.Catalog
|
||||
, module Hasura.RQL.DDL.Schema.Function
|
||||
, module Hasura.RQL.DDL.Schema.Rename
|
||||
, module Hasura.RQL.DDL.Schema.Table
|
||||
|
||||
, RunSQL(..)
|
||||
, runRunSQL
|
||||
, isSchemaCacheBuildRequiredRunSQL
|
||||
|
||||
, RunSQLRes(..)
|
||||
) where
|
||||
( module M
|
||||
, RunSQLRes(..)
|
||||
) where
|
||||
|
||||
import Hasura.Prelude
|
||||
|
||||
import qualified Data.Text.Encoding as TE
|
||||
import qualified Database.PG.Query as Q
|
||||
import qualified Database.PostgreSQL.LibPQ as PQ
|
||||
import qualified Text.Regex.TDFA as TDFA
|
||||
import qualified Data.Text.Encoding as TE
|
||||
import qualified Database.PG.Query as Q
|
||||
import qualified Database.PostgreSQL.LibPQ as PQ
|
||||
|
||||
import Control.Monad.Trans.Control (MonadBaseControl)
|
||||
import Data.Aeson
|
||||
import Data.Aeson.TH
|
||||
|
||||
import Hasura.Backends.Postgres.DDL.RunSQL
|
||||
import Hasura.Backends.Postgres.DDL.Source (ToMetadataFetchQuery)
|
||||
import Hasura.Base.Error
|
||||
import Hasura.Base.Instances ()
|
||||
import Hasura.EncJSON
|
||||
import Hasura.RQL.DDL.Schema.Cache
|
||||
import Hasura.RQL.DDL.Schema.Catalog
|
||||
import Hasura.RQL.DDL.Schema.Function
|
||||
import Hasura.RQL.DDL.Schema.Rename
|
||||
import Hasura.RQL.DDL.Schema.Table
|
||||
import Hasura.RQL.Types
|
||||
import Hasura.Server.Utils (quoteRegex)
|
||||
import Hasura.Base.Instances ()
|
||||
import Hasura.RQL.DDL.Schema.Cache as M
|
||||
import Hasura.RQL.DDL.Schema.Catalog as M
|
||||
import Hasura.RQL.DDL.Schema.Function as M
|
||||
import Hasura.RQL.DDL.Schema.Rename as M
|
||||
import Hasura.RQL.DDL.Schema.Table as M
|
||||
|
||||
|
||||
data RunSQL
|
||||
= RunSQL
|
||||
{ rSql :: Text
|
||||
, rSource :: !SourceName
|
||||
, rCascade :: !Bool
|
||||
, rCheckMetadataConsistency :: !(Maybe Bool)
|
||||
, rTxAccessMode :: !Q.TxAccess
|
||||
} deriving (Show, Eq)
|
||||
|
||||
instance FromJSON RunSQL where
|
||||
parseJSON = withObject "RunSQL" $ \o -> do
|
||||
rSql <- o .: "sql"
|
||||
rSource <- o .:? "source" .!= defaultSource
|
||||
rCascade <- o .:? "cascade" .!= False
|
||||
rCheckMetadataConsistency <- o .:? "check_metadata_consistency"
|
||||
isReadOnly <- o .:? "read_only" .!= False
|
||||
let rTxAccessMode = if isReadOnly then Q.ReadOnly else Q.ReadWrite
|
||||
pure RunSQL{..}
|
||||
|
||||
instance ToJSON RunSQL where
|
||||
toJSON RunSQL {..} =
|
||||
object
|
||||
[ "sql" .= rSql
|
||||
, "source" .= rSource
|
||||
, "cascade" .= rCascade
|
||||
, "check_metadata_consistency" .= rCheckMetadataConsistency
|
||||
, "read_only" .=
|
||||
case rTxAccessMode of
|
||||
Q.ReadOnly -> True
|
||||
Q.ReadWrite -> False
|
||||
]
|
||||
|
||||
-- | see Note [Checking metadata consistency in run_sql]
|
||||
isSchemaCacheBuildRequiredRunSQL :: RunSQL -> Bool
|
||||
isSchemaCacheBuildRequiredRunSQL RunSQL {..} =
|
||||
case rTxAccessMode of
|
||||
Q.ReadOnly -> False
|
||||
Q.ReadWrite -> fromMaybe (containsDDLKeyword rSql) rCheckMetadataConsistency
|
||||
where
|
||||
containsDDLKeyword :: Text -> Bool
|
||||
containsDDLKeyword = TDFA.match $$(quoteRegex
|
||||
TDFA.defaultCompOpt
|
||||
{ TDFA.caseSensitive = False
|
||||
, TDFA.multiline = True
|
||||
, TDFA.lastStarGreedy = True }
|
||||
TDFA.defaultExecOpt
|
||||
{ TDFA.captureGroups = False }
|
||||
"\\balter\\b|\\bdrop\\b|\\breplace\\b|\\bcreate function\\b|\\bcomment on\\b")
|
||||
|
||||
runRunSQL
|
||||
:: forall (pgKind :: PostgresKind) m
|
||||
. ( BackendMetadata ('Postgres pgKind)
|
||||
, ToMetadataFetchQuery pgKind
|
||||
, CacheRWM m
|
||||
, HasServerConfigCtx m
|
||||
, MetadataM m
|
||||
, MonadBaseControl IO m
|
||||
, MonadError QErr m
|
||||
, MonadIO m
|
||||
)
|
||||
=> RunSQL
|
||||
-> m EncJSON
|
||||
runRunSQL q@RunSQL {..}
|
||||
-- see Note [Checking metadata consistency in run_sql]
|
||||
| isSchemaCacheBuildRequiredRunSQL q
|
||||
= withMetadataCheck @pgKind rSource rCascade rTxAccessMode $ execRawSQL rSql
|
||||
| otherwise
|
||||
= askSourceConfig @('Postgres pgKind) rSource >>= \sourceConfig ->
|
||||
liftEitherM $ runExceptT $
|
||||
runLazyTx (_pscExecCtx sourceConfig) rTxAccessMode $ execRawSQL rSql
|
||||
where
|
||||
execRawSQL :: (MonadTx n) => Text -> n EncJSON
|
||||
execRawSQL =
|
||||
fmap (encJFromJValue @RunSQLRes) . liftTx . Q.multiQE rawSqlErrHandler . Q.fromText
|
||||
where
|
||||
rawSqlErrHandler txe =
|
||||
(err400 PostgresError "query execution failed") { qeInternal = Just $ toJSON txe }
|
||||
|
||||
{- Note [Checking metadata consistency in run_sql]
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
SQL queries executed by run_sql may change the Postgres schema in arbitrary
|
||||
ways. We attempt to automatically update the metadata to reflect those changes
|
||||
as much as possible---for example, if a table is renamed, we want to update the
|
||||
metadata to track the table under its new name instead of its old one. This
|
||||
schema diffing (plus some integrity checking) is handled by withMetadataCheck.
|
||||
|
||||
But this process has overhead---it involves reloading the metadata, diffing it,
|
||||
and rebuilding the schema cache---so we don’t want to do it if it isn’t
|
||||
necessary. The user can explicitly disable the check via the
|
||||
check_metadata_consistency option, and we also skip it if the current
|
||||
transaction is in READ ONLY mode, since the schema can’t be modified in that
|
||||
case, anyway.
|
||||
|
||||
However, even if neither read_only or check_metadata_consistency is passed, lots
|
||||
of queries may not modify the schema at all. As a (fairly stupid) heuristic, we
|
||||
check if the query contains any keywords for DDL operations, and if not, we skip
|
||||
the metadata check as well. -}
|
||||
|
||||
data RunSQLRes
|
||||
= RunSQLRes
|
||||
{ rrResultType :: !Text
|
||||
|
@ -2,14 +2,8 @@ module Hasura.RQL.DDL.Schema.Common where
|
||||
|
||||
import Hasura.Prelude
|
||||
|
||||
import qualified Data.HashMap.Strict as HM
|
||||
import qualified Database.PG.Query as Q
|
||||
import qualified Hasura.SQL.AnyBackend as AB
|
||||
|
||||
import Data.FileEmbed (makeRelativeToProject)
|
||||
|
||||
import qualified Hasura.SQL.AnyBackend as AB
|
||||
|
||||
import Hasura.Backends.Postgres.SQL.Types
|
||||
import Hasura.Base.Error
|
||||
import Hasura.RQL.DDL.ComputedField
|
||||
import Hasura.RQL.DDL.EventTrigger
|
||||
@ -38,21 +32,3 @@ purgeDependentObject source sourceObjId = case sourceObjId of
|
||||
throw500
|
||||
$ "unexpected dependent object: "
|
||||
<> reportSchemaObj (SOSourceObj source $ AB.mkAnyBackend sourceObjId)
|
||||
|
||||
-- | Fetch Postgres metadata for all user functions
|
||||
fetchFunctionMetadata :: (MonadTx m) => m (DBFunctionsMetadata ('Postgres 'Vanilla))
|
||||
fetchFunctionMetadata = do
|
||||
results <- liftTx $ Q.withQE defaultTxErrorHandler
|
||||
$(makeRelativeToProject "src-rsr/pg_function_metadata.sql" >>= Q.sqlFromFile) () True
|
||||
pure $ HM.fromList $ flip map results $
|
||||
\(schema, table, Q.AltJ infos) -> (QualifiedObject schema table, infos)
|
||||
|
||||
-- | Fetch all scalar types from Postgres
|
||||
fetchPgScalars :: MonadTx m => m (HashSet PGScalarType)
|
||||
fetchPgScalars =
|
||||
liftTx $ Q.getAltJ . runIdentity . Q.getRow
|
||||
<$> Q.withQE defaultTxErrorHandler
|
||||
[Q.sql|
|
||||
SELECT coalesce(json_agg(typname), '[]')
|
||||
FROM pg_catalog.pg_type where typtype = 'b'
|
||||
|] () True
|
||||
|
@ -1,100 +1,89 @@
|
||||
module Hasura.RQL.DDL.Schema.Diff
|
||||
( TableMeta(..)
|
||||
, ComputedFieldMeta(..)
|
||||
|
||||
, getDifference
|
||||
|
||||
, TableDiff(..)
|
||||
, getTableDiff
|
||||
, getTableChangeDeps
|
||||
, ComputedFieldDiff(..)
|
||||
|
||||
, SchemaDiff(..)
|
||||
, getSchemaDiff
|
||||
, getSchemaChangeDeps
|
||||
|
||||
, FunctionMeta(..)
|
||||
, FunctionDiff(..)
|
||||
, getFuncDiff
|
||||
, getOverloadedFuncs
|
||||
) where
|
||||
module Hasura.RQL.DDL.Schema.Diff where
|
||||
|
||||
import Hasura.Prelude
|
||||
|
||||
import qualified Data.HashMap.Strict as M
|
||||
import qualified Data.HashMap.Strict.InsOrd as OMap
|
||||
import qualified Data.HashSet as HS
|
||||
import qualified Data.List.NonEmpty as NE
|
||||
import qualified Language.GraphQL.Draft.Syntax as G
|
||||
|
||||
import Data.Aeson.TH
|
||||
import Data.List.Extended (duplicates)
|
||||
import Control.Lens ((.~))
|
||||
import Data.Aeson
|
||||
import Data.List.Extended
|
||||
import Data.Text.Extended
|
||||
|
||||
import qualified Hasura.SQL.AnyBackend as AB
|
||||
|
||||
import Hasura.Backends.Postgres.SQL.Types hiding (TableName)
|
||||
import Hasura.Backends.Postgres.SQL.Types hiding (ConstraintName, FunctionName, TableName)
|
||||
import Hasura.Base.Error
|
||||
import Hasura.RQL.Types hiding (ConstraintName, fmFunction,
|
||||
tmComputedFields, tmTable)
|
||||
import Hasura.RQL.DDL.Schema.Rename
|
||||
import Hasura.RQL.DDL.Schema.Table
|
||||
import Hasura.RQL.Types hiding (fmFunction, tmComputedFields, tmTable)
|
||||
|
||||
|
||||
data FunctionMeta
|
||||
data FunctionMeta b
|
||||
= FunctionMeta
|
||||
{ fmOid :: !OID
|
||||
, fmFunction :: !QualifiedFunction
|
||||
, fmFunction :: !(FunctionName b)
|
||||
, fmType :: !FunctionVolatility
|
||||
} deriving (Show, Eq)
|
||||
$(deriveJSON hasuraJSON ''FunctionMeta)
|
||||
} deriving (Generic)
|
||||
deriving instance (Backend b) => Show (FunctionMeta b)
|
||||
deriving instance (Backend b) => Eq (FunctionMeta b)
|
||||
|
||||
data ComputedFieldMeta
|
||||
instance (Backend b) => FromJSON (FunctionMeta b) where
|
||||
parseJSON = genericParseJSON hasuraJSON
|
||||
instance (Backend b) => ToJSON (FunctionMeta b) where
|
||||
toJSON = genericToJSON hasuraJSON
|
||||
|
||||
data ComputedFieldMeta b
|
||||
= ComputedFieldMeta
|
||||
{ ccmName :: !ComputedFieldName
|
||||
, ccmFunctionMeta :: !FunctionMeta
|
||||
} deriving (Show, Eq)
|
||||
$(deriveJSON hasuraJSON{omitNothingFields=True} ''ComputedFieldMeta)
|
||||
, ccmFunctionMeta :: !(FunctionMeta b)
|
||||
} deriving (Generic, Show, Eq)
|
||||
|
||||
instance (Backend b) => FromJSON (ComputedFieldMeta b) where
|
||||
parseJSON = genericParseJSON hasuraJSON{omitNothingFields=True}
|
||||
instance (Backend b) => ToJSON (ComputedFieldMeta b) where
|
||||
toJSON = genericToJSON hasuraJSON{omitNothingFields=True}
|
||||
|
||||
data TableMeta (b :: BackendType)
|
||||
= TableMeta
|
||||
{ tmTable :: !QualifiedTable
|
||||
{ tmTable :: !(TableName b)
|
||||
, tmInfo :: !(DBTableMetadata b)
|
||||
, tmComputedFields :: ![ComputedFieldMeta]
|
||||
, tmComputedFields :: ![ComputedFieldMeta b]
|
||||
} deriving (Show, Eq)
|
||||
|
||||
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
|
||||
data ComputedFieldDiff (b :: BackendType)
|
||||
= ComputedFieldDiff
|
||||
{ _cfdDropped :: [ComputedFieldName]
|
||||
, _cfdAltered :: [(ComputedFieldMeta, ComputedFieldMeta)]
|
||||
, _cfdOverloaded :: [(ComputedFieldName, QualifiedFunction)]
|
||||
} deriving (Show, Eq)
|
||||
, _cfdAltered :: [(ComputedFieldMeta b, ComputedFieldMeta b)]
|
||||
, _cfdOverloaded :: [(ComputedFieldName, FunctionName b)]
|
||||
}
|
||||
deriving instance (Backend b) => Show (ComputedFieldDiff b)
|
||||
deriving instance (Backend b) => Eq (ComputedFieldDiff b)
|
||||
|
||||
data TableDiff (b :: BackendType)
|
||||
= TableDiff
|
||||
{ _tdNewName :: !(Maybe QualifiedTable)
|
||||
{ _tdNewName :: !(Maybe (TableName b))
|
||||
, _tdDroppedCols :: ![Column b]
|
||||
, _tdAddedCols :: ![RawColumnInfo b]
|
||||
, _tdAlteredCols :: ![(RawColumnInfo b, RawColumnInfo b)]
|
||||
, _tdDroppedFKeyCons :: ![ConstraintName]
|
||||
, _tdComputedFields :: !ComputedFieldDiff
|
||||
, _tdDroppedFKeyCons :: ![ConstraintName b]
|
||||
, _tdComputedFields :: !(ComputedFieldDiff b)
|
||||
-- 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]
|
||||
, _tdUniqOrPriCons :: ![ConstraintName b]
|
||||
, _tdNewDescription :: !(Maybe PGDescription)
|
||||
}
|
||||
|
||||
getTableDiff :: TableMeta ('Postgres 'Vanilla) -> TableMeta ('Postgres 'Vanilla) -> TableDiff ('Postgres 'Vanilla)
|
||||
getTableDiff
|
||||
:: Backend b
|
||||
=> TableMeta b
|
||||
-> TableMeta b
|
||||
-> TableDiff b
|
||||
getTableDiff oldtm newtm =
|
||||
TableDiff mNewName droppedCols addedCols alteredCols
|
||||
TableDiff mNewName droppedCols alteredCols
|
||||
droppedFKeyConstraints computedFieldDiff uniqueOrPrimaryCons mNewDesc
|
||||
where
|
||||
mNewName = bool (Just $ tmTable newtm) Nothing $ tmTable oldtm == tmTable newtm
|
||||
@ -107,9 +96,8 @@ getTableDiff oldtm newtm =
|
||||
|
||||
mNewDesc = _ptmiDescription $ tmInfo newtm
|
||||
|
||||
droppedCols = map prciName $ getDifference prciPosition oldCols newCols
|
||||
addedCols = getDifference prciPosition newCols oldCols
|
||||
existingCols = getOverlap prciPosition oldCols newCols
|
||||
droppedCols = map prciName $ getDifferenceOn prciPosition oldCols newCols
|
||||
existingCols = getOverlapWith prciPosition oldCols newCols
|
||||
alteredCols = filter (uncurry (/=)) existingCols
|
||||
|
||||
-- foreign keys are considered dropped only if their oid
|
||||
@ -118,9 +106,9 @@ getTableDiff oldtm newtm =
|
||||
droppedFKeysWithOid `HS.intersection` droppedFKeysWithUniq
|
||||
tmForeignKeys = fmap unForeignKeyMetadata . toList . _ptmiForeignKeys . tmInfo
|
||||
droppedFKeysWithOid = HS.fromList $
|
||||
(getDifference (_cOid . _fkConstraint) `on` tmForeignKeys) oldtm newtm
|
||||
(getDifferenceOn (_cOid . _fkConstraint) `on` tmForeignKeys) oldtm newtm
|
||||
droppedFKeysWithUniq = HS.fromList $
|
||||
(getDifference mkFKeyUniqId `on` tmForeignKeys) oldtm newtm
|
||||
(getDifferenceOn mkFKeyUniqId `on` tmForeignKeys) oldtm newtm
|
||||
mkFKeyUniqId (ForeignKey _ reftn colMap) = (reftn, colMap)
|
||||
|
||||
-- calculate computed field diff
|
||||
@ -128,10 +116,10 @@ getTableDiff oldtm newtm =
|
||||
newComputedFieldMeta = tmComputedFields newtm
|
||||
|
||||
droppedComputedFields = map ccmName $
|
||||
getDifference (fmOid . ccmFunctionMeta) oldComputedFieldMeta newComputedFieldMeta
|
||||
getDifferenceOn (fmOid . ccmFunctionMeta) oldComputedFieldMeta newComputedFieldMeta
|
||||
|
||||
alteredComputedFields =
|
||||
getOverlap (fmOid . ccmFunctionMeta) oldComputedFieldMeta newComputedFieldMeta
|
||||
getOverlapWith (fmOid . ccmFunctionMeta) oldComputedFieldMeta newComputedFieldMeta
|
||||
|
||||
overloadedComputedFieldFunctions =
|
||||
let getFunction = fmFunction . ccmFunctionMeta
|
||||
@ -144,96 +132,241 @@ getTableDiff oldtm newtm =
|
||||
overloadedComputedFieldFunctions
|
||||
|
||||
getTableChangeDeps
|
||||
:: (QErrM m, CacheRM m)
|
||||
=> SourceName -> QualifiedTable -> TableDiff ('Postgres 'Vanilla) -> m [SchemaObjId]
|
||||
:: forall b m
|
||||
. (QErrM m, CacheRM m, Backend b)
|
||||
=> SourceName
|
||||
-> TableName b
|
||||
-> TableDiff b
|
||||
-> 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
|
||||
$ SOITableObj @b tn
|
||||
$ TOCol @b 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
|
||||
$ SOITableObj @b tn
|
||||
$ TOForeignKey @b droppedCons
|
||||
return $ getDependentObjs sc objId
|
||||
return $ droppedConsDeps <> droppedColDeps <> droppedComputedFieldDeps
|
||||
where
|
||||
TableDiff _ droppedCols _ _ droppedFKeyConstraints computedFieldDiff _ _ = tableDiff
|
||||
TableDiff _ droppedCols _ droppedFKeyConstraints computedFieldDiff _ _ = tableDiff
|
||||
droppedComputedFieldDeps =
|
||||
map
|
||||
(SOSourceObj source
|
||||
. AB.mkAnyBackend
|
||||
. SOITableObj @('Postgres 'Vanilla) tn
|
||||
. SOITableObj @b tn
|
||||
. TOComputedField)
|
||||
$ _cfdDropped computedFieldDiff
|
||||
|
||||
data SchemaDiff (b :: BackendType)
|
||||
= SchemaDiff
|
||||
{ _sdDroppedTables :: ![QualifiedTable]
|
||||
, _sdAlteredTables :: ![(QualifiedTable, TableDiff b)]
|
||||
{ _sdDroppedTables :: ![TableName b]
|
||||
, _sdAlteredTables :: ![(TableName b, TableDiff b)]
|
||||
}
|
||||
|
||||
getSchemaDiff :: [TableMeta ('Postgres 'Vanilla)] -> [TableMeta ('Postgres 'Vanilla)] -> SchemaDiff ('Postgres 'Vanilla)
|
||||
getSchemaDiff
|
||||
:: (Backend b) => [TableMeta b] -> [TableMeta b] -> SchemaDiff b
|
||||
getSchemaDiff oldMeta newMeta =
|
||||
SchemaDiff droppedTables survivingTables
|
||||
where
|
||||
droppedTables = map tmTable $ getDifference (_ptmiOid . tmInfo) oldMeta newMeta
|
||||
droppedTables = map tmTable $ getDifferenceOn (_ptmiOid . tmInfo) oldMeta newMeta
|
||||
survivingTables =
|
||||
flip map (getOverlap (_ptmiOid . tmInfo) oldMeta newMeta) $ \(oldtm, newtm) ->
|
||||
flip map (getOverlapWith (_ptmiOid . tmInfo) oldMeta newMeta) $ \(oldtm, newtm) ->
|
||||
(tmTable oldtm, getTableDiff oldtm newtm)
|
||||
|
||||
getSchemaChangeDeps
|
||||
:: (QErrM m, CacheRM m)
|
||||
=> SourceName -> SchemaDiff ('Postgres 'Vanilla) -> m [SchemaObjId]
|
||||
:: forall b m. (QErrM m, CacheRM m, Backend b)
|
||||
=> SourceName -> SchemaDiff b -> m [SourceObjId b]
|
||||
getSchemaChangeDeps source schemaDiff = do
|
||||
-- Get schema cache
|
||||
sc <- askSchemaCache
|
||||
let tableIds =
|
||||
map
|
||||
(SOSourceObj source . AB.mkAnyBackend . SOITable @('Postgres 'Vanilla))
|
||||
(SOSourceObj source . AB.mkAnyBackend . SOITable @b)
|
||||
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) $
|
||||
-- return $ filter (not . isDirectDep) $
|
||||
return $ mapMaybe getIndirectDep $
|
||||
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
|
||||
getIndirectDep :: SchemaObjId -> Maybe (SourceObjId b)
|
||||
getIndirectDep (SOSourceObj s exists) =
|
||||
AB.unpackAnyBackend exists >>= \case
|
||||
srcObjId@(SOITableObj tn _) ->
|
||||
-- Indirect dependancy shouldn't be of same source and not among dropped tables
|
||||
if not (s == source && tn `HS.member` HS.fromList droppedTables)
|
||||
then Just srcObjId
|
||||
else Nothing
|
||||
srcObjId -> Just srcObjId
|
||||
getIndirectDep _ = Nothing
|
||||
|
||||
data FunctionDiff
|
||||
data FunctionDiff b
|
||||
= FunctionDiff
|
||||
{ fdDropped :: ![QualifiedFunction]
|
||||
, fdAltered :: ![(QualifiedFunction, FunctionVolatility)]
|
||||
} deriving (Show, Eq)
|
||||
{ fdDropped :: ![FunctionName b]
|
||||
, fdAltered :: ![(FunctionName b, FunctionVolatility)]
|
||||
}
|
||||
deriving instance (Backend b) => Show (FunctionDiff b)
|
||||
deriving instance (Backend b) => Eq (FunctionDiff b)
|
||||
|
||||
getFuncDiff :: [FunctionMeta] -> [FunctionMeta] -> FunctionDiff
|
||||
getFuncDiff :: [FunctionMeta b] -> [FunctionMeta b] -> FunctionDiff b
|
||||
getFuncDiff oldMeta newMeta =
|
||||
FunctionDiff droppedFuncs alteredFuncs
|
||||
where
|
||||
droppedFuncs = map fmFunction $ getDifference fmOid oldMeta newMeta
|
||||
alteredFuncs = mapMaybe mkAltered $ getOverlap fmOid oldMeta newMeta
|
||||
droppedFuncs = map fmFunction $ getDifferenceOn fmOid oldMeta newMeta
|
||||
alteredFuncs = mapMaybe mkAltered $ getOverlapWith fmOid oldMeta newMeta
|
||||
mkAltered (oldfm, newfm) =
|
||||
let isTypeAltered = fmType oldfm /= fmType newfm
|
||||
alteredFunc = (fmFunction oldfm, fmType newfm)
|
||||
in bool Nothing (Just alteredFunc) $ isTypeAltered
|
||||
in bool Nothing (Just alteredFunc) isTypeAltered
|
||||
|
||||
getOverloadedFuncs
|
||||
:: [QualifiedFunction] -> [FunctionMeta] -> [QualifiedFunction]
|
||||
:: (Backend b) => [FunctionName b] -> [FunctionMeta b] -> [FunctionName b]
|
||||
getOverloadedFuncs trackedFuncs newFuncMeta =
|
||||
toList $ duplicates $ map fmFunction trackedMeta
|
||||
where
|
||||
trackedMeta = flip filter newFuncMeta $ \fm ->
|
||||
fmFunction fm `elem` trackedFuncs
|
||||
|
||||
processSchemaDiff
|
||||
:: forall b m
|
||||
. ( MonadError QErr m
|
||||
, CacheRM m
|
||||
, MonadWriter MetadataModifier m
|
||||
, BackendMetadata b
|
||||
)
|
||||
=> SourceName
|
||||
-> TableCache b
|
||||
-> SchemaDiff b
|
||||
-> m ()
|
||||
processSchemaDiff source preActionTables schemaDiff = do
|
||||
-- Purge the dropped tables
|
||||
dropTablesInMetadata @b source droppedTables
|
||||
|
||||
for_ alteredTables $ \(oldQtn, tableDiff) -> do
|
||||
ti <- onNothing
|
||||
(M.lookup oldQtn preActionTables)
|
||||
(throw500 $ "old table metadata not found in cache : " <>> oldQtn)
|
||||
alterTableInMetadata source (_tiCoreInfo ti) tableDiff
|
||||
where
|
||||
SchemaDiff droppedTables alteredTables = schemaDiff
|
||||
|
||||
alterTableInMetadata
|
||||
:: forall m b
|
||||
. ( MonadError QErr m
|
||||
, CacheRM m
|
||||
, MonadWriter MetadataModifier m
|
||||
, BackendMetadata b
|
||||
)
|
||||
=> SourceName -> TableCoreInfo b -> TableDiff b -> m ()
|
||||
alterTableInMetadata source ti tableDiff = do
|
||||
-- If table rename occurs then don't replace constraints and
|
||||
-- process dropped/added columns, because schema reload happens eventually
|
||||
sc <- askSchemaCache
|
||||
let tn = _tciName ti
|
||||
withOldTabName = do
|
||||
alterColumnsInMetadata source alteredCols tableFields sc tn
|
||||
|
||||
withNewTabName :: TableName b -> m ()
|
||||
withNewTabName newTN = do
|
||||
-- check for GraphQL schema conflicts on new name
|
||||
liftEither (tableGraphQLName @b newTN) >>= checkConflictingNode sc . G.unName
|
||||
alterColumnsInMetadata source alteredCols tableFields sc tn
|
||||
-- update new table in metadata
|
||||
renameTableInMetadata @b source newTN tn
|
||||
|
||||
-- Process computed field diff
|
||||
processComputedFieldDiff tn
|
||||
-- Drop custom column names for dropped columns
|
||||
alterCustomColumnNamesInMetadata source droppedCols ti
|
||||
maybe withOldTabName withNewTabName mNewName
|
||||
where
|
||||
TableDiff mNewName droppedCols alteredCols _ computedFieldDiff _ _ = tableDiff
|
||||
tableFields = _tciFieldInfoMap ti
|
||||
|
||||
processComputedFieldDiff :: TableName b -> m ()
|
||||
processComputedFieldDiff table = do
|
||||
let ComputedFieldDiff _ altered overloaded = computedFieldDiff
|
||||
getFunction = fmFunction . ccmFunctionMeta
|
||||
forM_ overloaded $ \(columnName, function) ->
|
||||
throw400 NotSupported $ "The function " <> function
|
||||
<<> " associated with computed field" <> columnName
|
||||
<<> " of table " <> table <<> " is being overloaded"
|
||||
forM_ altered $ \(old, new) ->
|
||||
if | (fmType . ccmFunctionMeta) new == FTVOLATILE ->
|
||||
throw400 NotSupported $ "The type of function " <> getFunction old
|
||||
<<> " associated with computed field " <> ccmName old
|
||||
<<> " of table " <> table <<> " is being altered to \"VOLATILE\""
|
||||
| otherwise -> pure ()
|
||||
|
||||
dropTablesInMetadata
|
||||
:: forall b m
|
||||
. ( MonadWriter MetadataModifier m
|
||||
, BackendMetadata b
|
||||
)
|
||||
=> SourceName
|
||||
-> [TableName b]
|
||||
-> m ()
|
||||
dropTablesInMetadata source droppedTables =
|
||||
forM_ droppedTables $
|
||||
\tn -> tell $ MetadataModifier $ metaSources.ix source.toSourceMetadata.(smTables @b) %~ OMap.delete tn
|
||||
|
||||
alterColumnsInMetadata
|
||||
:: forall b m
|
||||
. ( MonadError QErr m
|
||||
, CacheRM m
|
||||
, MonadWriter MetadataModifier m
|
||||
, BackendMetadata b
|
||||
)
|
||||
=> SourceName
|
||||
-> [(RawColumnInfo b, RawColumnInfo b)]
|
||||
-> FieldInfoMap (FieldInfo b)
|
||||
-> SchemaCache
|
||||
-> TableName b
|
||||
-> m ()
|
||||
alterColumnsInMetadata source alteredCols fields sc tn = for_ alteredCols $
|
||||
\( RawColumnInfo oldName _ oldType _ _
|
||||
, RawColumnInfo newName _ newType _ _ ) -> do
|
||||
if | oldName /= newName ->
|
||||
renameColumnInMetadata oldName newName source tn fields
|
||||
|
||||
| oldType /= newType -> do
|
||||
let colId =
|
||||
SOSourceObj source
|
||||
$ AB.mkAnyBackend
|
||||
$ SOITableObj @b tn
|
||||
$ TOCol @b oldName
|
||||
typeDepObjs = getDependentObjsWith (== DROnType) sc colId
|
||||
|
||||
unless (null typeDepObjs) $ throw400 DependencyError $
|
||||
"cannot change type of column " <> oldName <<> " in table "
|
||||
<> tn <<> " because of the following dependencies : " <>
|
||||
reportSchemaObjs typeDepObjs
|
||||
|
||||
| otherwise -> pure ()
|
||||
|
||||
alterCustomColumnNamesInMetadata
|
||||
:: forall b m
|
||||
. (MonadWriter MetadataModifier m, BackendMetadata b)
|
||||
=> SourceName
|
||||
-> [Column b]
|
||||
-> TableCoreInfo b
|
||||
-> m ()
|
||||
alterCustomColumnNamesInMetadata source droppedCols ti = do
|
||||
let TableConfig customFields customColumnNames customName = _tciCustomConfig ti
|
||||
tn = _tciName ti
|
||||
modifiedCustomColumnNames = foldl' (flip M.delete) customColumnNames droppedCols
|
||||
when (modifiedCustomColumnNames /= customColumnNames) $
|
||||
tell $ MetadataModifier $
|
||||
tableMetadataSetter @b source tn.tmConfiguration .~
|
||||
TableConfig @b customFields modifiedCustomColumnNames customName
|
||||
|
@ -544,8 +544,7 @@ instance Backend b => FromJSON (ForeignKeyMetadata b) where
|
||||
else fail "columns and foreign_columns differ in length"
|
||||
|
||||
|
||||
-- | Metadata of a Postgres table which is being extracted from
|
||||
-- database via 'src-rsr/pg_table_metadata.sql'
|
||||
-- | Metadata of any Backend table which is being extracted from source database
|
||||
data DBTableMetadata (b :: BackendType)
|
||||
= DBTableMetadata
|
||||
{ _ptmiOid :: !OID
|
||||
|
@ -4,20 +4,21 @@ module Hasura.Server.API.Query where
|
||||
|
||||
import Hasura.Prelude
|
||||
|
||||
import qualified Data.Environment as Env
|
||||
import qualified Data.HashMap.Strict as HM
|
||||
import qualified Database.PG.Query as Q
|
||||
import qualified Network.HTTP.Client as HTTP
|
||||
import qualified Data.Environment as Env
|
||||
import qualified Data.HashMap.Strict as HM
|
||||
import qualified Database.PG.Query as Q
|
||||
import qualified Network.HTTP.Client as HTTP
|
||||
|
||||
import Control.Monad.Trans.Control (MonadBaseControl)
|
||||
import Control.Monad.Trans.Control (MonadBaseControl)
|
||||
import Control.Monad.Unique
|
||||
import Data.Aeson
|
||||
import Data.Aeson.Casing
|
||||
import Data.Aeson.TH
|
||||
import Network.HTTP.Client.Extended
|
||||
|
||||
import qualified Hasura.Tracing as Tracing
|
||||
import qualified Hasura.Tracing as Tracing
|
||||
|
||||
import Hasura.Backends.Postgres.DDL.RunSQL
|
||||
import Hasura.Base.Error
|
||||
import Hasura.EncJSON
|
||||
import Hasura.Metadata.Class
|
||||
@ -45,7 +46,7 @@ import Hasura.RQL.Types
|
||||
import Hasura.RQL.Types.Run
|
||||
import Hasura.Server.Types
|
||||
import Hasura.Server.Utils
|
||||
import Hasura.Server.Version (HasVersion)
|
||||
import Hasura.Server.Version (HasVersion)
|
||||
import Hasura.Session
|
||||
|
||||
|
||||
|
@ -14,6 +14,7 @@ import Data.Aeson.TH
|
||||
|
||||
import qualified Hasura.Backends.BigQuery.DDL.RunSQL as BigQuery
|
||||
import qualified Hasura.Backends.MSSQL.DDL.RunSQL as MSSQL
|
||||
import qualified Hasura.Backends.Postgres.DDL.RunSQL as Postgres
|
||||
import qualified Hasura.Tracing as Tracing
|
||||
|
||||
import Hasura.Base.Error
|
||||
@ -39,9 +40,9 @@ data RQLQuery
|
||||
| RQUpdate !UpdateQuery
|
||||
| RQDelete !DeleteQuery
|
||||
| RQCount !CountQuery
|
||||
| RQRunSql !RunSQL
|
||||
| RQRunSql !Postgres.RunSQL
|
||||
| RQMssqlRunSql !MSSQL.MSSQLRunSQL
|
||||
| RQCitusRunSql !RunSQL
|
||||
| RQCitusRunSql !Postgres.RunSQL
|
||||
| RQBigqueryRunSql !BigQuery.BigQueryRunSQL
|
||||
| RQBigqueryDatabaseInspection !BigQuery.BigQueryRunSQL
|
||||
| RQBulk ![RQLQuery]
|
||||
@ -97,9 +98,10 @@ runQuery env instanceId userInfo schemaCache httpManager serverConfigCtx rqlQuer
|
||||
|
||||
queryModifiesSchema :: RQLQuery -> Bool
|
||||
queryModifiesSchema = \case
|
||||
RQRunSql q -> isSchemaCacheBuildRequiredRunSQL q
|
||||
RQBulk l -> any queryModifiesSchema l
|
||||
_ -> False
|
||||
RQRunSql q -> Postgres.isSchemaCacheBuildRequiredRunSQL q
|
||||
RQMssqlRunSql q -> MSSQL.sqlContainsDDLKeyword $ MSSQL._mrsSql q
|
||||
RQBulk l -> any queryModifiesSchema l
|
||||
_ -> False
|
||||
|
||||
runQueryM
|
||||
:: ( HasVersion
|
||||
@ -119,9 +121,9 @@ runQueryM env = \case
|
||||
RQUpdate q -> runUpdate env q
|
||||
RQDelete q -> runDelete env q
|
||||
RQCount q -> runCount q
|
||||
RQRunSql q -> runRunSQL @'Vanilla q
|
||||
RQRunSql q -> Postgres.runRunSQL @'Vanilla q
|
||||
RQMssqlRunSql q -> MSSQL.runSQL q
|
||||
RQCitusRunSql q -> runRunSQL @'Citus q
|
||||
RQCitusRunSql q -> Postgres.runRunSQL @'Citus q
|
||||
RQBigqueryRunSql q -> BigQuery.runSQL q
|
||||
RQBigqueryDatabaseInspection q -> BigQuery.runDatabaseInspection q
|
||||
RQBulk l -> encJFromList <$> indexedMapM (runQueryM env) l
|
||||
|
13
server/tests-py/queries/v1/run_sql/schema_setup_mssql.yaml
Normal file
13
server/tests-py/queries/v1/run_sql/schema_setup_mssql.yaml
Normal file
@ -0,0 +1,13 @@
|
||||
type: bulk
|
||||
args:
|
||||
|
||||
- type: mssql_run_sql
|
||||
args:
|
||||
source: mssql
|
||||
sql: |
|
||||
CREATE TABLE author (
|
||||
id INT NOT NULL IDENTITY PRIMARY KEY,
|
||||
name TEXT
|
||||
);
|
||||
|
||||
INSERT INTO author (name) VALUES ('Bob'), ('Alice');
|
@ -0,0 +1,8 @@
|
||||
type: bulk
|
||||
args:
|
||||
|
||||
- type: mssql_run_sql
|
||||
args:
|
||||
source: mssql
|
||||
sql: |
|
||||
DROP TABLE author;
|
8
server/tests-py/queries/v1/run_sql/setup_mssql.yaml
Normal file
8
server/tests-py/queries/v1/run_sql/setup_mssql.yaml
Normal file
@ -0,0 +1,8 @@
|
||||
type: bulk
|
||||
args:
|
||||
|
||||
- type: mssql_track_table
|
||||
args:
|
||||
source: mssql
|
||||
table:
|
||||
name: author
|
85
server/tests-py/queries/v1/run_sql/sql_add_column_mssql.yaml
Normal file
85
server/tests-py/queries/v1/run_sql/sql_add_column_mssql.yaml
Normal file
@ -0,0 +1,85 @@
|
||||
# Create a table
|
||||
- url: /v2/query
|
||||
status: 200
|
||||
query:
|
||||
type: mssql_run_sql
|
||||
args:
|
||||
source: mssql
|
||||
sql: |
|
||||
CREATE TABLE test (
|
||||
id INT NOT NULL IDENTITY PRIMARY KEY,
|
||||
name TEXT
|
||||
);
|
||||
|
||||
INSERT INTO test (name) VALUES ('Bob'), ('Alice');
|
||||
|
||||
# Track table
|
||||
- url: /v1/metadata
|
||||
status: 200
|
||||
query:
|
||||
type: mssql_track_table
|
||||
args:
|
||||
source: mssql
|
||||
table:
|
||||
name: test
|
||||
|
||||
# GraphQL Query to fetch data from 'test' table
|
||||
- url: /v1/graphql
|
||||
status: 200
|
||||
response:
|
||||
data:
|
||||
test:
|
||||
- id: 1
|
||||
name: Bob
|
||||
- id: 2
|
||||
name: Alice
|
||||
query:
|
||||
query: |
|
||||
query {
|
||||
test{
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
|
||||
# Add a column in SQL
|
||||
- url: /v2/query
|
||||
status: 200
|
||||
query:
|
||||
type: mssql_run_sql
|
||||
args:
|
||||
source: mssql
|
||||
sql: |
|
||||
ALTER TABLE test ADD age INT NOT NULL CONSTRAINT age_def DEFAULT 0;
|
||||
|
||||
# GraphQL Query to fetch data from 'test' table
|
||||
- url: /v1/graphql
|
||||
status: 200
|
||||
response:
|
||||
data:
|
||||
test:
|
||||
- id: 1
|
||||
name: Bob
|
||||
age: 0
|
||||
- id: 2
|
||||
name: Alice
|
||||
age: 0
|
||||
query:
|
||||
query: |
|
||||
query {
|
||||
test{
|
||||
id
|
||||
name
|
||||
age
|
||||
}
|
||||
}
|
||||
|
||||
# Now drop the 'test' table
|
||||
- url: /v2/query
|
||||
status: 200
|
||||
query:
|
||||
type: mssql_run_sql
|
||||
args:
|
||||
source: mssql
|
||||
sql: |
|
||||
DROP TABLE test;
|
@ -0,0 +1,81 @@
|
||||
# Create a table
|
||||
- url: /v2/query
|
||||
status: 200
|
||||
query:
|
||||
type: mssql_run_sql
|
||||
args:
|
||||
source: mssql
|
||||
sql: |
|
||||
CREATE TABLE test (
|
||||
id INT NOT NULL IDENTITY PRIMARY KEY,
|
||||
name TEXT
|
||||
);
|
||||
|
||||
INSERT INTO test (name) VALUES ('Bob'), ('Alice');
|
||||
|
||||
# Track table
|
||||
- url: /v1/metadata
|
||||
status: 200
|
||||
query:
|
||||
type: mssql_track_table
|
||||
args:
|
||||
source: mssql
|
||||
table:
|
||||
name: test
|
||||
|
||||
# GraphQL Query to fetch data from 'test' table
|
||||
- url: /v1/graphql
|
||||
status: 200
|
||||
response:
|
||||
data:
|
||||
test:
|
||||
- id: 1
|
||||
name: Bob
|
||||
- id: 2
|
||||
name: Alice
|
||||
query:
|
||||
query: |
|
||||
query {
|
||||
test{
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
|
||||
# Drop a column in SQL
|
||||
- url: /v2/query
|
||||
status: 200
|
||||
query:
|
||||
type: mssql_run_sql
|
||||
args:
|
||||
source: mssql
|
||||
sql: |
|
||||
ALTER TABLE test DROP COLUMN name;
|
||||
|
||||
# Try to fetch the column data via GraphQL
|
||||
- url: /v1/graphql
|
||||
status: 200
|
||||
response:
|
||||
errors:
|
||||
- extensions:
|
||||
path: $.selectionSet.test.selectionSet.name
|
||||
code: validation-failed
|
||||
message: "field \"name\" not found in type: 'test'"
|
||||
query:
|
||||
query: |
|
||||
query {
|
||||
test{
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
|
||||
# Now drop the 'test' table
|
||||
- url: /v2/query
|
||||
status: 200
|
||||
query:
|
||||
type: mssql_run_sql
|
||||
args:
|
||||
source: mssql
|
||||
sql: |
|
||||
DROP TABLE test;
|
71
server/tests-py/queries/v1/run_sql/sql_drop_table_mssql.yaml
Normal file
71
server/tests-py/queries/v1/run_sql/sql_drop_table_mssql.yaml
Normal file
@ -0,0 +1,71 @@
|
||||
# Create a table
|
||||
- url: /v2/query
|
||||
status: 200
|
||||
query:
|
||||
type: mssql_run_sql
|
||||
args:
|
||||
source: mssql
|
||||
sql: |
|
||||
CREATE TABLE test (
|
||||
id INT NOT NULL IDENTITY PRIMARY KEY,
|
||||
name TEXT
|
||||
);
|
||||
|
||||
INSERT INTO test (name) VALUES ('Bob'), ('Alice');
|
||||
|
||||
# Track table
|
||||
- url: /v1/metadata
|
||||
status: 200
|
||||
query:
|
||||
type: mssql_track_table
|
||||
args:
|
||||
source: mssql
|
||||
table:
|
||||
name: test
|
||||
|
||||
# GraphQL Query to fetch data from 'test' table
|
||||
- url: /v1/graphql
|
||||
status: 200
|
||||
response:
|
||||
data:
|
||||
test:
|
||||
- id: 1
|
||||
name: Bob
|
||||
- id: 2
|
||||
name: Alice
|
||||
query:
|
||||
query: |
|
||||
query {
|
||||
test{
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
|
||||
# Drop the table in SQL
|
||||
- url: /v2/query
|
||||
status: 200
|
||||
query:
|
||||
type: mssql_run_sql
|
||||
args:
|
||||
source: mssql
|
||||
sql: |
|
||||
DROP TABLE test;
|
||||
|
||||
# Try to fetch data from dropped table
|
||||
- url: /v1/graphql
|
||||
status: 200
|
||||
response:
|
||||
errors:
|
||||
- extensions:
|
||||
path: $.selectionSet.test
|
||||
code: validation-failed
|
||||
message: "field \"test\" not found in type: 'query_root'"
|
||||
query:
|
||||
query: |
|
||||
query {
|
||||
test{
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
@ -0,0 +1,82 @@
|
||||
# Create a table
|
||||
- url: /v2/query
|
||||
status: 200
|
||||
query:
|
||||
type: mssql_run_sql
|
||||
args:
|
||||
source: mssql
|
||||
sql: |
|
||||
CREATE TABLE test (
|
||||
id INT NOT NULL IDENTITY PRIMARY KEY,
|
||||
name TEXT
|
||||
);
|
||||
|
||||
INSERT INTO test (name) VALUES ('Bob'), ('Alice');
|
||||
|
||||
# Track table
|
||||
- url: /v1/metadata
|
||||
status: 200
|
||||
query:
|
||||
type: mssql_track_table
|
||||
args:
|
||||
source: mssql
|
||||
table:
|
||||
name: test
|
||||
|
||||
# GraphQL Query to fetch data from 'test' table
|
||||
- url: /v1/graphql
|
||||
status: 200
|
||||
response:
|
||||
data:
|
||||
test:
|
||||
- id: 1
|
||||
name: Bob
|
||||
- id: 2
|
||||
name: Alice
|
||||
query:
|
||||
query: |
|
||||
query {
|
||||
test{
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
|
||||
# Add a column in SQL
|
||||
- url: /v2/query
|
||||
status: 200
|
||||
query:
|
||||
type: mssql_run_sql
|
||||
args:
|
||||
source: mssql
|
||||
sql: |
|
||||
EXEC sp_rename 'test.name', 'name_new', 'COLUMN';
|
||||
|
||||
# GraphQL Query to fetch data from 'test' table
|
||||
- url: /v1/graphql
|
||||
status: 200
|
||||
response:
|
||||
data:
|
||||
test:
|
||||
- id: 1
|
||||
name_new: Bob
|
||||
- id: 2
|
||||
name_new: Alice
|
||||
query:
|
||||
query: |
|
||||
query {
|
||||
test{
|
||||
id
|
||||
name_new
|
||||
}
|
||||
}
|
||||
|
||||
# Now drop the 'test' table
|
||||
- url: /v2/query
|
||||
status: 200
|
||||
query:
|
||||
type: mssql_run_sql
|
||||
args:
|
||||
source: mssql
|
||||
sql: |
|
||||
DROP TABLE test;
|
@ -0,0 +1,82 @@
|
||||
# Create a table
|
||||
- url: /v2/query
|
||||
status: 200
|
||||
query:
|
||||
type: mssql_run_sql
|
||||
args:
|
||||
source: mssql
|
||||
sql: |
|
||||
CREATE TABLE test (
|
||||
id INT NOT NULL IDENTITY PRIMARY KEY,
|
||||
name TEXT
|
||||
);
|
||||
|
||||
INSERT INTO test (name) VALUES ('Bob'), ('Alice');
|
||||
|
||||
# Track table
|
||||
- url: /v1/metadata
|
||||
status: 200
|
||||
query:
|
||||
type: mssql_track_table
|
||||
args:
|
||||
source: mssql
|
||||
table:
|
||||
name: test
|
||||
|
||||
# GraphQL Query to fetch data from 'test' table
|
||||
- url: /v1/graphql
|
||||
status: 200
|
||||
response:
|
||||
data:
|
||||
test:
|
||||
- id: 1
|
||||
name: Bob
|
||||
- id: 2
|
||||
name: Alice
|
||||
query:
|
||||
query: |
|
||||
query {
|
||||
test{
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
|
||||
# Rename the table in SQL
|
||||
- url: /v2/query
|
||||
status: 200
|
||||
query:
|
||||
type: mssql_run_sql
|
||||
args:
|
||||
source: mssql
|
||||
sql: |
|
||||
EXEC sp_rename 'dbo.test', 'test_new'
|
||||
|
||||
# GraphQL Query to fetch data from renamed 'test_new' table
|
||||
- url: /v1/graphql
|
||||
status: 200
|
||||
response:
|
||||
data:
|
||||
test_new:
|
||||
- id: 1
|
||||
name: Bob
|
||||
- id: 2
|
||||
name: Alice
|
||||
query:
|
||||
query: |
|
||||
query {
|
||||
test_new{
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
|
||||
# Now drop the 'test_new' table
|
||||
- url: /v2/query
|
||||
status: 200
|
||||
query:
|
||||
type: mssql_run_sql
|
||||
args:
|
||||
source: mssql
|
||||
sql: |
|
||||
DROP TABLE test_new;
|
@ -0,0 +1,17 @@
|
||||
url: /v2/query
|
||||
status: 200
|
||||
response:
|
||||
result_type: TuplesOk
|
||||
result:
|
||||
- - id
|
||||
- name
|
||||
- - 1
|
||||
- Bob
|
||||
- - 2
|
||||
- Alice
|
||||
query:
|
||||
type: mssql_run_sql
|
||||
args:
|
||||
source: mssql
|
||||
sql: |
|
||||
SELECT * FROM author;
|
2
server/tests-py/queries/v1/run_sql/teardown_mssql.yaml
Normal file
2
server/tests-py/queries/v1/run_sql/teardown_mssql.yaml
Normal file
@ -0,0 +1,2 @@
|
||||
type: bulk
|
||||
args: [] # Nothing to teardown as schema_teardown_mssql.yaml drops the table in metadata
|
@ -536,6 +536,30 @@ class TestRunSQL:
|
||||
def dir(cls):
|
||||
return "queries/v1/run_sql"
|
||||
|
||||
@pytest.mark.parametrize("backend", ['mssql'])
|
||||
@usefixtures('per_class_tests_db_state')
|
||||
class TestRunSQLMssql:
|
||||
@classmethod
|
||||
def dir(cls):
|
||||
return "queries/v1/run_sql"
|
||||
|
||||
def test_select_query(self, hge_ctx):
|
||||
check_query_f(hge_ctx, self.dir() + '/sql_select_query_mssql.yaml')
|
||||
|
||||
def test_drop_table(self, hge_ctx):
|
||||
check_query_f(hge_ctx, self.dir() + '/sql_drop_table_mssql.yaml')
|
||||
|
||||
def test_rename_table(self, hge_ctx):
|
||||
check_query_f(hge_ctx, self.dir() + '/sql_rename_table_mssql.yaml')
|
||||
|
||||
def test_drop_column(self, hge_ctx):
|
||||
check_query_f(hge_ctx, self.dir() + '/sql_drop_column_mssql.yaml')
|
||||
|
||||
def test_add_column(self, hge_ctx):
|
||||
check_query_f(hge_ctx, self.dir() + '/sql_add_column_mssql.yaml')
|
||||
|
||||
def test_rename_column(self, hge_ctx):
|
||||
check_query_f(hge_ctx, self.dir() + '/sql_rename_column_mssql.yaml')
|
||||
|
||||
@usefixtures('per_method_tests_db_state')
|
||||
class TestRelationships:
|
||||
|
Loading…
Reference in New Issue
Block a user