From 859fc0298354d75c4678ee744ba0a95284f39e61 Mon Sep 17 00:00:00 2001 From: Nick Muoh Date: Sun, 1 Nov 2026 23:41:53 -0800 Subject: [PATCH 1/4] fix(tests): fix two Windows CI file-lock flakes - tests/core/engine_adapter/test_duckdb.py: three tests that call duck_conn.install_extension("ducklake") now share the same xdist_group("ducklake_extension"). On Windows, parallel xdist workers all downloading and renaming the extension binary at once triggers 'Could not move file: Access is denied'. Grouping them forces the three tests onto a single worker, so extension installation is always sequential. - tests/core/linter/test_helpers.py: both sushi-context tests now use the copy_to_temp_path fixture instead of pointing directly at examples/sushi. The shared .cache/optimized_query directory is excluded by the copytree ignore rules, so each test gets its own writable cache root and Windows file-lock contention is eliminated. Signed-off-by: Nick Muoh Co-authored-by: Cursor --- tests/core/engine_adapter/test_duckdb.py | 3 +++ tests/core/linter/test_helpers.py | 8 ++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/tests/core/engine_adapter/test_duckdb.py b/tests/core/engine_adapter/test_duckdb.py index 696689219b..e5736a71bd 100644 --- a/tests/core/engine_adapter/test_duckdb.py +++ b/tests/core/engine_adapter/test_duckdb.py @@ -126,6 +126,7 @@ def test_drop_catalog_motherduck(make_mocked_engine_adapter: t.Callable) -> None assert to_sql_calls(adapter) == ['DROP DATABASE IF EXISTS "foo" CASCADE'] +@pytest.mark.xdist_group("ducklake_extension") def test_ducklake_partitioning(adapter: EngineAdapter, duck_conn, tmp_path): catalog = "a_ducklake_db" @@ -156,6 +157,7 @@ def test_ducklake_partitioning(adapter: EngineAdapter, duck_conn, tmp_path): assert partition_info.shape[0] == 1 +@pytest.mark.xdist_group("ducklake_extension") def test_drop_table_ducklake_no_cascade(adapter: EngineAdapter, duck_conn, tmp_path): # DuckLake does not implement DROP TABLE/VIEW ... CASCADE, so the adapter must # omit CASCADE for objects in a DuckLake catalog while keeping it for native catalogs. @@ -187,6 +189,7 @@ def test_drop_table_ducklake_no_cascade(adapter: EngineAdapter, duck_conn, tmp_p assert not adapter.table_exists("memory.main.native_v") +@pytest.mark.xdist_group("ducklake_extension") def test_drop_table_ducklake_no_cascade_mixed_case_alias( adapter: EngineAdapter, duck_conn, tmp_path ): diff --git a/tests/core/linter/test_helpers.py b/tests/core/linter/test_helpers.py index c3ba46f304..7e0c7027ae 100644 --- a/tests/core/linter/test_helpers.py +++ b/tests/core/linter/test_helpers.py @@ -7,8 +7,8 @@ from sqlmesh.core.model import SqlModel -def test_get_position_of_model_block(): - context = Context(paths=["examples/sushi"]) +def test_get_position_of_model_block(copy_to_temp_path): + context = Context(paths=copy_to_temp_path("examples/sushi")) sql_models = [ model @@ -40,8 +40,8 @@ def test_get_position_of_model_block(): assert read_range.endswith(";") -def test_get_range_of_a_key_in_model_block_testing_on_sushi(): - context = Context(paths=["examples/sushi"]) +def test_get_range_of_a_key_in_model_block_testing_on_sushi(copy_to_temp_path): + context = Context(paths=copy_to_temp_path("examples/sushi")) sql_models = [ model From 95b152292020559c7abc6fc46a039c2853144d1c Mon Sep 17 00:00:00 2001 From: Nick Muoh Date: Mon, 2 Nov 2026 08:03:42 -0800 Subject: [PATCH 2/4] fix(tests): explicitly close duck_conn fixture to release Windows DLL locks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On Windows, a loaded DuckDB extension (.dll) holds an OS file handle that blocks install_extension() from atomically moving the newly-downloaded file into place. The duck_conn fixture previously only returned the connection (no explicit close), relying on CPython reference-counting to tear it down between tests. With the adapter fixture holding the connection via a lambda closure, and pytest-xdist running three ducklake tests sequentially on the same worker, two stale connections can still hold the ducklake.dll open by the time the third test calls install_extension(), producing: _duckdb.IOException: IO Error: Could not move file: Access is denied. Change the fixture to yield + conn.close() so the connection — and any DLLs it has loaded — is fully released before the next test begins. Signed-off-by: Nick Muoh Co-authored-by: Cursor --- tests/conftest.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 1f7f6d9325..bac7ae2da1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -272,8 +272,15 @@ def reset_console(): @pytest.fixture -def duck_conn() -> duckdb.DuckDBPyConnection: - return duckdb.connect() +def duck_conn() -> t.Generator[duckdb.DuckDBPyConnection, None, None]: + conn = duckdb.connect() + yield conn + # Explicitly close so that any extension DLLs loaded during the test are + # released before teardown. On Windows an open DLL handle prevents + # install_extension() from moving the newly-downloaded file into place, + # causing an "Access is denied" IOException when subsequent tests in the + # same xdist worker call install_extension() for the same extension. + conn.close() def push_plan(context: Context, plan: Plan) -> None: From 2811f79e19538c01d21d270b79669fbf9d4a9364 Mon Sep 17 00:00:00 2001 From: Nick Muoh Date: Tue, 3 Nov 2026 04:23:48 -0800 Subject: [PATCH 3/4] fix(tests): skip DuckLake reinstall when the extension is already loaded Windows keeps ducklake.dll mapped after the connection closes, so a later test's install_extension cannot move the file into place. Signed-off-by: Nick Muoh Co-authored-by: Cursor --- tests/core/engine_adapter/test_duckdb.py | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/tests/core/engine_adapter/test_duckdb.py b/tests/core/engine_adapter/test_duckdb.py index e5736a71bd..49b281173b 100644 --- a/tests/core/engine_adapter/test_duckdb.py +++ b/tests/core/engine_adapter/test_duckdb.py @@ -126,12 +126,26 @@ def test_drop_catalog_motherduck(make_mocked_engine_adapter: t.Callable) -> None assert to_sql_calls(adapter) == ['DROP DATABASE IF EXISTS "foo" CASCADE'] +def _ensure_ducklake(conn) -> None: + """Load DuckLake without reinstalling it when a previous test already did. + + On Windows, install_extension moves a file into the extension directory. + A ducklake.dll that is still mapped from an earlier test in this process + makes that move fail with "Access is denied". + """ + installed = conn.execute( + "SELECT installed FROM duckdb_extensions() WHERE extension_name = 'ducklake'" + ).fetchone() + if not installed or not installed[0]: + conn.install_extension("ducklake") + conn.load_extension("ducklake") + + @pytest.mark.xdist_group("ducklake_extension") def test_ducklake_partitioning(adapter: EngineAdapter, duck_conn, tmp_path): catalog = "a_ducklake_db" - duck_conn.install_extension("ducklake") - duck_conn.load_extension("ducklake") + _ensure_ducklake(duck_conn) duck_conn.execute( f"ATTACH 'ducklake:{tmp_path}/{catalog}.ducklake' AS {catalog} (DATA_PATH '{tmp_path}');" ) @@ -163,8 +177,7 @@ def test_drop_table_ducklake_no_cascade(adapter: EngineAdapter, duck_conn, tmp_p # omit CASCADE for objects in a DuckLake catalog while keeping it for native catalogs. catalog = "a_ducklake_db" - duck_conn.install_extension("ducklake") - duck_conn.load_extension("ducklake") + _ensure_ducklake(duck_conn) duck_conn.execute( f"ATTACH 'ducklake:{tmp_path}/{catalog}.ducklake' AS {catalog} (DATA_PATH '{tmp_path}');" ) @@ -195,8 +208,7 @@ def test_drop_table_ducklake_no_cascade_mixed_case_alias( ): # Catalog aliases keep their case in duckdb_databases(), but SQLMesh normalizes # table names to lowercase, so the catalog type lookup must be case-insensitive. - duck_conn.install_extension("ducklake") - duck_conn.load_extension("ducklake") + _ensure_ducklake(duck_conn) duck_conn.execute( f"ATTACH 'ducklake:{tmp_path}/my_lake.ducklake' AS \"MyLake\" (DATA_PATH '{tmp_path}');" ) From 64869907870f99e19cbacad4fca791ebaf94a994 Mon Sep 17 00:00:00 2001 From: Nick Muoh Date: Tue, 3 Nov 2026 04:30:05 -0800 Subject: [PATCH 4/4] test(duckdb): replace _ensure_ducklake helper with ducklake_conn fixture A fixture loads DuckLake once per test without a second install call while the DLL is still mapped, which on Windows causes 'Access is denied' when install_extension tries to move ducklake.dll. Signed-off-by: Nick Muoh Co-authored-by: Cursor --- tests/core/engine_adapter/test_duckdb.py | 51 ++++++++++++------------ 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/tests/core/engine_adapter/test_duckdb.py b/tests/core/engine_adapter/test_duckdb.py index 49b281173b..18caca8547 100644 --- a/tests/core/engine_adapter/test_duckdb.py +++ b/tests/core/engine_adapter/test_duckdb.py @@ -126,32 +126,35 @@ def test_drop_catalog_motherduck(make_mocked_engine_adapter: t.Callable) -> None assert to_sql_calls(adapter) == ['DROP DATABASE IF EXISTS "foo" CASCADE'] -def _ensure_ducklake(conn) -> None: +@pytest.fixture +def ducklake_conn(duck_conn): """Load DuckLake without reinstalling it when a previous test already did. On Windows, install_extension moves a file into the extension directory. A ducklake.dll that is still mapped from an earlier test in this process - makes that move fail with "Access is denied". + makes that move fail with "Access is denied". Installing only when + duckdb_extensions() says it is absent, then calling load_extension, avoids + the second install while the DLL is still mapped. """ - installed = conn.execute( + installed = duck_conn.execute( "SELECT installed FROM duckdb_extensions() WHERE extension_name = 'ducklake'" ).fetchone() if not installed or not installed[0]: - conn.install_extension("ducklake") - conn.load_extension("ducklake") + duck_conn.install_extension("ducklake") + duck_conn.load_extension("ducklake") + return duck_conn @pytest.mark.xdist_group("ducklake_extension") -def test_ducklake_partitioning(adapter: EngineAdapter, duck_conn, tmp_path): +def test_ducklake_partitioning(adapter: EngineAdapter, ducklake_conn, tmp_path): catalog = "a_ducklake_db" - _ensure_ducklake(duck_conn) - duck_conn.execute( + ducklake_conn.execute( f"ATTACH 'ducklake:{tmp_path}/{catalog}.ducklake' AS {catalog} (DATA_PATH '{tmp_path}');" ) # no partitions on catalog creation - partition_info = duck_conn.execute( + partition_info = ducklake_conn.execute( f"SELECT * FROM __ducklake_metadata_{catalog}.main.ducklake_partition_info" ).fetchdf() assert partition_info.empty @@ -165,31 +168,30 @@ def test_ducklake_partitioning(adapter: EngineAdapter, duck_conn, tmp_path): ) # 1 partition after table creation - partition_info = duck_conn.execute( + partition_info = ducklake_conn.execute( f"SELECT * FROM __ducklake_metadata_{catalog}.main.ducklake_partition_info" ).fetchdf() assert partition_info.shape[0] == 1 @pytest.mark.xdist_group("ducklake_extension") -def test_drop_table_ducklake_no_cascade(adapter: EngineAdapter, duck_conn, tmp_path): +def test_drop_table_ducklake_no_cascade(adapter: EngineAdapter, ducklake_conn, tmp_path): # DuckLake does not implement DROP TABLE/VIEW ... CASCADE, so the adapter must # omit CASCADE for objects in a DuckLake catalog while keeping it for native catalogs. catalog = "a_ducklake_db" - _ensure_ducklake(duck_conn) - duck_conn.execute( + ducklake_conn.execute( f"ATTACH 'ducklake:{tmp_path}/{catalog}.ducklake' AS {catalog} (DATA_PATH '{tmp_path}');" ) - duck_conn.execute(f"CREATE SCHEMA {catalog}.phys") - duck_conn.execute(f"CREATE SCHEMA {catalog}.virt") - duck_conn.execute(f"CREATE TABLE {catalog}.phys.t (i INTEGER)") - duck_conn.execute(f"CREATE VIEW {catalog}.virt.v AS SELECT * FROM {catalog}.phys.t") + ducklake_conn.execute(f"CREATE SCHEMA {catalog}.phys") + ducklake_conn.execute(f"CREATE SCHEMA {catalog}.virt") + ducklake_conn.execute(f"CREATE TABLE {catalog}.phys.t (i INTEGER)") + ducklake_conn.execute(f"CREATE VIEW {catalog}.virt.v AS SELECT * FROM {catalog}.phys.t") # native catalog, cascade is passed through - duck_conn.execute("CREATE TABLE memory.main.native_t (i INTEGER)") - duck_conn.execute("CREATE VIEW memory.main.native_v AS SELECT * FROM memory.main.native_t") + ducklake_conn.execute("CREATE TABLE memory.main.native_t (i INTEGER)") + ducklake_conn.execute("CREATE VIEW memory.main.native_v AS SELECT * FROM memory.main.native_t") adapter.drop_table(f"{catalog}.phys.t", cascade=True) adapter.drop_view(f"{catalog}.virt.v", cascade=True) @@ -204,17 +206,16 @@ def test_drop_table_ducklake_no_cascade(adapter: EngineAdapter, duck_conn, tmp_p @pytest.mark.xdist_group("ducklake_extension") def test_drop_table_ducklake_no_cascade_mixed_case_alias( - adapter: EngineAdapter, duck_conn, tmp_path + adapter: EngineAdapter, ducklake_conn, tmp_path ): # Catalog aliases keep their case in duckdb_databases(), but SQLMesh normalizes # table names to lowercase, so the catalog type lookup must be case-insensitive. - _ensure_ducklake(duck_conn) - duck_conn.execute( + ducklake_conn.execute( f"ATTACH 'ducklake:{tmp_path}/my_lake.ducklake' AS \"MyLake\" (DATA_PATH '{tmp_path}');" ) - duck_conn.execute("CREATE SCHEMA MyLake.phys") - duck_conn.execute("CREATE TABLE MyLake.phys.t (i INTEGER)") - duck_conn.execute("CREATE VIEW MyLake.phys.v AS SELECT * FROM MyLake.phys.t") + ducklake_conn.execute("CREATE SCHEMA MyLake.phys") + ducklake_conn.execute("CREATE TABLE MyLake.phys.t (i INTEGER)") + ducklake_conn.execute("CREATE VIEW MyLake.phys.v AS SELECT * FROM MyLake.phys.t") adapter.drop_table("mylake.phys.t", cascade=True) adapter.drop_view("mylake.phys.v", cascade=True)