diff --git a/CHANGELOG.md b/CHANGELOG.md index 569a385b7bd..bd78c3c2453 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/server/graphql-engine.cabal b/server/graphql-engine.cabal index efe1c57944e..e370dca1f1e 100644 --- a/server/graphql-engine.cabal +++ b/server/graphql-engine.cabal @@ -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 diff --git a/server/src-lib/Data/List/Extended.hs b/server/src-lib/Data/List/Extended.hs index 507d542c4f0..ed693eb1006 100644 --- a/server/src-lib/Data/List/Extended.hs +++ b/server/src-lib/Data/List/Extended.hs @@ -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)) diff --git a/server/src-lib/Hasura/Backends/MSSQL/Connection.hs b/server/src-lib/Hasura/Backends/MSSQL/Connection.hs index e39b152c46d..41111eb0ca3 100644 --- a/server/src-lib/Hasura/Backends/MSSQL/Connection.hs +++ b/server/src-lib/Hasura/Backends/MSSQL/Connection.hs @@ -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 diff --git a/server/src-lib/Hasura/Backends/MSSQL/DDL/RunSQL.hs b/server/src-lib/Hasura/Backends/MSSQL/DDL/RunSQL.hs index 4518563387a..c4bb8765dcf 100644 --- a/server/src-lib/Hasura/Backends/MSSQL/DDL/RunSQL.hs +++ b/server/src-lib/Hasura/Backends/MSSQL/DDL/RunSQL.hs @@ -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 diff --git a/server/src-lib/Hasura/Backends/MSSQL/Meta.hs b/server/src-lib/Hasura/Backends/MSSQL/Meta.hs index 8e62fe6afe2..df9c594e10d 100644 --- a/server/src-lib/Hasura/Backends/MSSQL/Meta.hs +++ b/server/src-lib/Hasura/Backends/MSSQL/Meta.hs @@ -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 diff --git a/server/src-lib/Hasura/Backends/Postgres/DDL/RunSQL.hs b/server/src-lib/Hasura/Backends/Postgres/DDL/RunSQL.hs index 6fb2ca78c04..551f7454989 100644 --- a/server/src-lib/Hasura/Backends/Postgres/DDL/RunSQL.hs +++ b/server/src-lib/Hasura/Backends/Postgres/DDL/RunSQL.hs @@ -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 () diff --git a/server/src-lib/Hasura/Backends/Postgres/DDL/Source.hs b/server/src-lib/Hasura/Backends/Postgres/DDL/Source.hs index 90019efbbd3..3fc23a2a5fe 100644 --- a/server/src-lib/Hasura/Backends/Postgres/DDL/Source.hs +++ b/server/src-lib/Hasura/Backends/Postgres/DDL/Source.hs @@ -1,8 +1,8 @@ module Hasura.Backends.Postgres.DDL.Source ( ToMetadataFetchQuery - , fetchFunctionMetadata , fetchPgScalars , fetchTableMetadata + , fetchFunctionMetadata , initCatalogForSource , postDropSourceHook , resolveDatabaseMetadata diff --git a/server/src-lib/Hasura/RQL/DDL/Schema.hs b/server/src-lib/Hasura/RQL/DDL/Schema.hs index 536870a9553..81af0a65ef0 100644 --- a/server/src-lib/Hasura/RQL/DDL/Schema.hs +++ b/server/src-lib/Hasura/RQL/DDL/Schema.hs @@ -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 diff --git a/server/src-lib/Hasura/RQL/DDL/Schema/Common.hs b/server/src-lib/Hasura/RQL/DDL/Schema/Common.hs index 4fb433abea0..74a064c5850 100644 --- a/server/src-lib/Hasura/RQL/DDL/Schema/Common.hs +++ b/server/src-lib/Hasura/RQL/DDL/Schema/Common.hs @@ -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 diff --git a/server/src-lib/Hasura/RQL/DDL/Schema/Diff.hs b/server/src-lib/Hasura/RQL/DDL/Schema/Diff.hs index 9df60b7a489..3ab919b523d 100644 --- a/server/src-lib/Hasura/RQL/DDL/Schema/Diff.hs +++ b/server/src-lib/Hasura/RQL/DDL/Schema/Diff.hs @@ -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 diff --git a/server/src-lib/Hasura/RQL/Types/Table.hs b/server/src-lib/Hasura/RQL/Types/Table.hs index af2cde38e22..db4f2820ba4 100644 --- a/server/src-lib/Hasura/RQL/Types/Table.hs +++ b/server/src-lib/Hasura/RQL/Types/Table.hs @@ -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 diff --git a/server/src-lib/Hasura/Server/API/Query.hs b/server/src-lib/Hasura/Server/API/Query.hs index 6d67ab9e95a..3eaaad85241 100644 --- a/server/src-lib/Hasura/Server/API/Query.hs +++ b/server/src-lib/Hasura/Server/API/Query.hs @@ -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 diff --git a/server/src-lib/Hasura/Server/API/V2Query.hs b/server/src-lib/Hasura/Server/API/V2Query.hs index 11d61ccf2ac..b01f33dd9f6 100644 --- a/server/src-lib/Hasura/Server/API/V2Query.hs +++ b/server/src-lib/Hasura/Server/API/V2Query.hs @@ -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 diff --git a/server/tests-py/queries/v1/run_sql/schema_setup_mssql.yaml b/server/tests-py/queries/v1/run_sql/schema_setup_mssql.yaml new file mode 100644 index 00000000000..a095e3f8c69 --- /dev/null +++ b/server/tests-py/queries/v1/run_sql/schema_setup_mssql.yaml @@ -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'); diff --git a/server/tests-py/queries/v1/run_sql/schema_teardown_mssql.yaml b/server/tests-py/queries/v1/run_sql/schema_teardown_mssql.yaml new file mode 100644 index 00000000000..6aae7c65e3b --- /dev/null +++ b/server/tests-py/queries/v1/run_sql/schema_teardown_mssql.yaml @@ -0,0 +1,8 @@ +type: bulk +args: + +- type: mssql_run_sql + args: + source: mssql + sql: | + DROP TABLE author; diff --git a/server/tests-py/queries/v1/run_sql/setup_mssql.yaml b/server/tests-py/queries/v1/run_sql/setup_mssql.yaml new file mode 100644 index 00000000000..7b82e47c393 --- /dev/null +++ b/server/tests-py/queries/v1/run_sql/setup_mssql.yaml @@ -0,0 +1,8 @@ +type: bulk +args: + +- type: mssql_track_table + args: + source: mssql + table: + name: author diff --git a/server/tests-py/queries/v1/run_sql/sql_add_column_mssql.yaml b/server/tests-py/queries/v1/run_sql/sql_add_column_mssql.yaml new file mode 100644 index 00000000000..335bae863b7 --- /dev/null +++ b/server/tests-py/queries/v1/run_sql/sql_add_column_mssql.yaml @@ -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; diff --git a/server/tests-py/queries/v1/run_sql/sql_drop_column_mssql.yaml b/server/tests-py/queries/v1/run_sql/sql_drop_column_mssql.yaml new file mode 100644 index 00000000000..8e6b09e6574 --- /dev/null +++ b/server/tests-py/queries/v1/run_sql/sql_drop_column_mssql.yaml @@ -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; diff --git a/server/tests-py/queries/v1/run_sql/sql_drop_table_mssql.yaml b/server/tests-py/queries/v1/run_sql/sql_drop_table_mssql.yaml new file mode 100644 index 00000000000..952afcf30be --- /dev/null +++ b/server/tests-py/queries/v1/run_sql/sql_drop_table_mssql.yaml @@ -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 + } + } diff --git a/server/tests-py/queries/v1/run_sql/sql_rename_column_mssql.yaml b/server/tests-py/queries/v1/run_sql/sql_rename_column_mssql.yaml new file mode 100644 index 00000000000..50e6ac9ee32 --- /dev/null +++ b/server/tests-py/queries/v1/run_sql/sql_rename_column_mssql.yaml @@ -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; diff --git a/server/tests-py/queries/v1/run_sql/sql_rename_table_mssql.yaml b/server/tests-py/queries/v1/run_sql/sql_rename_table_mssql.yaml new file mode 100644 index 00000000000..38727db8439 --- /dev/null +++ b/server/tests-py/queries/v1/run_sql/sql_rename_table_mssql.yaml @@ -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; diff --git a/server/tests-py/queries/v1/run_sql/sql_select_query_mssql.yaml b/server/tests-py/queries/v1/run_sql/sql_select_query_mssql.yaml new file mode 100644 index 00000000000..d732446cd55 --- /dev/null +++ b/server/tests-py/queries/v1/run_sql/sql_select_query_mssql.yaml @@ -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; diff --git a/server/tests-py/queries/v1/run_sql/teardown_mssql.yaml b/server/tests-py/queries/v1/run_sql/teardown_mssql.yaml new file mode 100644 index 00000000000..828f9819ee1 --- /dev/null +++ b/server/tests-py/queries/v1/run_sql/teardown_mssql.yaml @@ -0,0 +1,2 @@ +type: bulk +args: [] # Nothing to teardown as schema_teardown_mssql.yaml drops the table in metadata diff --git a/server/tests-py/test_v1_queries.py b/server/tests-py/test_v1_queries.py index 2d6b6e0abf9..2e0053c2cf2 100644 --- a/server/tests-py/test_v1_queries.py +++ b/server/tests-py/test_v1_queries.py @@ -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: