Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/sqlbuild/cli/commands/_helpers/entry/dispatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,9 @@
def dispatch_cli_command(*, args: CliNamespace, handlers: CliEntrypointHandlers) -> int:
"""Route a parsed CLI namespace to its command handler and return its exit code."""

project_dir: Path | None = None if args.project_dir is None else Path(args.project_dir)
project_dir: Path | None = (
None if args.project_dir is None else Path(args.project_dir).resolve()
)
effective_project_dir: Path = project_dir if project_dir is not None else Path.cwd()
selector_inputs: SelectorInputs = read_selector_file_inputs(args.select_file)
select: tuple[str, ...] = (*tuple(args.select), *selector_inputs.selectors)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -283,17 +283,30 @@ def edit_source(
insertion_lines.extend(_yaml_column_lines(column=column, indent=source_indent + 4))
lines[source_end:source_end] = insertion_lines
return "".join(lines), None
columns_indent: int = _indent(lines[columns_index])
detected_column_indent: int | None = _yaml_sequence_indent(
lines=lines,
start=columns_index + 1,
end=source_end,
key_indent=columns_indent,
)
column_indent: int = (
detected_column_indent if detected_column_indent is not None else columns_indent + 2
)
columns_end: int = columns_index + 1
while columns_end < source_end:
stripped: str = lines[columns_end].strip()
if (
stripped
and not stripped.startswith("#")
and _indent(lines[columns_end]) <= source_indent + 2
):
break
if stripped and not stripped.startswith("#"):
line_indent: int = _indent(lines[columns_end])
if line_indent < columns_indent or (
line_indent == columns_indent
and not (
column_indent == columns_indent
and _yaml_list_name(lines[columns_end]) is not None
)
):
break
columns_end += 1
column_indent: int = source_indent + 4
column_blocks: dict[str, tuple[int, int]] = {}
index: int = columns_index + 1
while index < columns_end:
Expand Down Expand Up @@ -400,6 +413,19 @@ def _yaml_key(line: str) -> str | None:
return stripped.split(_YAML_KEY_SEPARATOR, maxsplit=1)[0].strip()


def _yaml_sequence_indent(*, lines: list[str], start: int, end: int, key_indent: int) -> int | None:
for index in range(start, end):
stripped: str = lines[index].strip()
if not stripped or stripped.startswith("#"):
continue
line_indent: int = _indent(lines[index])
if _yaml_list_name(lines[index]) is not None and line_indent >= key_indent:
return line_indent
if line_indent <= key_indent:
return None
return None


def _yaml_comment(line: str) -> str:
marker: int = line.find(" #")
if marker < 0:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,14 @@ class ContractNullabilityCompileIntegrationTestCase:
expected_diagnostics: tuple[str, ...]


@dataclass(frozen=True)
class ProjectDirectoryCompileIntegrationTestCase:
"""One project-directory normalization expectation through compile."""

description: str
expected_exit_code: int


@dataclass(frozen=True)
class FormatCompileIntegrationTestCase:
"""One format-to-compile compatibility expectation."""
Expand Down
44 changes: 44 additions & 0 deletions tests/integration/src/sqlbuild/cli/commands/main/test_compile.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from sqlbuild.cli.commands.main.entrypoint.entry import main
from tests.integration.src.sqlbuild.cli.commands.main._test_types import (
ContractNullabilityCompileIntegrationTestCase,
ProjectDirectoryCompileIntegrationTestCase,
SnowflakeCompileIntegrationTestCase,
)

Expand Down Expand Up @@ -112,3 +113,46 @@ def test_given_nullable_output_when_compiling_contract_then_only_schema_nullabil

assert exit_code == test_case.expected_exit_code
assert tuple(item["code"] for item in diagnostics) == test_case.expected_diagnostics


@pytest.mark.parametrize(
"test_case",
(
ProjectDirectoryCompileIntegrationTestCase(
description="relative project directory preserves private macro scope",
expected_exit_code=0,
),
),
ids=lambda case: case.description,
)
def test_given_relative_project_directory_when_compiling_then_private_macro_is_visible(
test_case: ProjectDirectoryCompileIntegrationTestCase,
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
capsys: pytest.CaptureFixture[str],
) -> None:
project_dir: Path = tmp_path / "project"
(project_dir / "models" / "orders" / "_sqlbuild" / "_macros").mkdir(parents=True)
(project_dir / "sqlbuild_project.toml").write_text(
'name = "orders"\nadapter = "duckdb"\n', encoding="utf-8"
)
(project_dir / "models" / "orders" / "_sqlbuild" / "_macros" / "order_id.py").write_text(
'def order_id() -> str:\n return "CAST(1 AS INTEGER)"\n', encoding="utf-8"
)
(project_dir / "models" / "orders" / "orders.sql").write_text(
"MODEL (materialized table);\nSELECT @order_id() AS order_id\n",
encoding="utf-8",
)
monkeypatch.chdir(tmp_path)

exit_code: int = main(["--project-dir", project_dir.name, "compile", "--json", "--no-cache"])
result: dict[str, object] = json.loads(capsys.readouterr().out)
summary: dict[str, object] = cast(dict[str, object], result["summary"])

assert exit_code == test_case.expected_exit_code
assert summary["errors"] == 0
assert summary["models"] == 1


if __name__ == "__main__":
pytest.main([__file__, "-vv"])
Original file line number Diff line number Diff line change
Expand Up @@ -534,5 +534,113 @@ def test_given_unresolved_additive_type_conflict_when_writing_then_file_is_not_r
assert model_path.read_text(encoding="utf-8") == original


@pytest.mark.parametrize(
"test_case",
[
ContractCommandIntegrationTestCase(
description="relative project directory preserves private macro scope",
expected_exit_code=1,
)
],
ids=lambda case: case.description,
)
def test_given_relative_project_directory_when_diffing_contract_then_private_macro_is_visible(
test_case: ContractCommandIntegrationTestCase,
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
capsys: pytest.CaptureFixture[str],
) -> None:
project_dir: Path = tmp_path / "project"
project_dir.mkdir()
_ = prepare_contract_project(project_dir)
root_model_path: Path = project_dir / "models" / "orders.sql"
root_model_path.unlink()
model_path: Path = project_dir / "models" / "orders" / "orders.sql"
model_path.parent.mkdir()
_ = model_path.write_text(
"MODEL (materialized table);\nSELECT @order_id() AS id, CAST('a' AS VARCHAR) AS name\n",
encoding="utf-8",
)
macro_path: Path = model_path.parent / "_sqlbuild" / "_macros" / "order_id.py"
macro_path.parent.mkdir(parents=True)
_ = macro_path.write_text(
'def order_id() -> str:\n return "CAST(1 AS INTEGER)"\n', encoding="utf-8"
)
monkeypatch.chdir(tmp_path)

exit_code: int = main(
[
"--no-color",
"--project-dir",
project_dir.name,
"contract",
"diff",
"--from",
"prod",
"--select",
"orders",
]
)

captured: CaptureResult[str] = capsys.readouterr()
assert exit_code == test_case.expected_exit_code
assert "model:orders" in captured.out
assert "inaccessible" not in captured.err


@pytest.mark.parametrize(
"test_case",
[
ContractCommandIntegrationTestCase(
description="indentless source column sequence receives physical types",
expected_exit_code=0,
)
],
ids=lambda case: case.description,
)
def test_given_indentless_source_columns_when_generating_then_preserves_metadata_and_adds_types(
test_case: ContractCommandIntegrationTestCase,
tmp_path: Path,
capsys: pytest.CaptureFixture[str],
) -> None:
_ = prepare_contract_project(tmp_path)
source_path: Path = tmp_path / "sources" / "raw.yml"
_ = source_path.write_text(
"""sources:
- name: raw_orders
schema: raw
table: orders
description: keep source
columns:
- name: id
description: identifier
""",
encoding="utf-8",
)

exit_code: int = main(
[
"--no-color",
"--project-dir",
str(tmp_path),
"contract",
"generate",
"--from",
"prod",
"--select",
"source:raw_orders",
"--write",
]
)

source_yaml: str = source_path.read_text(encoding="utf-8")
assert exit_code == test_case.expected_exit_code
assert "0 contract difference(s)" in capsys.readouterr().out
assert "description: keep source" in source_yaml
assert "description: identifier" in source_yaml
assert " - name: id\n type: 'BIGINT'" in source_yaml
assert " - name: status\n type: 'VARCHAR'" in source_yaml


if __name__ == "__main__":
pytest.main([__file__, "-vv"])
Loading