From 6deca2e4c1548a4380f19c96aa2aeff55de3bd67 Mon Sep 17 00:00:00 2001 From: Ragnor Comerford Date: Mon, 1 Jun 2026 18:39:14 +0200 Subject: [PATCH] fix(cli): require graph selection for scoped query registries --- crates/omnigraph-cli/src/main.rs | 47 ++++++++++++++++++++++----- crates/omnigraph-cli/tests/cli.rs | 53 +++++++++++++++++++++++++++++++ docs/user/cli-reference.md | 2 +- 3 files changed, 93 insertions(+), 9 deletions(-) diff --git a/crates/omnigraph-cli/src/main.rs b/crates/omnigraph-cli/src/main.rs index 300f430..ff0239e 100644 --- a/crates/omnigraph-cli/src/main.rs +++ b/crates/omnigraph-cli/src/main.rs @@ -1798,6 +1798,43 @@ fn load_registry_or_report( }) } +fn graph_query_registry_names(config: &OmnigraphConfig) -> Vec<&str> { + config + .graphs + .iter() + .filter_map(|(name, graph)| (!graph.queries.is_empty()).then_some(name.as_str())) + .collect() +} + +fn resolve_registry_selection_for_list( + config: &OmnigraphConfig, + target: Option<&str>, +) -> Result> { + let selected = target + .map(str::to_string) + .or_else(|| config.cli_graph_name().map(str::to_string)); + if let Some(name) = selected.as_deref() { + config.resolve_graph_selection(Some(name))?; + return Ok(selected); + } + + if !config.query_entries().is_empty() { + return Ok(None); + } + + let graph_names = graph_query_registry_names(config); + if graph_names.is_empty() { + return Ok(None); + } + + bail!( + "stored-query registries are configured for graph{} {} but no graph was selected. Pass `--target {}` or set `cli.graph`.", + if graph_names.len() == 1 { "" } else { "s" }, + graph_names.join(", "), + graph_names[0], + ) +} + fn validate_registry_for_catalog( registry: &QueryRegistry, catalog: &omnigraph_compiler::catalog::Catalog, @@ -1878,14 +1915,8 @@ fn execute_queries_list( json: bool, ) -> Result<()> { let config = load_cli_config(config_path)?; - // `list` takes no URI, so the selection is the target or the configured - // default graph (named → its per-graph block; else top-level). Validate - // membership explicitly — every URI-resolving command rejects an unknown - // name as a side effect of `resolve_target_uri`, but `list` opens no URI, - // so it would otherwise fall back to the top-level registry silently. - let selected = - config.resolve_graph_selection(target.as_deref().or_else(|| config.cli_graph_name()))?; - let registry = load_registry_or_report(&config, selected)?; + let selected = resolve_registry_selection_for_list(&config, target.as_deref())?; + let registry = load_registry_or_report(&config, selected.as_deref())?; let output = QueriesListOutput { queries: registry diff --git a/crates/omnigraph-cli/tests/cli.rs b/crates/omnigraph-cli/tests/cli.rs index 382fc55..9682d9a 100644 --- a/crates/omnigraph-cli/tests/cli.rs +++ b/crates/omnigraph-cli/tests/cli.rs @@ -2457,6 +2457,59 @@ fn queries_list_prints_registered_query() { ); } +#[test] +fn queries_list_requires_graph_selection_for_per_graph_only_registries() { + let graph = SystemGraph::loaded(); + graph.write_query( + "find_person.gq", + "query find_person($name: String) { match { $p: Person { name: $name } } return { $p.age } }", + ); + let config = graph.write_config( + "omnigraph.yaml", + &format!( + concat!( + "graphs:\n", + " local:\n", + " uri: '{}'\n", + " queries:\n", + " find_person:\n", + " file: ./find_person.gq\n", + "policy: {{}}\n", + ), + graph.path().to_string_lossy().replace('\'', "''") + ), + ); + + let output = output_failure(cli().arg("queries").arg("list").arg("--config").arg(&config)); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("local") && stderr.contains("--target local"), + "error must name the graph and give a concrete selection hint; stderr:\n{stderr}" + ); +} + +#[test] +fn queries_list_without_graph_selection_lists_top_level_registry() { + let graph = SystemGraph::loaded(); + graph.write_query( + "top_find.gq", + "query top_find($name: String) { match { $p: Person { name: $name } } return { $p.age } }", + ); + let config = graph.write_config( + "omnigraph.yaml", + concat!( + "queries:\n", + " top_find:\n", + " file: ./top_find.gq\n", + "policy: {}\n", + ), + ); + + let output = output_success(cli().arg("queries").arg("list").arg("--config").arg(&config)); + let stdout = stdout_string(&output); + assert!(stdout.contains("top_find"), "stdout:\n{stdout}"); +} + #[test] fn queries_list_unknown_target_errors() { // `queries list` opens no graph URI, so unknown-graph validation can't ride diff --git a/docs/user/cli-reference.md b/docs/user/cli-reference.md index 931b7f9..7155d0f 100644 --- a/docs/user/cli-reference.md +++ b/docs/user/cli-reference.md @@ -20,7 +20,7 @@ A reference for the `omnigraph` binary's command surface and `omnigraph.yaml` sc | `run list \| show \| publish \| abort` | transactional run ops | | `schema plan \| apply \| show (alias: get)` | migrations | | `lint` (alias: `check`) | offline / graph-backed query validation. Replaces `query lint` / `query check`, which are kept as deprecated argv-level shims that print a one-line warning and rewrite to `omnigraph lint` | -| `queries validate \| list` | operate on the server-side stored-query registry (the `queries:` block). `validate` type-checks every stored query against the live schema offline (opens the selected graph; exits non-zero on any breakage), catching schema drift without restarting the server; `list` prints each query's name, MCP exposure, and typed params. Distinct from `lint`, which validates a single `.gq` file | +| `queries validate \| list` | operate on the server-side stored-query registry (the `queries:` block). `validate` type-checks every stored query against the live schema offline (opens the selected graph; exits non-zero on any breakage), catching schema drift without restarting the server; `list` prints the selected registry's query names, MCP exposure, and typed params. For per-graph registries, pass `--target ` or set `cli.graph`; with no graph selection, `list` shows only top-level `queries:`. Distinct from `lint`, which validates a single `.gq` file | | `optimize` | non-destructive Lance compaction | | `cleanup --keep N --older-than 7d --confirm` | destructive version GC | | `embed` | offline JSONL embedding pipeline |