Skip to content

fix(compiler)!: name the namespace of a mutation target - #1101

Open
lab1207 wants to merge 3 commits into
ModernRelay:mainfrom
lab1207:fix/edge-type-namespace-shadow
Open

lab1207 wants to merge 3 commits into
ModernRelay:mainfrom
lab1207:fix/edge-type-namespace-shadow

Conversation

@lab1207

@lab1207 lab1207 commented Oct 20, 2026

Copy link
Copy Markdown
Contributor

Closes #921.

The problem

A schema can declare a node type and an edge type with the same name — the two live in separate namespaces, and the catalog only refuses duplicates within one kind:

node Link {}
edge Link: Link -> Link

The compiler always resolved a mutation's target to the node type when both namespaces contained the name, so the edge could never be mutated:

insert Link { from: "a", to: "a" }

failed with

type error: T11: type `Link` has no property `from`

delete had the same defect — delete Link where @src = "a" also selected the node type and failed on the @src meta-field. Loading and reading those edges worked, and so did inserting the node type, which is what made the failure look like a property bug rather than a name-resolution one.

The fix

A mutation may now name the namespace it targets, using the spelling load csv already uses:

insert node:Person { … }
insert edge:Knows { … }
update node:Person where …
delete edge:Knows where …
  • the grammar takes the namespace on the mutation target (query.pest),
  • the parser carries it through as MutationKind::Node / MutationKind::Edge on insert, update and delete (ast.rs),
  • the type checker resolves the target in the named namespace (typecheck.rs).

A bare name that both namespaces declare is now refused with a new code, T60 — "a mutation target names a type in both the node and the edge namespace" — instead of silently picking the node type. A statement that does not say which one it means should not quietly mean one of them; that is the only case where the old behaviour was wrong silently.

This is a breaking change for schemas that use a shared name, which is why it ships with changelog.d/mutation-target-namespace.breaking.md. A target that only one namespace declares keeps working unqualified, so queries are unaffected unless the schema really does declare the name twice.

Test

  • crates/omnigraph-compiler/src/query/typecheck_tests.rs — the existing same-named-namespace test now spells insert node:Shared, and a new test covers insert edge:Shared { from: "a", to: "b" } resolving to MutationTarget::Edge. A second new test pins the refusal: the bare spelling of a name both namespaces declare is rejected.
  • crates/omnigraph-compiler/src/query/lint_tests.rs — the lint-level test now uses the qualified spelling.
  • crates/omnigraph-gqt/cases/issue_921_shared_node_and_edge_mutation_target.gqt — an end-to-end case in the repository's own GQT format: the schema from the issue, the seed from the issue, the mutation, and expect affected: nodes=0 edges=1.

Note on validation

I did not build or run the test suite locally: CI on this pull request is the judge. The change is contained in the compiler crate plus the GQT case, and it adds no new dependency and no new process or filesystem lifecycle.

Model Used

MiniMax M3.1 Flash Free via the opencode agent harness (minimax-m3.1-flash-free)

A node type and an edge type may share a name, and the type checker always
selected the node namespace, so \insert Link {from: ..., to: ...}\ was
refused with \T11: type 'Link' has no property 'from'\ and an edge of a
shared name stayed unreachable through GQ.

A mutation target now names its namespace, \
ode:Person\ or \edge:Knows\,
the spelling \load csv\ already uses, and one resolver serves insert,
update and delete. A bare name still resolves in the namespace that
declares it; a bare name both namespaces declare is refused with \T60\
instead of silently selecting one of them.
@lab1207 lab1207 changed the title feat(query): let a mutation name the namespace it targets fix(compiler)!: name the namespace of a mutation target Oct 20, 2026

ragnorc commented Oct 20, 2026

Copy link
Copy Markdown
Contributor

Request changes.

A node type and an edge type may share a name. The two names live in separate namespaces. A mutation must say which namespace it means. This change adds that spelling, node:Person and edge:Knows, the same spelling load csv already uses. One resolver serves insert, update, and delete. A bare name that both namespaces declare is refused with T60. A bare name that only one namespace declares still resolves as before.

This is the layer that owns the contract. Invariant 9 says the compiler selects the node or edge target, and execution keeps that choice. Execution already carries MutationTarget and writes the table key node:<name> or edge:<name> from it. The defect was that the type checker always selected the node namespace, so the edge arm never ran for a shared name. The new resolver closes that path for all three statements.

Do not merge this head. The compiler crate does not build, and the user-guide length check fails. The resolution behavior is correct once the crate builds. I proved that with a one-token local compile fix that only changes an unreachable panic format. I restored that edit. It is not in this pull request.

