Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -2696,11 +2696,23 @@ async def _plan_coverage_backfill(
The span is recorded beside the cube's other backfills and never asked for
twice, since availability does not move until the backfill lands. A branch
deploy asks for nothing: it previews what the push would give its author,
and a preview does not spend hundreds of partition runs.
and a preview does not spend hundreds of partition runs. It says so in
the report, so the missing backfill reads as a choice.
"""
coverage = block.coverage
partition = coverage_partition(revision)
if not coverage or partition is None or await self._is_branch_deploy():
if not coverage or partition is None:
return
if await self._is_branch_deploy():
self.deployed_results.append(
DeploymentResult(
name=revision.name,
deploy_type=DeploymentResult.Type.MATERIALIZATION,
status=DeploymentResult.Status.SKIPPED,
operation=DeploymentResult.Operation.NOOP,
message="no coverage backfill on a branch deploy",
),
)
return
span = coverage.span(datetime.now(UTC).date())
if span is None:
Expand Down
36 changes: 17 additions & 19 deletions datajunction-server/datajunction_server/internal/namespaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -286,35 +286,29 @@ def resolve_git_info_from_map(
None,
)

# Resolve config_ns: find the git root (has github_repo_path).
# If branch_ns.parent_namespace points outside the string hierarchy (a sibling),
# use the FK-hop parent if it was pre-loaded into ns_map.
# Otherwise, the git root is reachable via string ancestors.
config_ns: NodeNamespace | None = None
# Rows git config resolves from, most specific first.
# A pre-loaded FK parent outside the hierarchy leads.
candidates: list[NodeNamespace] = []
if (
branch_ns
and branch_ns.parent_namespace
and branch_ns.parent_namespace not in ancestor_names
and branch_ns.parent_namespace in ns_map
):
fk_parent = ns_map[branch_ns.parent_namespace]
if fk_parent.github_repo_path:
config_ns = fk_parent
if not config_ns:
config_ns = next(
(
ns_map[n]
for n in reversed_names
if ns_map.get(n) and ns_map[n].github_repo_path
),
None,
)
candidates.append(ns_map[branch_ns.parent_namespace])
candidates.extend(ns_map[n] for n in reversed_names if ns_map.get(n))

config_ns = next((ns for ns in candidates if ns.github_repo_path), None)

if not config_ns:
return None

branch = branch_ns.git_branch if branch_ns else None
default_branch = config_ns.default_branch
# The default branch may sit on another row.
default_branch = next(
(ns.default_branch for ns in candidates if ns.default_branch),
None,
)
# Effective git_only cascades: any ancestor (or the namespace itself)
# with git_only=True locks all descendants. Without this the UI would
# treat a child namespace as editable when its parent is locked,
Expand All @@ -329,7 +323,9 @@ def resolve_git_info_from_map(
"default_branch": default_branch,
"path": config_ns.git_path,
"is_default_branch": (
branch is None # root namespace — no branch means it IS the default
# No branch means default only when this namespace IS the git
# root itself — not merely when no ancestor has git_branch set.
(branch is None and config_ns.namespace == namespace)
or (default_branch is not None and branch == default_branch)
),
"parent_namespace": branch_ns.parent_namespace if branch_ns else None,
Expand Down Expand Up @@ -363,6 +359,8 @@ async def get_git_info_for_namespace(
load the git root (which carries ``github_repo_path`` / ``git_path``).
Otherwise, look for ``github_repo_path`` among the string ancestors
(self-contained root case).
3. Take ``default_branch`` from the first row that has one, in the same
order — branch namespaces carry the repo path but not the default branch.
"""
ancestor_names = get_parent_namespaces(namespace) + [namespace]
stmt = select(NodeNamespace).where(NodeNamespace.namespace.in_(ancestor_names))
Expand Down
48 changes: 48 additions & 0 deletions datajunction-server/tests/api/namespaces_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -3690,6 +3690,54 @@ async def test_hard_delete_git_default_branch_namespace_blocked(
assert "default branch" in response.json()["message"]


@pytest.mark.asyncio
async def test_delete_default_branch_owning_repo_blocked(
module__client_with_all_examples: AsyncClient,
) -> None:
"""
A branch namespace that carries its own ``github_repo_path`` -- the shape
branch creation writes -- still resolves its repo's default branch from the
git root, so both deletes are refused.
"""
root = "ownrepo.root"
branch_ns = "ownrepo.root.main"

await module__client_with_all_examples.post(f"/namespaces/{root}/")
await module__client_with_all_examples.post(f"/namespaces/{branch_ns}/")
await module__client_with_all_examples.patch(
f"/namespaces/{root}/git",
json={"github_repo_path": "corp/ownrepo", "default_branch": "main"},
)

# Branch creation copies the repo path onto the branch namespace.
await module__client_with_all_examples.patch(
f"/namespaces/{branch_ns}/git",
json={"github_repo_path": "corp/ownrepo"},
)
await module__client_with_all_examples.patch(
f"/namespaces/{branch_ns}/git",
json={"parent_namespace": root, "git_branch": "main"},
)

refusal = (
f"Cannot delete namespace `{branch_ns}`: it is the default branch "
"of a git-backed namespace (corp/ownrepo). "
"Only non-default branch namespaces can be deleted."
)

response = await module__client_with_all_examples.delete(
f"/namespaces/{branch_ns}/",
)
assert response.status_code == 422
assert response.json()["message"] == refusal

response = await module__client_with_all_examples.delete(
f"/namespaces/{branch_ns}/hard/",
)
assert response.status_code == 422
assert response.json()["message"] == refusal


@pytest.mark.asyncio
async def test_delete_non_default_git_branch_namespace_allowed(
module__client_with_all_examples: AsyncClient,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4184,7 +4184,8 @@ async def test_a_branch_deploy_asks_for_nothing(
):
"""
A branch namespace previews what the push would give its author, and a
preview does not spend hundreds of partition runs.
preview does not spend hundreds of partition runs. The report says the
backfill was skipped, so the author is not left guessing.
"""
revision, materialization = await self._cube(
session,
Expand All @@ -4203,6 +4204,15 @@ async def test_a_branch_deploy_asks_for_nothing(

assert self._queued(orchestrator) == []
assert await self._recorded(session, materialization) == []
assert orchestrator.deployed_results == [
DeploymentResult(
name="default.a_cube",
deploy_type=DeploymentResult.Type.MATERIALIZATION,
status=DeploymentResult.Status.SKIPPED,
operation=DeploymentResult.Operation.NOOP,
message="no coverage backfill on a branch deploy",
),
]

@pytest.mark.asyncio
async def test_an_uncountable_window_is_warned_about(
Expand Down
133 changes: 133 additions & 0 deletions datajunction-server/tests/internal/git/test_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -801,6 +801,31 @@ async def test_is_default_branch_true_when_no_git_branch(
assert result["branch"] is None
assert result["is_default_branch"] is True

@pytest.mark.asyncio
async def test_is_default_branch_false_for_plain_child_of_git_root(
self,
session: AsyncSession,
):
"""A plain child namespace (no git_branch of its own) under a git root is
NOT the default branch — only the git root itself gets that treatment when
no git_branch is found anywhere in the ancestor chain."""
session.add(
NodeNamespace(
namespace="proj",
github_repo_path="org/repo",
default_branch="main",
),
)
session.add(NodeNamespace(namespace="proj.stale_child"))
await session.commit()

result = await get_git_info_for_namespace(session, "proj.stale_child")

assert result is not None
assert result["branch"] is None
assert result["git_root_namespace"] == "proj"
assert result["is_default_branch"] is False

@pytest.mark.asyncio
async def test_is_default_branch_false_when_no_default_branch(
self,
Expand Down Expand Up @@ -828,6 +853,114 @@ async def test_is_default_branch_false_when_no_default_branch(
assert result is not None
assert result["is_default_branch"] is False

@pytest.mark.asyncio
async def test_default_branch_from_ancestor_row(self, session: AsyncSession):
"""The branch namespace owns the repo path; its parent owns default_branch."""
session.add(
NodeNamespace(
namespace="shop.metrics",
github_repo_path="corp/examplerepo",
default_branch="main",
git_path="defs/",
),
)
session.add(
NodeNamespace(
namespace="shop.metrics.main",
github_repo_path="corp/examplerepo",
git_branch="main",
git_path="defs/",
parent_namespace="shop.metrics",
),
)
await session.commit()

result = await get_git_info_for_namespace(session, "shop.metrics.main")

assert result == {
"repo": "corp/examplerepo",
"branch": "main",
"default_branch": "main",
"path": "defs/",
"is_default_branch": True,
"parent_namespace": "shop.metrics",
"git_only": False,
"git_root_namespace": "shop.metrics.main",
"branch_namespace": "shop.metrics.main",
}

@pytest.mark.asyncio
async def test_feature_branch_with_own_repo(self, session: AsyncSession):
"""A feature branch owning the repo path is not the default branch."""
session.add(
NodeNamespace(
namespace="shop.metrics",
github_repo_path="corp/examplerepo",
default_branch="main",
git_path="defs/",
),
)
session.add(
NodeNamespace(
namespace="shop.metrics.featureone",
github_repo_path="corp/examplerepo",
git_branch="test_cube",
git_path="defs/",
parent_namespace="shop.metrics",
),
)
await session.commit()

result = await get_git_info_for_namespace(session, "shop.metrics.featureone")

assert result == {
"repo": "corp/examplerepo",
"branch": "test_cube",
"default_branch": "main",
"path": "defs/",
"is_default_branch": False,
"parent_namespace": "shop.metrics",
"git_only": False,
"git_root_namespace": "shop.metrics.featureone",
"branch_namespace": "shop.metrics.featureone",
}

@pytest.mark.asyncio
async def test_default_branch_via_fk_parent(self, session: AsyncSession):
"""The git root is reached by the FK hop, not by name."""
session.add(
NodeNamespace(
namespace="roots.repo",
github_repo_path="org/repo",
default_branch="main",
git_path="defs/",
),
)
session.add(
NodeNamespace(
namespace="branches.main",
github_repo_path="org/repo",
git_branch="main",
git_path="defs/",
parent_namespace="roots.repo",
),
)
await session.commit()

result = await get_git_info_for_namespace(session, "branches.main")

assert result == {
"repo": "org/repo",
"branch": "main",
"default_branch": "main",
"path": "defs/",
"is_default_branch": True,
"parent_namespace": "roots.repo",
"git_only": False,
"git_root_namespace": "roots.repo",
"branch_namespace": "branches.main",
}

# ------------------------------------------------------------------
# parent_namespace
# ------------------------------------------------------------------
Expand Down
Loading