Repository navigation
fix(query): 4xx for unknown graphs and commit refs; urn:default names the default graph - #2005
Conversation
…:default names the default graph
Mistakes a client makes naming a dataset or a commit reached it as a 500
InternalError. They are now InvalidQuery, a 400:
- a default-graph-uri, named-graph-uri or FROM graph the ledger does not
hold ("Unknown named graph"), on the ledger and connection routes, and
the embedded within-ledger dataset errors
- a commit pin that passes the address grammar but cannot be resolved: a
prefix too short once sha256: is stripped, an abbreviated CID, a prefix
too long, or one matching no commit or several. This covers the ledger
path, SPARQL FROM, JSON-LD from/at and branch create's at.
The commit errors are not NotFound, because load_view_from_source takes a
NotFound from db_at to mean the source is not a ledger.
urn:default, the name ledger info lists the default graph under, now
names the default graph wherever a graph within a ledger is addressed:
FROM / FROM NAMED and the protocol dataset parameters on both routes,
ledger#urn:default, and JSON-LD graph selectors.
A ledger address still requires its time-spec tag, since a bare 123456
is both a t and a commit prefix. Refusing an untagged spec now names the
tagged spelling it most likely meant: @commit:<id>, @t:<N> or @time:<ts>.
aaj3f
left a comment
There was a problem hiding this comment.
This is really good cleanup, @bplatz -- FWIW, I think you and I were running into similar issues and investigating them from different symptoms. With that in mind, please do take a look at #2007 (especially if you introduce any new commits). I'll take the work to rebase (and semantically/logically adapt) to #2002, #2004, and this PR before merging #2007, but we should all just be clear-eyed about the alignment we're working towards across all of them. Full review below:
Caller mistakes in dataset and commit naming have been 500s for a long time, and retyping them at the source (select_graph, normalize_commit_ref, time_resolve, the within-ledger dataset errors) means SPARQL, JSON-LD and the embedded API all report the same 400 without any translation layer. The "did you mean @commit:…" hint is a lovely touch.
This PR got no CI (stacked base), so I ran clippy -D warnings and fmt --check on this head, and the tests on a trial merge with origin/main, which now carries db#1997. The merge is clean, all of #1997's new tests and all of this stack's pass on it, and so does the W3C testsuite-sparql. The two PRs don't conflict semantically either: an unknown graph in a query dataset is a 400 here, an unknown USING / WITH graph in an update reads empty there, both fail closed, and JSON-LD matches SPARQL on each surface.
The two that need to land first:
fluree-db-core/src/graph_registry.rs:44—urn:defaultnames the default graph in dataset positions only. On a trial merge withmain,USING <urn:default>reads empty andWITH <urn:default> INSERTwrites a hidden named graph thatFROM <urn:default>can't see. Fix path: makeir::names_ledgerand theGRAPHalias check honor the name, or refuse it as a write target and narrow the comment.fluree-db-server/tests/sparql_protocol_dataset_params.rs:307— agree with #2007 (mine, still a draft) on the unknown-graph status before this lands. This PR makes "a dataset clause names a graph the ledger doesn't have"400 err:db/InvalidQuery; #2007 makes the same case404 err:db/GraphNotFound. I merged your stack with #2007 on top locally, anda_graph_the_ledger_does_not_hold_is_a_400is the one server test that fails (it gets the 404). There's no merge that keeps both, and whichever lands before #2000 is what 4.2.3 ships. Details and my lean inline.
The rest:
fluree-db-api/src/ledger_view.rs:524— theNotFoundbehind branch create's mislabel is shared with commit show and revert; one variant fixes all three.
Two smaller overlaps with #2007, neither of which needs anything from you:
urn:default: #2007 replaces three of the four places this PR teaches it (dataset.rs:458,fluree_ext.rs:63,view/query.rs:1258) with one keyword table,GraphSel::keywordinfluree-db-core/src/dataset_ref.rs. I tried it as the single arm"default" | DEFAULT_GRAPH_IRIthere, and with that one line bothurn_default_names_the_default_graphandsparql_within_ledger_from_urn_default_scopes_default_graphpass on the merged tree, so I'll carry it when I rebase.- The 400 you added to
sparql_within_ledger_from_alias_spelling_mismatch_is_rejected(it_query_dataset.rs:2544): #2007 retires that test, because it makesFROM <wl>onwl:mainread the default graph. I'll take that on the rebase too.
The order I'm proposing is #2002 → #2004 → this → #2007 rebased on top, with the port on me. Happy to talk it through if you'd sequence it differently.
Adherence to repo commitments:
- Patterns/abstractions: ✔ one constant for
urn:default, errors typed where they arise;⚠️ the name is now resolved in several query-side places (fluree_ext.rs,view/query.rs,dataset.rs,ledger_info.rs) and in none on the update side. - Performance (speed first, memory second): ✔ error typing and one string comparison in dataset resolution; no performance-degradation risk.
- Deployment targets: ✔ API and core changes reach every host, and a 500 → 400 change is an improvement on each; nothing host-specific.
- Testing: ✔ each new server test covers both routes and both protocol parameters plus the JSON-LD twin, and breaking the unknown-graph retype turns the 400 test red;
⚠️ no update-sideurn:defaultcase, and no CI on this head. - Conventions: ✔ thorough commit body; endpoint and dataset docs updated.
Verified locally: clippy -D warnings on fluree-db-core / -api / -server, fmt --check, and the core untagged_spec_is_refused_with_its_tagged_spelling at this head. On the trial merge: sparql_protocol_dataset_params (22), query_path_time_pin (15), sparql_dataset_semantics (18), it_query_dataset (75), it_commit_identity (2), it_named_graphs (51), it_trig_insert (17), it_sql_pushdown_lane (19), and testsuite-sparql (all 36 suites).
Approving now so you can merge without waiting on another pass from me — just be sure both are in before you do.
| pub const FIRST_USER_GRAPH_ID: GraphId = 3; | ||
|
|
||
| /// The name ledger info gives the default graph, which has no IRI of its own. | ||
| /// Anywhere a graph within a ledger is addressed, it names the default graph. |
There was a problem hiding this comment.
🔴 Must address before merge: "anywhere a graph within a ledger is addressed, it names the default graph" holds for dataset positions only, and on main a write can now put data under urn:default where nothing that reads urn:default as the default graph will see it.
This PR makes urn:default the default graph in FROM / FROM NAMED, the protocol parameters, ledger#urn:default and the JSON-LD selectors. Everywhere else it's still an ordinary IRI, and since db#1997 landed on main (USING / WITH of an unknown graph reads an empty graph), that splits one name four ways. On a trial merge of this branch with origin/main, with Alice in the default graph:
SELECT … FROM <urn:default>→Alice(this PR)DELETE { ?s ex:name ?n } USING <urn:default> WHERE { ?s ex:name ?n }→ deletes nothing (fix: unresolvable graph references fail closed, and TriG directives apply in document order #1997: unknownUSINGgraph reads empty)WITH <urn:default> INSERT { ex:zed ex:name "Zed" } WHERE { }→ Zed lands in a named graph calledurn:default: the default graph andFROM <urn:default>don't see it,GRAPH <urn:default>does, and per your note/infohides itWITH <ledger-id> INSERT …→ the default graph (fix: unresolvable graph references fail closed, and TriG directives apply in document order #1997'sir::names_ledger)
So someone who reads the new docs and writes WITH <urn:default> gets their data put somewhere hidden, silently. Your "Not changed" list already names the GRAPH <urn:default> and write cases; I'd fold them in here rather than leave them, since this constant's comment is the invariant they contradict.
Either direction is small: teach ir::names_ledger (fluree-db-transact/src/ir.rs:608, after a rebase onto main) and the GRAPH alias check (ExecutionContext::single_db_user_graph_id and the is_alias test in GraphOperator) the same name, so urn:default means the default graph everywhere; or refuse urn:default as a write target and narrow this comment to the positions that honor it. Commenting here because fluree-db-transact/src/ir.rs and fluree-db-query/src/graph.rs are not in this diff.
There was a problem hiding this comment.
Addressed in 65a0e48
Took both halves: urn:default reads as the default graph in every position that reads a graph or picks a default graph (USING, USING NAMED, WITH, GRAPH in a query or an update's WHERE, and the JSON-LD twins), and a write that names a graph by it is a 400 (GRAPH templates and quads, TriG, import, graph management, sync).
| _ => { | ||
| let ids: Vec<_> = matches.iter().map(|h| &h[..7.min(h.len())]).collect(); | ||
| Err(ApiError::query(format!( | ||
| Err(ApiError::invalid_query(format!( |
There was a problem hiding this comment.
🟡 Optional: the unmatched-prefix NotFound that keeps branch create on err:db/LedgerNotFound is shared with commit show and revert.
fluree-db-api/src/ledger_view.rs:417 (resolve_commit_prefix, zero matches) is still ApiError::NotFound, and it's what branch create (ledger/loading.rs:451), commit show (graph_commit_builder.rs:257) and revert (revert.rs:372-384) all reach through resolve_commit. So the mislabel you note for branch create applies to all three. A commit-specific not-found (still a 404, with its own label) would fix all three at once. Minor and non-blocking — but if you agree it's right, I'd rather see it folded in now than lost in the backlog. Commenting here because ledger_view.rs:417 is not in this diff.
There was a problem hiding this comment.
Addressed in 65a0e48
ApiError::CommitNotFound (404, err:db/CommitNotFound). For consistency with GraphNotFound, a query's @commit: that matches nothing is now this 404 too; malformed, abbreviated or ambiguous refs stay 400. Revert had been surfacing err:system/InternalError (the committer flattened the error to a bare status), so SubmissionError::CommitNotFound carries the type through.
| } | ||
|
|
||
| // Step 4: Return result based on match count | ||
| // A 400, not `NotFound`: `load_view_from_source` takes any `is_not_found()` |
There was a problem hiding this comment.
👍 Nice: retyping at the source with the reason written down.
Putting the "a 400, not NotFound, because load_view_from_source reads is_not_found() as not-a-ledger" reasoning right on the line means the next person tempted to make this a 404 finds out why not before they break the from clause. And because the 400 comes from here and from select_graph, the embedded API, both HTTP routes and the JSON-LD selectors all agree without any server-side translation.
| for key in ["default-graph-uri", "named-graph-uri"] { | ||
| let (status, json) = | ||
| get_query(&app, &path, NAMES, &format!("{key}={}", enc(&graph))).await; | ||
| assert_eq!(status, StatusCode::BAD_REQUEST, "{path} {key}: {json}"); |
There was a problem hiding this comment.
🔴 Must address before merge: this 400 contradicts #2007 (mine, draft), which answers the same case with 404 err:db/GraphNotFound — let's pick one before this lands.
Both PRs take "a dataset clause names a graph the ledger doesn't have" off the 500 and chose different answers: here it's 400 err:db/InvalidQuery (this assertion, and docs/api/endpoints.md:1225), and in #2007 it's 404 with @type err:db/GraphNotFound (sparql_dataset_semantics.rs:1029, it_query_dataset.rs:3802, docs/api/errors.md). I merged your stack with #2007 on top locally, and this test is the only one of 171 server grp_query tests that fails: it gets {"status":404,"@type":"err:db/GraphNotFound"}. The docs would contradict each other the same way, since endpoints.md merges cleanly.
My lean is 404 with the typed @type, mostly because it's what we already do for the other things a dataset member can name that don't exist (LedgerNotFound, and GraphSourceNotFound at fluree-db-server/src/error.rs:112-118). The swallowing problem the body calls out for commit refs (load_view_from_source treating any is_not_found() as "not a ledger") doesn't apply, because #2007's GraphNotFound is deliberately outside is_not_found(). I may be missing why #1908 wanted 400 for an unknown named graph specifically, though — if you'd rather keep 400, I'll change #2007 to match.
If we go with 404, the smallest version of this PR is probably to drop the unknown-graph row (this test, the Unknown named graph retype at view/fluree_ext.rs:108, the it_query_dataset.rs:2544 assertion and the endpoints.md sentence) and let #2007 own that case. The commit-ref 400s and the untagged-pin hint are independent of #2007; they merge cleanly with it and pass on the merged tree. Either way, it'd be good to settle before #2000, so 4.2.3 doesn't ship one status and the next release flip it.
…mitNotFound urn:default named the default graph only in dataset positions. Everywhere else it was an ordinary IRI, so USING <urn:default> read an empty graph and WITH <urn:default> INSERT wrote a named graph that no read of urn:default reached. Now: - Every position that reads a graph or chooses a default graph reads it as the default graph: USING, USING NAMED, WITH (read and write), GRAPH in an update's WHERE, the JSON-LD graph / from / fromNamed / ["graph", ...] twins, and GRAPH <urn:default> in a query, union default graph included. GRAPH ?g never binds it. - A write that names a graph by it is refused with a 400 (TransactError::DefaultGraphNameAsGraph): a GRAPH template or data quad, GRAPH ?g bound to it, a TriG block, JSON-LD @graph or ["graph", ...] templates, CREATE/CLEAR/COPY/MOVE/ADD operands, a sync target, and bulk import. A dataset clause naming a graph the ledger does not hold is now a 404 with @type err:db/GraphNotFound (ApiError::GraphNotFound), not a 400. Like the other missing dataset members (LedgerNotFound, GraphSourceNotFound), and kept out of is_not_found() so it never reads as a missing ledger. A commit reference that names no commit is a 404 with @type err:db/CommitNotFound (ApiError::CommitNotFound) on every surface: a query's @commit: pin (was a 400), and branch create's at, commit show and revert, which shared an unmatched-prefix NotFound labelled err:db/LedgerNotFound (err:system/InternalError on revert, whose committer flattened the error to a bare status). SubmissionError::CommitNotFound carries the type through the committer. Malformed, abbreviated or ambiguous references stay 400.
What this fixes
Mistakes a client makes when naming a dataset or a commit came back as
500 err:system/InternalError. They now come back as a 4xx:400 err:db/InvalidQueryfor a malformed reference, and a typed404for one that names something the ledger doesn't have.default-graph-uri/named-graph-uri/FROMnaming a graph the ledger doesn't hold, on/query/{ledger}or/queryerr:db/GraphNotFound@commit:sha256:abc(too short oncesha256:is stripped)@commit:<abbreviated CID>@commit:<prefix>that matches several commits@commit:<prefix>that matches no commiterr:db/CommitNotFoundat, commit showcommit, or revert naming no commiterr:db/LedgerNotFound(revert:err:system/InternalError)err:db/CommitNotFoundFROM, and JSON-LDfrom/at. The too-short and abbreviated-CID cases also apply to branch create'sat.NotFound:ApiError::GraphNotFoundandApiError::CommitNotFoundstay out ofis_not_found(), whichload_view_from_sourcereads as "this source isn't a ledger".GraphNotFoundmatches fix: typed dataset and graph references: one grammar, one resolver #2007's definition.SubmissionError::CommitNotFoundcarries the type through; an exhaustive match onSubmissionErrorneeds an arm for it.InvalidQuerynow.urn:defaulturn:defaultis the name/infolists the default graph under. The name is now one constant,fluree_db_core::DEFAULT_GRAPH_IRI, and ledger info uses it too. It names the default graph wherever a graph is read or a default graph is chosen:FROM/FROM NAMED, the protocol dataset parameters on both routes,ledger#urn:default, and the JSON-LDfrom/graphselectorsGRAPH <urn:default>in a query, asGRAPH <ledger-alias>does;GRAPH ?gnever binds itUSING,USING NAMED,WITH(which also writes the default graph),GRAPHin the WHERE, and the JSON-LDgraph/from/fromNamed/["graph", …]twinsThe default graph is not a named graph, so a write that names a graph
urn:defaultis a 400 (TransactError::DefaultGraphNameAsGraph): aGRAPHtemplate or data quad,GRAPH ?gbound to it, a TriG block, JSON-LD@graphor["graph", …]templates,CREATE/CLEAR/DROP/COPY/MOVE/ADDoperands, a sync target, and bulk import. Before this,WITH <urn:default> INSERTwrote such a graph, which/infohid and nothing readingurn:defaultreached.With #2004's union setting on,
FROM <urn:default>reads the union, asFROM <ledger>and"from": "default"do;GRAPH <urn:default>still reads the default graph alone.Bare commit id in a ledger path
A ledger address still requires its tag, since a bare
123456is both atand a commit prefix;at/--atkeep accepting bare spellings as before. When a ledger address has no tag, the 400 now suggests the tagged form it most likely meant:It gives the same kind of suggestion for a bare transaction number (
@t:5) and a bare timestamp (@time:…).Partially addresses #1908: this moves the dataset-addressing and commit-resolution callers of
ApiError::queryonto caller errors (invalid_query,GraphNotFound,CommitNotFound). The remaining callers still need the audit that issue describes.Not changed
FROMstays a 400. Telling an unknown graph from another ledger's address needs fix: typed dataset and graph references: one grammar, one resolver #2007's resolver.urn:defaultis no longer reachable: every read resolves the name to the default graph, and graph management refuses it.Tests
fluree-db-server/tests/sparql_protocol_dataset_params.rs:a_graph_the_ledger_does_not_hold_is_a_404, on both routes and both parameters, with the@typeurn_default_names_the_default_graph, covering the ledger route default and named, merged with a named graph, connectionledger#urn:default, and the JSON-LDfromtwinfluree-db-server/tests/query_path_time_pin.rs:a_malformed_commit_pin_is_a_400, covering the ledger path,FROM, JSON-LDfrom/atand branch createata_commit_reference_that_names_no_commit_is_a_404, covering the ledger path,FROM, JSON-LDfrom/at, branch createat, commit show and revert, with the@typean_untagged_commit_id_in_the_path_names_its_tagfluree-db-api/tests/it_default_graph_name.rs:urn:defaultin every update position, SPARQL and JSON-LD; each refused write form; bulk import; andGRAPH <urn:default>in a query with the union on and off.fluree-db-api/tests/it_query_dataset.rs:sparql_within_ledger_from_urn_default_scopes_default_graph, plus a 400 assertion on the existing not-in-this-ledger test.fluree-db-api/tests/it_named_graphs.rs: the unknown-named-graph test assertsGraphNotFoundand 404.fluree-db-api/tests/it_commit_identity.rs: a 400 assertion on the abbreviated-CID refusal.fluree-db-core:untagged_spec_is_refused_with_its_tagged_spelling.fluree-db-server:submission_error_variants_are_exhaustively_classifiedclassifiesCommitNotFound.Each fix was reverted and its test watched fail. The new server tests failed against the unfixed code with the reported 500, the missing hint, or the old label. The
urn:defaultpieces were reverted one at a time (update default-graph positions, the write refusal, queryGRAPH, the update WHERE's named graphs,USING NAMED, the union alias, graph management), as were theGraphNotFoundsite and eachCommitNotFoundsite (the shared resolver, the query pin, the committer); each turned its cases red.