Blocking

  1. parse_mutation_target formats a Rule with {other}. Rule has no Display implementation. The other matches in this file use {other:?}. cargo check -p omnigraph-compiler --locked fails with E0277 at parser.rs:1175. The same error failed Clippy, Test Workspace, and GQT dst-clippy on commit 411ff74e.

  2. docs/user/mutations/index.md is 364 lines. scripts/check-docs.py refuses an authored user page over 350 lines unless the page contains docs-check: allow-long-page. Main is 350 lines. The Check AGENTS.md Links job on 411ff74e failed with "authored user page exceeds 350 lines". Shorten the page by at least 14 lines, or add that exemption if a longer page is accepted.

Optional

mutation_target_refuses_a_name_missing_from_its_namespace_issue_921 inserts edge:Label. Label is in neither namespace of that catalog, so the test does not show that a qualified name ignores the other namespace. Point it at a node-only name and an edge-only name. I ran that pair in a temporary case. insert edge:OnlyNode returns T4. insert node:OnlyEdge returns T1.

The embedded skill still shows only the bare insert form. The T60 text already names both spellings, so a caller can correct the statement. A line in skills/omnigraph/references/queries.md would match the user guide.

Tradeoff and liability

The reported statement insert Link { from: "a", to: "a" } returns T60. It does not write the edge. That follows the accepted triage on issue 921. A node may declare properties named from and to. Choosing the namespace from those assignments can retarget a stored query when the schema gains the other kind. The refusal fails at check time and names both spellings.

The break is limited to a schema that declares one name in both namespaces. A bare mutation of a name that exists in only one namespace stays valid. The changelog and the mutation guide say who must change a query. There is no storage, protocol, or manifest change. The change is reversible.

The resolver replaces three node-then-edge lookups with one function. Later kinds, and the schema-namespace names from open RFC #1015 (node:crm::Contact), stay in that function. type_name does not accept :: yet. That extension belongs with the namespace work. The node: / edge: prefix uses one colon and matches the load csv spelling that RFC already uses.

Lance does not own this name. The compiler resolves the target before the engine opens a dataset. I read the insert, update, and delete arms in crates/omnigraph/src/exec/mutation.rs. They use the compiler's MutationTarget and do not search the other namespace.

Validation

Before, main d4aa7fe9, clean target /tmp/target-main, sources under /workspace:

CARGO_TARGET_DIR=/tmp/target-main cargo run -p omnigraph-gqt --locked --offline --bin omnigraph-gqt -- /tmp/issue_921_before.gqt

FAIL in 0.63s. type error: T11: type Linkhas no propertyfrom``. The case is the report from issue 921 and expects one edge. The mutation is refused, so no edge is written. The compiler crate compiled for that run is /workspace/crates/omnigraph-compiler.

After, head fe8ac72c, with only the local {other:?} change so the crate builds:

CARGO_TARGET_DIR=/workspace/target cargo test -p omnigraph-gqt --locked --offline --test gq_logic_tests issue_921 -- --test-threads=4

case::issue_921_shared_node_and_edge_mutation_target.gqt ok in 0.66s. insert edge:Link affects nodes=0 edges=1. The same case inserts the node, refuses the bare name with T60, and deletes the edge.

A temporary case on that same build, deleted afterward, passed in 0.74s. It covers update node:Link, T16 for update edge:Link, T60 for a bare update and a bare delete, delete of the node, a bare insert of a name that exists in only one namespace, and a qualified name that exists only in the other namespace. On that build the reported statement insert Link { from: "a", to: "a" } returns T60.

cargo test -p omnigraph-compiler --locked --offline --lib issue_921 : 4 passed.

cargo clippy -p omnigraph-compiler --all-targets --locked --offline -- -D warnings -W clippy::dbg_macro passes with the same local fix.

The committed head does not compile, so these runs are not a green result for fe8ac72c itself.

CI on 411ff74e: E0277 at parser.rs:1175 in Clippy, Test Workspace, and GQT dst-clippy. The documentation check failed on the 350-line limit. GQT ordinary was cancelled. CI, DST, and GQ Logic Tests on fe8ac72c are action_required and did not run. Fix Regression Gate passed. That gate checks that the pull request names a regression. It does not run the test.

I did not run DST, S3, or Azure. This change does not touch storage or deployment.

Fix the format string and the page length. The design can merge after those two.

