Skip to content

Commit 23eff11

Browse files
committed
Fix(windows): Allow 'sqlmesh clean' to delete cache files that exceed 260 chars
1 parent c8bee08 commit 23eff11

File tree

4 files changed

+101
-10
lines changed

4 files changed

+101
-10
lines changed

sqlmesh/core/context.py

Lines changed: 13 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,18 @@ 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 = [
2599+
fix_windows_path(path)
2600+
for path in paths_to_remove
2601+
]
2602+
2603+
for path in paths_to_remove:
2604+
if path.exists():
2605+
rmtree(path)
25992606

26002607
if isinstance(self._state_sync, CachingStateSync):
26012608
self._state_sync.clear_cache()

sqlmesh/utils/windows.py

Lines changed: 14 additions & 4 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-
return Path("\\\\?\\" + str(path.absolute()))
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 call path.resolve() to resolve the relative sections before appending the prefix to work around this
21+
"""
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: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@
6262
NoChangesPlanError,
6363
)
6464
from sqlmesh.utils.metaprogramming import Executable
65+
from sqlmesh.utils.windows import IS_WINDOWS, fix_windows_path
66+
from sqlmesh.utils import yaml
6567
from tests.utils.test_helpers import use_terminal_console
6668
from tests.utils.test_filesystem import create_temp_file
6769

@@ -700,6 +702,45 @@ def test_clear_caches(tmp_path: pathlib.Path):
700702
assert not cache_dir.exists()
701703

702704

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

tests/utils/test_windows.py

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

0 commit comments

Comments
 (0)