Skip to content

Commit 5435ff8

Browse files
authored
Fix(windows): Allow 'sqlmesh clean' to delete cache file paths that exceed 260 chars (#5512)
1 parent 84da6ac commit 5435ff8

File tree

4 files changed

+102
-9
lines changed

4 files changed

+102
-9
lines changed

sqlmesh/core/context.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@
139139
)
140140
from sqlmesh.utils.config import print_config
141141
from sqlmesh.utils.jinja import JinjaMacroRegistry
142+
from sqlmesh.utils.windows import IS_WINDOWS, fix_windows_path
142143

143144
if t.TYPE_CHECKING:
144145
import pandas as pd
@@ -2590,12 +2591,15 @@ def table_name(
25902591
)
25912592

25922593
def clear_caches(self) -> None:
2593-
for path in self.configs:
2594-
cache_path = path / c.CACHE
2595-
if cache_path.exists():
2596-
rmtree(cache_path)
2597-
if self.cache_dir.exists():
2598-
rmtree(self.cache_dir)
2594+
paths_to_remove = [path / c.CACHE for path in self.configs]
2595+
paths_to_remove.append(self.cache_dir)
2596+
2597+
if IS_WINDOWS:
2598+
paths_to_remove = [fix_windows_path(path) for path in paths_to_remove]
2599+
2600+
for path in paths_to_remove:
2601+
if path.exists():
2602+
rmtree(path)
25992603

26002604
if isinstance(self._state_sync, CachingStateSync):
26012605
self._state_sync.clear_cache()

sqlmesh/utils/windows.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,22 @@
33

44
IS_WINDOWS = platform.system() == "Windows"
55

6+
WINDOWS_LONGPATH_PREFIX = "\\\\?\\"
7+
68

79
def fix_windows_path(path: Path) -> Path:
810
"""
911
Windows paths are limited to 260 characters: https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation
1012
Users can change this by updating a registry entry but we cant rely on that.
11-
We can quite commonly generate a cache file path that exceeds 260 characters which causes a FileNotFound error.
12-
If we prefix the path with "\\?\" then we can have paths up to 32,767 characters
13+
14+
SQLMesh quite commonly generates cache file paths that exceed 260 characters and thus cause a FileNotFound error.
15+
If we prefix paths with "\\?\" then we can have paths up to 32,767 characters.
16+
17+
Note that this prefix also means that relative paths no longer work. From the above docs:
18+
> Because you cannot use the "\\?\" prefix with a relative path, relative paths are always limited to a total of MAX_PATH characters.
19+
20+
So we also call path.resolve() to resolve the relative sections so that operations like `path.read_text()` continue to work
1321
"""
14-
return Path("\\\\?\\" + str(path.absolute()))
22+
if path.parts and not path.parts[0].startswith(WINDOWS_LONGPATH_PREFIX):
23+
path = Path(WINDOWS_LONGPATH_PREFIX + str(path.absolute()))
24+
return path.resolve()

tests/core/test_context.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
NoChangesPlanError,
6363
)
6464
from sqlmesh.utils.metaprogramming import Executable
65+
from sqlmesh.utils.windows import IS_WINDOWS, fix_windows_path
6566
from tests.utils.test_helpers import use_terminal_console
6667
from tests.utils.test_filesystem import create_temp_file
6768

@@ -700,6 +701,45 @@ def test_clear_caches(tmp_path: pathlib.Path):
700701
assert not cache_dir.exists()
701702

702703

704+
def test_clear_caches_with_long_base_path(tmp_path: pathlib.Path):
705+
base_path = tmp_path / ("abcde" * 50)
706+
assert (
707+
len(str(base_path.absolute())) > 260
708+
) # Paths longer than 260 chars trigger problems on Windows
709+
710+
default_cache_dir = base_path / c.CACHE
711+
custom_cache_dir = base_path / ".test_cache"
712+
713+
# note: we create the Context here so it doesnt get passed any "fixed" paths
714+
ctx = Context(config=Config(cache_dir=str(custom_cache_dir)), paths=base_path)
715+
716+
if IS_WINDOWS:
717+
# fix these so we can use them in this test
718+
default_cache_dir = fix_windows_path(default_cache_dir)
719+
custom_cache_dir = fix_windows_path(custom_cache_dir)
720+
721+
default_cache_dir.mkdir(parents=True)
722+
custom_cache_dir.mkdir(parents=True)
723+
724+
default_cache_file = default_cache_dir / "cache.txt"
725+
custom_cache_file = custom_cache_dir / "cache.txt"
726+
727+
default_cache_file.write_text("test")
728+
custom_cache_file.write_text("test")
729+
730+
assert default_cache_file.exists()
731+
assert custom_cache_file.exists()
732+
assert default_cache_dir.exists()
733+
assert custom_cache_dir.exists()
734+
735+
ctx.clear_caches()
736+
737+
assert not default_cache_file.exists()
738+
assert not custom_cache_file.exists()
739+
assert not default_cache_dir.exists()
740+
assert not custom_cache_dir.exists()
741+
742+
703743
def test_cache_path_configurations(tmp_path: pathlib.Path):
704744
project_dir = tmp_path / "project"
705745
project_dir.mkdir(parents=True)

tests/utils/test_windows.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import pytest
2+
from pathlib import Path
3+
from sqlmesh.utils.windows import IS_WINDOWS, WINDOWS_LONGPATH_PREFIX, fix_windows_path
4+
5+
6+
@pytest.mark.skipif(
7+
not IS_WINDOWS, reason="pathlib.Path only produces WindowsPath objects on Windows"
8+
)
9+
def test_fix_windows_path():
10+
short_path = Path("c:\\foo")
11+
short_path_prefixed = Path(WINDOWS_LONGPATH_PREFIX + "c:\\foo")
12+
13+
segments = "\\".join(["bar", "baz", "bing"] * 50)
14+
long_path = Path("c:\\" + segments)
15+
long_path_prefixed = Path(WINDOWS_LONGPATH_PREFIX + "c:\\" + segments)
16+
17+
assert len(str(short_path.absolute)) < 260
18+
assert len(str(long_path.absolute)) > 260
19+
20+
# paths less than 260 chars are still prefixed because they may be being used as a base path
21+
assert fix_windows_path(short_path) == short_path_prefixed
22+
23+
# paths greater than 260 characters don't work at all without the prefix
24+
assert fix_windows_path(long_path) == long_path_prefixed
25+
26+
# multiple calls dont keep appending the same prefix
27+
assert (
28+
fix_windows_path(fix_windows_path(fix_windows_path(long_path_prefixed)))
29+
== long_path_prefixed
30+
)
31+
32+
# paths with relative sections need to have relative sections resolved before they can be used
33+
# since the \\?\ prefix doesnt work for paths with relative sections
34+
assert fix_windows_path(Path("c:\\foo\\..\\bar")) == Path(WINDOWS_LONGPATH_PREFIX + "c:\\bar")
35+
36+
# also check that relative sections are still resolved if they are added to a previously prefixed path
37+
base = fix_windows_path(Path("c:\\foo"))
38+
assert base == Path(WINDOWS_LONGPATH_PREFIX + "c:\\foo")
39+
assert fix_windows_path(base / ".." / "bar") == Path(WINDOWS_LONGPATH_PREFIX + "c:\\bar")

0 commit comments

Comments
 (0)