Skip to content

Commit c2110eb

Browse files
committed
fix(native): never reuse a native discovery session for edited inputs
Document that a missing session means readers use the Python objects, and drop the session from the inputs each Rule fix pass lints.
1 parent 98102ef commit c2110eb

5 files changed

Lines changed: 82 additions & 1 deletion

File tree

‎src/sqlbuild/compiler/discovery/models.py‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -639,6 +639,11 @@ class DiscoveredDeclarationFiles:
639639
macro_files: tuple[DiscoveredMacroFile, ...]
640640
adapter_file: DiscoveredAdapterFile | None
641641
native_session: NativeDiscoverySession | None = field(default=None, compare=False, repr=False)
642+
"""The native session that read these declaration files, for later native stages.
643+
644+
`None` means no session: readers must use the Python objects. Any copy that changes a
645+
discovered collection must set it to `None` so it never describes other files.
646+
"""
642647

643648

644649
@dataclass(frozen=True)
@@ -674,7 +679,11 @@ class DiscoveredProjectInputs:
674679
providers: tuple[DiscoveredProvider, ...] = field(default_factory=tuple)
675680
adapter_file: DiscoveredAdapterFile | None = None
676681
native_session: NativeDiscoverySession | None = field(default=None, compare=False, repr=False)
677-
"""The native discovery session that read the declaration files, for later native stages."""
682+
"""The native session that read these declaration files, for later native stages.
683+
684+
`None` means no session: readers must use the Python objects. Any copy that changes a
685+
discovered collection must set it to `None` so it never describes other files.
686+
"""
678687

679688

680689
@dataclass(frozen=True)

‎src/sqlbuild/lint/_helpers/fixes.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,4 +439,5 @@ def _updated_inputs(
439439
else model
440440
for model in inputs.model_files
441441
),
442+
native_session=None,
442443
)

‎tests/integration/src/sqlbuild/cli/commands/main/_test_types.py‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1050,3 +1050,13 @@ class InferredFreshnessCliTestCase:
10501050
description: str
10511051
sources_yaml: str
10521052
expected_sources: tuple[tuple[object, ...], ...]
1053+
1054+
1055+
@dataclass(frozen=True)
1056+
class FixSessionTestCase:
1057+
"""A Rule fix under an engine whose discovery retains a native session."""
1058+
1059+
description: str
1060+
engine: str
1061+
expected_original_sessions: tuple[bool, ...]
1062+
expected_fix_pass_sessions: frozenset[bool]

‎tests/integration/src/sqlbuild/cli/commands/main/helpers.py‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434
)
3535
from sqlbuild.compiler.planner.exceptions import NativeSqlTestPlanningError
3636
from sqlbuild.compiler.planner.models import NativeSqlTestArtifact
37+
from sqlbuild.lint._helpers import fixes as lint_fixes
38+
from sqlbuild.lint.main import run_format as run_format_module
3739
from tests.integration.src.sqlbuild.cli.commands.main._test_types import (
3840
RepeatedJsonParseTestCase,
3941
)
@@ -189,6 +191,27 @@ def recorded_plan(**kwargs: Any) -> PlannedStaticSqlTests:
189191
return threads
190192

191193

194+
def record_fix_sessions(monkeypatch: pytest.MonkeyPatch) -> tuple[list[bool], list[bool]]:
195+
"""Record whether the inputs a Rule fix starts from, and each fix pass lints, hold a session."""
196+
197+
original: list[bool] = []
198+
passes: list[bool] = []
199+
plan: Callable[..., Any] = run_format_module.plan_rule_fixes
200+
lint: Callable[..., Any] = lint_fixes.run_lint
201+
202+
def recorded_plan(**kwargs: Any) -> Any:
203+
original.append(getattr(kwargs["discovered_inputs"], "native_session", None) is not None)
204+
return plan(**kwargs)
205+
206+
def recorded_lint(**kwargs: Any) -> Any:
207+
passes.append(kwargs["discovered_inputs"].native_session is not None)
208+
return lint(**kwargs)
209+
210+
monkeypatch.setattr(run_format_module, "plan_rule_fixes", recorded_plan)
211+
monkeypatch.setattr(lint_fixes, "run_lint", recorded_lint)
212+
return original, passes
213+
214+
192215
def skip_artifact_preparation(self: PreparedCompileArtifacts, **kwargs: object) -> None:
193216
"""Disable background preparation so the write phase plans SQL tests inline."""
194217

‎tests/integration/src/sqlbuild/cli/commands/main/test_format_fix.py‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,16 @@
1010
import pytest
1111

1212
from sqlbuild.cli.commands.main.entrypoint.entry import main
13+
from sqlbuild.compiler.frontier.constants import COMPILER_ENGINE_ENV_VAR
1314
from tests.integration.src.sqlbuild.cli.commands.main._test_types import (
15+
FixSessionTestCase,
1416
FormatCompileIntegrationTestCase,
1517
SemanticFixTestCase,
1618
UnusedOutputDifferentialCase,
1719
)
1820
from tests.integration.src.sqlbuild.cli.commands.main.helpers import (
1921
built_model_rows,
22+
record_fix_sessions,
2023
rule_fix_statuses,
2124
)
2225

@@ -31,6 +34,41 @@
3134
)
3235

3336

37+
@pytest.mark.parametrize(
38+
"test_case",
39+
[
40+
FixSessionTestCase(
41+
description="preview fix passes drop the session of the unfixed files",
42+
engine="native-preview",
43+
expected_original_sessions=(True,),
44+
expected_fix_pass_sessions=frozenset({False}),
45+
),
46+
],
47+
ids=lambda case: case.description,
48+
)
49+
def test_given_retained_native_session_when_fixing_then_fix_passes_never_reuse_it(
50+
test_case: FixSessionTestCase,
51+
tmp_path: Path,
52+
capsys: pytest.CaptureFixture[str],
53+
monkeypatch: pytest.MonkeyPatch,
54+
) -> None:
55+
(tmp_path / "sqlbuild_project.toml").write_text('name = "orders"\nadapter = "duckdb"\n')
56+
model: Path = tmp_path / "models" / "orders.sql"
57+
model.parent.mkdir()
58+
model.write_text(
59+
'MODEL (description "Orders");\nSELECT 1 AS order_id UNION SELECT 2 AS order_id\n'
60+
)
61+
monkeypatch.setenv(COMPILER_ENGINE_ENV_VAR, test_case.engine)
62+
original, passes = record_fix_sessions(monkeypatch)
63+
64+
exit_code: int = main(["--project-dir", str(tmp_path), "format", "--fix", "--json"])
65+
66+
capsys.readouterr()
67+
assert exit_code == 0
68+
assert tuple(original) == test_case.expected_original_sessions
69+
assert frozenset(passes) == test_case.expected_fix_pass_sessions
70+
71+
3472
@pytest.mark.parametrize(
3573
"test_case",
3674
[FormatCompileIntegrationTestCase("bare union", "UNION DISTINCT")],

0 commit comments

Comments
 (0)