From f0479e7d1100ed2aac498733978fd21073dc2b93 Mon Sep 17 00:00:00 2001 From: Marty Kulma <18468315+martykulma@users.noreply.github.com> Date: Thu, 25 Sep 2025 10:08:23 -0400 Subject: [PATCH 1/5] add sql server validation check for agent state --- .../mzcompose/services/sql_server.py | 3 +- src/sql-server-util/src/inspect.rs | 11 ++++++ src/storage-types/src/connections.rs | 1 + test/sql-server-cdc/mzcompose.py | 39 +++++++++++++++++++ 4 files changed, 53 insertions(+), 1 deletion(-) diff --git a/misc/python/materialize/mzcompose/services/sql_server.py b/misc/python/materialize/mzcompose/services/sql_server.py index a65219b19eff1..7a6e2684d568f 100644 --- a/misc/python/materialize/mzcompose/services/sql_server.py +++ b/misc/python/materialize/mzcompose/services/sql_server.py @@ -25,6 +25,7 @@ def __init__( name: str = "sql-server", environment_extra: list[str] = [], volumes_extra: list[str] = [], + enable_agent: bool = True, ) -> None: super().__init__( name=name, @@ -35,7 +36,7 @@ def __init__( "environment": [ "ACCEPT_EULA=Y", "MSSQL_PID=Developer", - "MSSQL_AGENT_ENABLED=True", + f"MSSQL_AGENT_ENABLED={enable_agent}", f"SA_PASSWORD={sa_password}", *environment_extra, ], diff --git a/src/sql-server-util/src/inspect.rs b/src/sql-server-util/src/inspect.rs index 8ff52b1549250..7f436738c7415 100644 --- a/src/sql-server-util/src/inspect.rs +++ b/src/sql-server-util/src/inspect.rs @@ -642,6 +642,17 @@ pub async fn ensure_snapshot_isolation_enabled(client: &mut Client) -> Result<() Ok(()) } +/// Ensure the SQL Server Agent is running. +/// +/// See: +pub async fn ensure_sql_server_agent_running(client: &mut Client) -> Result<(), SqlServerError> { + static AGENT_STATUS_QUERY: &str = "SELECT status_desc FROM sys.dm_server_services WHERE servicename LIKE 'SQL Server Agent%';"; + let result = client.simple_query(AGENT_STATUS_QUERY).await?; + + check_system_result(&result, "SQL Server Agent status".to_string(), "Running")?; + Ok(()) +} + pub async fn get_tables(client: &mut Client) -> Result, SqlServerError> { let result = client .simple_query(&format!("{GET_COLUMNS_FOR_TABLES_WITH_CDC_QUERY};")) diff --git a/src/storage-types/src/connections.rs b/src/storage-types/src/connections.rs index 471905679eb5d..ec0ed2fee4971 100644 --- a/src/storage-types/src/connections.rs +++ b/src/storage-types/src/connections.rs @@ -2106,6 +2106,7 @@ impl SqlServerConnectionDetails { for error in [ mz_sql_server_util::inspect::ensure_database_cdc_enabled(&mut client).await, mz_sql_server_util::inspect::ensure_snapshot_isolation_enabled(&mut client).await, + mz_sql_server_util::inspect::ensure_sql_server_agent_running(&mut client).await, ] { match error { Err(mz_sql_server_util::SqlServerError::InvalidSystemSetting { diff --git a/test/sql-server-cdc/mzcompose.py b/test/sql-server-cdc/mzcompose.py index c09ae8e9efe20..92b0913c95633 100644 --- a/test/sql-server-cdc/mzcompose.py +++ b/test/sql-server-cdc/mzcompose.py @@ -124,6 +124,45 @@ def run(file: pathlib.Path | str) -> None: c.test_parts(sharded_files, run) +def workflow_no_agent(c: Composition, parser: WorkflowArgumentParser) -> None: + """ + Ensures that MZ detects that the SQL Server Agent is not running at purification and produces + an error to the user. + """ + + with c.override( + SqlServer(volumes_extra=["tmp:/var/opt/mssql"]), + ): + c.up("materialized", "sql-server", Service("testdrive", idle=True)) + c.run_testdrive_files( + "setup/setup.td", + f"--var=default-sql-server-user={SqlServer.DEFAULT_USER}", + f"--var=default-sql-server-password={SqlServer.DEFAULT_SA_PASSWORD}", + ) + c.stop("sql-server") + + with c.override( + SqlServer(enable_agent=False, volumes_extra=["tmp:/var/opt/mssql"]) + ): + c.up("sql-server") + + c.testdrive( + dedent( + f""" + > CREATE SECRET IF NOT EXISTS sql_server_pass AS '{SqlServer.DEFAULT_SA_PASSWORD}' + + ! CREATE CONNECTION sql_server_conn TO SQL SERVER ( + HOST 'sql-server', + PORT 1433, + DATABASE test, + USER '{SqlServer.DEFAULT_USER}', + PASSWORD = SECRET sql_server_pass); + contains:Invalid SQL Server system replication settings + """ + ) + ) + + def workflow_snapshot_consistency( c: Composition, parser: WorkflowArgumentParser ) -> None: From b8bf725784f6141993c76410c8c533d252f97973 Mon Sep 17 00:00:00 2001 From: Marty Kulma <18468315+martykulma@users.noreply.github.com> Date: Thu, 25 Sep 2025 10:25:14 -0400 Subject: [PATCH 2/5] Add a note to the doc to ensure the SQL Server Agent is running --- .../sql-server-direct/before-you-begin.html | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/doc/user/layouts/shortcodes/sql-server-direct/before-you-begin.html b/doc/user/layouts/shortcodes/sql-server-direct/before-you-begin.html index 9d7c352630851..6b83e8d622b00 100644 --- a/doc/user/layouts/shortcodes/sql-server-direct/before-you-begin.html +++ b/doc/user/layouts/shortcodes/sql-server-direct/before-you-begin.html @@ -5,3 +5,14 @@ - Ensure you have access to your SQL Server instance via the [`sqlcmd` client](https://learn.microsoft.com/en-us/sql/tools/sqlcmd/sqlcmd-utility), or your preferred SQL client. + +- Ensure SQL Server Agent is running. + ```mzsql + USE msdb; + SELECT + servicename, + status_desc, + startup_type_desc + FROM sys.dm_server_services + WHERE servicename LIKE 'SQL Server Agent%'; + ``` From 26498ba29924c29188c8a5a7679dd9cb340698bc Mon Sep 17 00:00:00 2001 From: Marty Kulma <18468315+martykulma@users.noreply.github.com> Date: Fri, 26 Sep 2025 11:44:09 -0400 Subject: [PATCH 3/5] SQL user requires VIEW SERVER STATE --- test/sql-server-cdc/21-privileges.td | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/sql-server-cdc/21-privileges.td b/test/sql-server-cdc/21-privileges.td index 5a6a4796b7c22..e16d46eda7462 100644 --- a/test/sql-server-cdc/21-privileges.td +++ b/test/sql-server-cdc/21-privileges.td @@ -34,9 +34,13 @@ DENY SELECT ON OBJECT::cdc.dbo_t1_privileges_ct(__$start_lsn) TO authorization_u CREATE LOGIN authorization_user_table_perms WITH PASSWORD = '${arg.default-sql-server-password}'; CREATE USER authorization_user_table_perms FOR LOGIN authorization_user_table_perms; ALTER ROLE db_datareader ADD MEMBER authorization_user_table_perms; - DENY SELECT ON OBJECT::dbo.t1_privileges TO authorization_user_table_perms; +USE master; +GRANT VIEW SERVER STATE TO authorization_user_column_perms; +GRANT VIEW SERVER STATE TO authorization_user_column_perms_capture_instance; +GRANT VIEW SERVER STATE TO authorization_user_table_perms; + > CREATE SECRET IF NOT EXISTS sql_server_pass AS '${arg.default-sql-server-password}' > CREATE CONNECTION sql_server_privileges_connection_table TO SQL SERVER ( From 92c98396ad9b61afcb154885cb6d00ece1d4f186 Mon Sep 17 00:00:00 2001 From: Marty Kulma <18468315+martykulma@users.noreply.github.com> Date: Fri, 26 Sep 2025 12:31:27 -0400 Subject: [PATCH 4/5] try using a dediced volume for MS --- test/sql-server-cdc/mzcompose.py | 70 +++++++++++++++++++------------- 1 file changed, 42 insertions(+), 28 deletions(-) diff --git a/test/sql-server-cdc/mzcompose.py b/test/sql-server-cdc/mzcompose.py index 92b0913c95633..ab9d5469c69d5 100644 --- a/test/sql-server-cdc/mzcompose.py +++ b/test/sql-server-cdc/mzcompose.py @@ -48,6 +48,10 @@ ), ] +VOLUMES = { + "ms_scratch": {} +} + # # Test that SQL Server ingestion works @@ -129,38 +133,48 @@ def workflow_no_agent(c: Composition, parser: WorkflowArgumentParser) -> None: Ensures that MZ detects that the SQL Server Agent is not running at purification and produces an error to the user. """ + # Start with a fresh state + c.kill("sql-server") + c.rm("sql-server") + c.kill("materialized") + c.rm("materialized") - with c.override( - SqlServer(volumes_extra=["tmp:/var/opt/mssql"]), - ): - c.up("materialized", "sql-server", Service("testdrive", idle=True)) - c.run_testdrive_files( - "setup/setup.td", - f"--var=default-sql-server-user={SqlServer.DEFAULT_USER}", - f"--var=default-sql-server-password={SqlServer.DEFAULT_SA_PASSWORD}", - ) - c.stop("sql-server") - + try: with c.override( - SqlServer(enable_agent=False, volumes_extra=["tmp:/var/opt/mssql"]) + SqlServer(volumes_extra=["ms_scratch:/var/opt/mssql"]), ): - c.up("sql-server") - - c.testdrive( - dedent( - f""" - > CREATE SECRET IF NOT EXISTS sql_server_pass AS '{SqlServer.DEFAULT_SA_PASSWORD}' - - ! CREATE CONNECTION sql_server_conn TO SQL SERVER ( - HOST 'sql-server', - PORT 1433, - DATABASE test, - USER '{SqlServer.DEFAULT_USER}', - PASSWORD = SECRET sql_server_pass); - contains:Invalid SQL Server system replication settings - """ - ) + c.up("materialized", "sql-server", Service("testdrive", idle=True)) + c.run_testdrive_files( + "setup/setup.td", + f"--var=default-sql-server-user={SqlServer.DEFAULT_USER}", + f"--var=default-sql-server-password={SqlServer.DEFAULT_SA_PASSWORD}", ) + c.kill("sql-server") + + with c.override( + SqlServer(enable_agent=False, volumes_extra=["ms_scratch:/var/opt/mssql"]) + ): + c.up("sql-server") + + c.testdrive( + dedent( + f""" + > CREATE SECRET IF NOT EXISTS sql_server_pass AS '{SqlServer.DEFAULT_SA_PASSWORD}' + + ! CREATE CONNECTION sql_server_conn TO SQL SERVER ( + HOST 'sql-server', + PORT 1433, + DATABASE test, + USER '{SqlServer.DEFAULT_USER}', + PASSWORD = SECRET sql_server_pass); + contains:Invalid SQL Server system replication settings + """ + ) + ) + finally: + c.kill("sql-server") + c.rm("sql-server") + c.rm_volumes("ms_scratch") def workflow_snapshot_consistency( From 2be34093d5e1038e6e863035aa3111542f6f0d9e Mon Sep 17 00:00:00 2001 From: Marty Kulma <18468315+martykulma@users.noreply.github.com> Date: Fri, 26 Sep 2025 14:14:55 -0400 Subject: [PATCH 5/5] fix lint --- test/sql-server-cdc/mzcompose.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/sql-server-cdc/mzcompose.py b/test/sql-server-cdc/mzcompose.py index ab9d5469c69d5..1b8a3ffb4f641 100644 --- a/test/sql-server-cdc/mzcompose.py +++ b/test/sql-server-cdc/mzcompose.py @@ -48,9 +48,7 @@ ), ] -VOLUMES = { - "ms_scratch": {} -} +VOLUMES = {"ms_scratch": {}} # @@ -152,7 +150,9 @@ def workflow_no_agent(c: Composition, parser: WorkflowArgumentParser) -> None: c.kill("sql-server") with c.override( - SqlServer(enable_agent=False, volumes_extra=["ms_scratch:/var/opt/mssql"]) + SqlServer( + enable_agent=False, volumes_extra=["ms_scratch:/var/opt/mssql"] + ) ): c.up("sql-server")