Inline comments that could not be attached:

  • crates/omnigraph-compiler/src/query/parser.rs:1175: Rule has no Display implementation. {other} does not compile. The other matches in this file use {other:?}. cargo check -p omnigraph-compiler --locked fails here with E0277. This is the error that failed Clippy and Test Workspace on 411ff74e.
  • docs/user/mutations/index.md:39: This page is 364 lines. scripts/check-docs.py refuses an authored user page over 350 lines unless the page contains docs-check: allow-long-page. Main is 350 lines. The Check AGENTS.md Links job failed on this text. Shorten the page by at least 14 lines, or add that exemption if a longer page is accepted.
  • crates/omnigraph-compiler/src/query/typecheck_tests.rs:246 (optional): Label is in neither namespace of this catalog, so this assertion does not show that a qualified name ignores the other namespace. Point a case at a node-only name and an edge-only name. I ran that pair. insert edge:OnlyNode returns T4. insert node:OnlyEdge returns T1.

`Rule` has no `Display` implementation, so the arm that formats it
with `{other}` did not compile. Use the Debug form, as every other match
in the file does.
@lab1207

lab1207 commented Oct 21, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the precise review — both blockers are fixed, and the optional point was a good catch.

1. The build. parse_mutation_target formatted a Rule with {other}, and Rule has no Display implementation. It now uses the Debug form, as every other match in the file does:

other => unreachable!("grammar: mutation_target holds no {other:?}"),

2. The user page length. docs/user/mutations/index.md is now 348 lines, inside the 350-line cap, with no fact dropped. The namespace paragraph is now the compact form that matches how the page cites other error codes (T14, T44, T45):

A node type and an edge type may share a name, so a mutation names the
namespace it targets: `node:Person` or `edge:Knows`, the spelling
[`load csv`](#csv-loading) already uses. A qualified name resolves in that
namespace only; a bare name both namespaces declare is refused (`T60`).

The lines came from re-wrapping two paragraphs that were raggedly wrapped, not from removing content. The schema guide already states the namespace rule and points here for the mutation spelling, so the two pages now agree.

3. The optional point. mutation_target_refuses_a_name_missing_from_its_namespace_issue_921 used edge:Label, a name in neither namespace, so it did not show that a qualified name ignores the other one. It now uses a catalog with a node-only type and an edge-only type, and checks both directions:

  • edge:OnlyNode → T4: unknown edge type \OnlyNode``
  • node:OnlyEdge → T1: unknown node type \OnlyEdge``

Same limit as the other branch: no cached crates.io dependencies here, so cargo check and cargo test are CI's to run. The Rust changes are the one-character format fix and the test above.

ragnorc commented Oct 21, 2026

Copy link
Copy Markdown
Contributor

Re-review of bfc87aaa. The two blockers from the last review are fixed. I recommend merge. Two optional notes remain.

A node type and an edge type may share a name. The two names live in separate namespaces. A mutation must say which namespace it means when both declare the name. This change adds that spelling, node:Person and edge:Knows, the same spelling load csv already uses. One resolver serves insert, update, and delete. A bare name that both namespaces declare is refused with T60. A bare name that only one namespace declares still resolves as before.

This is the layer that owns the contract. Invariant 9 says the compiler selects the node or edge target, and execution keeps that choice. Execution already carries MutationTarget and writes the table key node:<name> or edge:<name> from it. The defect was that the type checker always selected the node namespace, so the edge arm never ran for a shared name. The resolver closes that path for all three statements.

Earlier findings

  1. Resolved. parse_mutation_target formatted a Rule with {other}. Rule has no Display implementation. The arm now uses {other:?}, as the other matches in this file do. cargo test -p omnigraph-compiler --locked --offline --lib issue_921 compiles and passes. cargo clippy -p omnigraph-compiler --all-targets --locked --offline -- -D warnings -W clippy::dbg_macro passes.

  2. Resolved. docs/user/mutations/index.md is 348 lines. The cap is 350. python3 scripts/check-docs.py reports Documentation OK on this head (360 Markdown files). A test merge onto current main (235cd3b9) auto-merges. The merged page is 329 lines. It keeps the T60 paragraph and the text perf(load): fan out a csv overwrite's Lance writers over file-sized chunks #1121 moved on main. I aborted that merge. It is not on the branch.

  3. Resolved. mutation_target_refuses_a_name_missing_from_its_namespace_issue_921 now uses a node-only type and an edge-only type. insert edge:OnlyNode returns T4. insert node:OnlyEdge returns T1. The test passed.

  4. Open, optional. The embedded skill still shows only the bare insert form. The author did not change skills/omnigraph/references/queries.md. The T60 text already names both spellings, so a caller can correct the statement.

Blocking

None.

Optional

  1. Add one line to skills/omnigraph/references/queries.md under Insert, next to the bare insert Signal example. Say that a shared name needs node: or edge:, and that a bare name in both namespaces is T60. The file is not in this diff, so this note is not inline.

  2. The mutations page should say that a bare name in only one namespace still resolves. The new paragraph says a mutation names the namespace, then says a bare name both namespaces declare is refused. The examples above the paragraph stay bare. The changelog and the schema page already state the narrower rule. On fe8ac72c this page also had that sentence, the insert edge:Link { from, to } example, and the T60 sample. Those three are gone. The other paragraphs match main word for word. The reply said no fact was dropped. That is not accurate for this paragraph. The page is still inside the cap, and a merge with current main leaves more room.

Tradeoff and liability

The reported statement insert Link { from: "a", to: "a" } returns T60. It does not write the edge. That follows the accepted triage on issue 921. A node may declare properties named from and to. Choosing the namespace from those assignments can retarget a stored query when the schema gains the other kind. The refusal fails at check time and names both spellings.

The break is limited to a schema that declares one name in both namespaces. A bare mutation of a name that exists in only one namespace stays valid. The changelog says who must change a query. There is no storage, protocol, or manifest change. The change is reversible.

The resolver replaces three node-then-edge lookups with one function. Later kinds, and the schema-namespace names from open RFC #1015 (node:crm::Contact), stay in that function. type_name does not accept :: yet. load csv uses the same type_name, so that extension is one grammar change. The node: / edge: prefix uses one colon and matches the load csv spelling that RFC already uses.

Lance does not own this name. The compiler resolves the target before the engine opens a dataset. I read the insert, update, and delete arms in crates/omnigraph/src/exec/mutation.rs on this head. They use the compiler's MutationTarget and do not search the other namespace. A qualified edge update stays T16, which is the existing refusal.

Validation

Before, workspace e466a127. The mutation resolver is unchanged from there through current main 235cd3b9: typecheck.rs, parser.rs, and query.pest have an empty diff.

cargo run -p omnigraph-gqt --locked --offline --bin omnigraph-gqt -- /tmp/issue_921_before.gqt

The case is the report from issue 921 and expects one edge. FAIL in 0.59s. type error: T11: type Linkhas no propertyfrom``. The mutation is refused, so no edge is written.

The same binary on this pull request's case file fails before the first mutation: parse error: expected type_name. The committed case starts with insert edge:Link, which the old grammar does not accept. The T11 result above is the predicted failure of the reported statement.

After, head bfc87aaa, worktree of that commit, after a rebuild of omnigraph-compiler and omnigraph-engine from that tree:

cargo test -p omnigraph-compiler --locked --offline --lib issue_921 -- --test-threads=4

4 passed.

cargo clippy -p omnigraph-compiler --all-targets --locked --offline -- -D warnings -W clippy::dbg_macro

passes.

omnigraph-gqt crates/omnigraph-gqt/cases/issue_921_shared_node_and_edge_mutation_target.gqt

ok in 0.71s. insert edge:Link affects nodes=0 edges=1. The same case inserts the node, refuses the bare name with T60, and deletes the edge.

The reported statement, in a temporary case that expects T60, ok in 0.60s. A second temporary case, deleted afterward, ok in 0.65s. It covers update node:Shared, T16 for update edge:Shared, T60 for a bare update and a bare delete, a bare insert of a name that exists in only one namespace, a bare delete of an edge-only name, and a qualified name that exists only in the other namespace (edge:OnlyNode is T4, node:OnlyEdge is T1).

python3 scripts/check-docs.py on this head: Documentation OK (360 Markdown files checked).

CI on bfc87aaa: Fix Regression Gate passed. PR Title passed. Devin Review skipped (trial expired). CI, GQ Logic Tests, and DST are action_required and did not run. The gate checks that the pull request names a regression. It does not run the test.

I did not run DST, S3, Azure, the full workspace suite, or the full GQT corpus. This change does not touch storage or deployment. The server and the CLI use this compiler. I did not run those processes.

Inline comment that could not be attached, docs/user/mutations/index.md line 30: Add one sentence: a bare name that only one namespace declares still resolves. This paragraph can be read as a rule that every mutation must use node: or edge:. The examples just above stay bare. The changelog and the schema page already state the narrower rule. On fe8ac72c this page had that sentence. The 350-line cap still has room (348 lines on this head).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: inserting an edge whose type shares a node type's name fails

2 participants