feat(cli)!: query/mutate invoke stored queries by name; server kind-assert (RFC-011 D3) (#247)

omnigraph query <name> / mutate <name> invoke a stored query by name from the served catalog (served-only). The verb asserts kind via a new expect_mutation on POST /queries/{name} (400 on mismatch). -e/--query + --store is the ad-hoc lane; the positional selects within the source (replacing --name). The bare positional graph URI, --uri, and --name are removed from query/mutate.
This commit is contained in:
Andrew Altshuler 2026-06-15 16:52:58 +03:00 committed by GitHub
parent 1bc0ea6b51
commit 9ef5f90991
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 331 additions and 120 deletions

View file

@ -92,20 +92,18 @@ pub(crate) enum Command {
/// when used. Pairs with `omnigraph mutate` on the write side.
#[command(visible_alias = "read")]
Query {
/// Graph URI
#[arg(long)]
uri: Option<String>,
#[arg(hide = true)]
legacy_uri: Option<String>,
/// Query name. With no `--query`/`-e`, the stored query to invoke from
/// the catalog (served — addressed via --server/--profile). With
/// `--query`/`-e`, selects which query in that ad-hoc source to run.
name: Option<String>,
#[arg(long)]
config: Option<PathBuf>,
/// Ad-hoc query file (a `.gq` you're authoring / break-glass).
#[arg(long, conflicts_with = "query_string")]
query: Option<PathBuf>,
/// Inline GQ source — alternative to `--query <path>`.
/// Inline ad-hoc GQ source — alternative to `--query <path>`.
#[arg(short = 'e', long = "query-string", value_name = "GQ", conflicts_with = "query")]
query_string: Option<String>,
#[arg(long)]
name: Option<String>,
#[command(flatten)]
params: ParamsArgs,
#[arg(long, conflicts_with = "snapshot")]
@ -124,20 +122,18 @@ pub(crate) enum Command {
/// warning when used. Pairs with `omnigraph query` on the read side.
#[command(visible_alias = "change")]
Mutate {
/// Graph URI
#[arg(long)]
uri: Option<String>,
#[arg(hide = true)]
legacy_uri: Option<String>,
/// Query name. With no `--query`/`-e`, the stored mutation to invoke
/// from the catalog (served — addressed via --server/--profile). With
/// `--query`/`-e`, selects which query in that ad-hoc source to run.
name: Option<String>,
#[arg(long)]
config: Option<PathBuf>,
/// Ad-hoc mutation file (a `.gq` you're authoring / break-glass).
#[arg(long, conflicts_with = "query_string")]
query: Option<PathBuf>,
/// Inline GQ source — alternative to `--query <path>`.
/// Inline ad-hoc GQ source — alternative to `--query <path>`.
#[arg(short = 'e', long = "query-string", value_name = "GQ", conflicts_with = "query")]
query_string: Option<String>,
#[arg(long)]
name: Option<String>,
#[command(flatten)]
params: ParamsArgs,
#[arg(long)]

View file

@ -29,7 +29,8 @@ use omnigraph::db::{Omnigraph, ReadTarget};
use omnigraph_api_types::{
BranchCreateOutput, BranchCreateRequest, BranchDeleteOutput, BranchListOutput,
BranchMergeOutput, BranchMergeRequest, ChangeOutput, CommitListOutput, CommitOutput,
ErrorOutput, ExportRequest, GraphListResponse, IngestOutput, IngestRequest, ReadOutput,
ErrorOutput, ExportRequest, GraphListResponse, IngestOutput, IngestRequest,
InvokeStoredQueryRequest, ReadOutput,
ReadRequest, SchemaApplyOutput, SchemaApplyRequest, SchemaOutput, SnapshotOutput, commit_output,
ingest_output, read_output, schema_apply_output, snapshot_payload,
};
@ -563,6 +564,50 @@ impl GraphClient {
}
}
/// `invoke_named` — run a stored query **by catalog name** (RFC-011 D3).
/// Served-only: the catalog is server-owned, so a `--store` (embedded)
/// scope has nothing to resolve the name against. `expect_mutation` carries
/// the verb's asserted kind; the server rejects a mismatch (400) before
/// running, so the response is exactly the expected envelope — the caller
/// deserializes it as the concrete `T` (`ReadOutput` for `query`,
/// `ChangeOutput` for `mutate`), sidestepping the untagged wire enum.
pub(crate) async fn invoke_named<T: serde::de::DeserializeOwned>(
&self,
name: &str,
expect_mutation: bool,
params_json: Option<&Value>,
branch: Option<String>,
snapshot: Option<String>,
) -> Result<T> {
match self {
GraphClient::Remote {
http,
base_url,
token,
} => {
let body = InvokeStoredQueryRequest {
params: params_json.cloned(),
branch,
snapshot,
expect_mutation: Some(expect_mutation),
};
remote_json(
http,
Method::POST,
remote_url(base_url, &["queries", name], &[])?,
Some(serde_json::to_value(body)?),
token.as_deref(),
)
.await
}
GraphClient::Embedded { .. } => bail!(
"by-name invocation needs a server (the stored-query catalog is \
server-owned); use -e '<gq>' or --query <file> for an ad-hoc query \
against --store, or address a server with --server / --profile"
),
}
}
pub(crate) async fn branch_create_from(
&self,
from: &str,

View file

@ -578,75 +578,96 @@ async fn main() -> Result<()> {
.await?;
}
Command::Query {
uri,
legacy_uri,
name,
config,
query,
query_string,
name,
params,
branch,
snapshot,
format,
json,
} => {
if query.is_none() && query_string.is_none() {
bail!("provide a query: --query <file> or -e '<inline gq>'");
}
let config = load_cli_config(config.as_ref())?;
let client = client::GraphClient::resolve(
&config,
cli.server.as_deref(),
cli.graph.as_deref(),
uri.or(legacy_uri),
None,
cli.profile.as_deref(),
cli.store.as_deref(),
)
.await?;
let query_source =
resolve_query_source(&config, query.as_ref(), query_string.as_deref(), None)?;
let params_json = load_params_json(&params)?;
let target = resolve_read_target(&config, branch, snapshot, None)?;
let output = client
.query(target, &query_source, name.as_deref(), params_json.as_ref())
.await?;
let output: ReadOutput = if query.is_some() || query_string.is_some() {
// Ad-hoc lane: run the source; the positional `name` selects
// within it when it holds more than one query.
let query_source =
resolve_query_source(&config, query.as_ref(), query_string.as_deref(), None)?;
client
.query(target, &query_source, name.as_deref(), params_json.as_ref())
.await?
} else {
// Catalog lane (served-only): invoke the stored query by name.
let Some(name) = name else {
bail!(
"provide a query name to invoke from the catalog, or -e '<gq>' / \
--query <file> for an ad-hoc query"
);
};
let (branch, snapshot) = match &target {
ReadTarget::Branch(b) => (Some(b.clone()), None),
ReadTarget::Snapshot(s) => (None, Some(s.as_str().to_string())),
};
client
.invoke_named(&name, false, params_json.as_ref(), branch, snapshot)
.await?
};
let format = resolve_read_format(&config, format, json, None);
print_read_output(&output, format, &config)?;
}
Command::Mutate {
uri,
legacy_uri,
name,
config,
query,
query_string,
name,
params,
branch,
json,
} => {
if query.is_none() && query_string.is_none() {
bail!("provide a mutation query: --query <file> or -e '<inline gq>'");
}
let config = load_cli_config(config.as_ref())?;
let client = client::GraphClient::resolve_with_policy(
&config,
cli.server.as_deref(),
cli.graph.as_deref(),
uri.or(legacy_uri),
None,
cli.as_actor.as_deref(),
cli.profile.as_deref(),
cli.store.as_deref(),
)
.await?;
let query_source =
resolve_query_source(&config, query.as_ref(), query_string.as_deref(), None)?;
let params_json = load_params_json(&params)?;
let branch = resolve_branch(&config, branch, None, "main");
let output = client
.mutate(&branch, &query_source, name.as_deref(), params_json.as_ref())
.await?;
let output: ChangeOutput = if query.is_some() || query_string.is_some() {
// Ad-hoc lane: run the source; positional `name` selects within it.
let query_source =
resolve_query_source(&config, query.as_ref(), query_string.as_deref(), None)?;
client
.mutate(&branch, &query_source, name.as_deref(), params_json.as_ref())
.await?
} else {
// Catalog lane (served-only): invoke the stored mutation by name.
let Some(name) = name else {
bail!(
"provide a mutation name to invoke from the catalog, or -e '<gq>' / \
--query <file> for an ad-hoc mutation"
);
};
client
.invoke_named(&name, true, params_json.as_ref(), Some(branch), None)
.await?
};
if json {
print_json(&output)?;
} else {

View file

@ -200,15 +200,17 @@ fn wrong_address_guard_message_has_no_trailing_space() {
#[test]
fn graph_flag_on_a_positional_uri_errors() {
// RFC-011: `--graph` selects within a multi-graph scope (a server or
// cluster). A bare positional URI is already a single graph, so pairing it
// with `--graph` is a loud error, not a silently-dropped flag. (The guard
// lets `--graph` reach a data verb; the scope resolver is what rejects it.)
// cluster). An explicit `--store <uri>` is already a single graph, so
// pairing it with `--graph` is a loud error, not a silently-dropped flag.
// (The guard lets `--graph` reach a data verb; the scope resolver rejects
// it.)
let temp = tempdir().unwrap();
let graph = graph_path(temp.path());
init_graph(&graph);
let output = output_failure(
cli()
.arg("query")
.arg("--store")
.arg(&graph)
.arg("--graph")
.arg("knowledge")
@ -218,7 +220,29 @@ fn graph_flag_on_a_positional_uri_errors() {
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("already a single graph"),
"expected --graph-on-positional-URI rejection; got: {stderr}"
"expected --graph-on-explicit-store rejection; got: {stderr}"
);
}
#[test]
fn query_by_name_against_a_store_needs_a_server() {
// RFC-011 D3: by-name (catalog) invocation is served-only — the catalog is
// server-owned, so a bare `--store` has nothing to resolve the name
// against. The ad-hoc lane (`-e`/`--query`) is the local alternative.
let temp = tempdir().unwrap();
let graph = graph_path(temp.path());
init_graph(&graph);
let output = output_failure(
cli()
.arg("query")
.arg("find_people")
.arg("--store")
.arg(&graph),
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("needs a server"),
"expected a served-only by-name error; got: {stderr}"
);
}
@ -835,10 +859,10 @@ fn read_json_outputs_rows_for_named_query() {
let output = output_success(
cli()
.arg("read")
.arg("--store")
.arg(&graph)
.arg("--query")
.arg(&queries)
.arg("--name")
.arg("get_person")
.arg("--params")
.arg(r#"{"name":"Alice"}"#)
@ -867,7 +891,6 @@ fn read_via_store_flag_and_profile_match_positional_uri() {
let output = output_success(
cmd.arg("--query")
.arg(&queries)
.arg("--name")
.arg("get_person")
.arg("--params")
.arg(r#"{"name":"Alice"}"#)
@ -876,8 +899,8 @@ fn read_via_store_flag_and_profile_match_positional_uri() {
serde_json::from_slice(&output.stdout).unwrap()
};
// Baseline: positional URI.
let baseline = read_rows(cli().arg("query").arg(&graph));
// Baseline: --store names the graph.
let baseline = read_rows(cli().arg("query").arg("--store").arg(&graph));
assert_eq!(baseline["rows"][0]["p.name"], "Alice");
// --store names the same graph directly.
@ -1097,7 +1120,6 @@ fn read_can_resolve_uri_from_config() {
.arg(&config)
.arg("--query")
.arg(fixture("test.gq"))
.arg("--name")
.arg("get_person")
.arg("--params")
.arg(r#"{"name":"Alice"}"#)
@ -1117,10 +1139,10 @@ fn read_csv_format_outputs_header_and_row_values() {
let output = output_success(
cli()
.arg("read")
.arg("--store")
.arg(&graph)
.arg("--query")
.arg(fixture("test.gq"))
.arg("--name")
.arg("get_person")
.arg("--params")
.arg(r#"{"name":"Alice"}"#)
@ -1154,10 +1176,10 @@ fn read_uses_operator_default_output_format() {
command
.env("OMNIGRAPH_HOME", operator_home.path())
.arg("read")
.arg("--store")
.arg(&graph)
.arg("--query")
.arg(fixture("test.gq"))
.arg("--name")
.arg("get_person")
.arg("--params")
.arg(r#"{"name":"Alice"}"#);
@ -1189,10 +1211,10 @@ fn read_jsonl_format_outputs_metadata_header_first() {
let output = output_success(
cli()
.arg("read")
.arg("--store")
.arg(&graph)
.arg("--query")
.arg(fixture("test.gq"))
.arg("--name")
.arg("get_person")
.arg("--params")
.arg(r#"{"name":"Alice"}"#)
@ -1224,6 +1246,7 @@ query insert_person($name: String, $age: I32) {
let output = output_success(
cli()
.arg("change")
.arg("--store")
.arg(&graph)
.arg("--query")
.arg(&mutation_file)
@ -1240,10 +1263,10 @@ query insert_person($name: String, $age: I32) {
let verify = output_success(
cli()
.arg("read")
.arg("--store")
.arg(&graph)
.arg("--query")
.arg(fixture("test.gq"))
.arg("--name")
.arg("get_person")
.arg("--params")
.arg(r#"{"name":"Eve"}"#)
@ -1298,6 +1321,7 @@ fn read_requires_name_for_multi_query_files() {
let output = output_failure(
cli()
.arg("read")
.arg("--store")
.arg(&graph)
.arg("--query")
.arg(fixture("test.gq")),
@ -1316,6 +1340,7 @@ fn read_supports_inline_query_string() {
let output = output_success(
cli()
.arg("read")
.arg("--store")
.arg(&repo)
.arg("-e")
.arg("query find($name: String) { match { $p: Person { name: $name } } return { $p.name, $p.age } }")
@ -1331,11 +1356,12 @@ fn read_supports_inline_query_string() {
#[test]
fn positional_http_uri_on_a_data_verb_is_rejected() {
// RFC-011: a positional/`--uri` http(s):// URL no longer dispatches to a
// remote server — that requires `--server <url>`.
// RFC-011: a `--store` http(s):// URL no longer dispatches to a remote
// server — that requires `--server <url>`.
let output = output_failure(
cli()
.arg("query")
.arg("--store")
.arg("http://127.0.0.1:1")
.arg("-e")
.arg("query q() { match { $p: Person { } } return { $p } }"),
@ -1343,7 +1369,7 @@ fn positional_http_uri_on_a_data_verb_is_rejected() {
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("must be addressed with `--server <url>`"),
"expected positional-remote rejection; got: {stderr}"
"expected store-remote rejection; got: {stderr}"
);
}
@ -1381,6 +1407,7 @@ fn change_supports_inline_query_string() {
let output = output_success(
cli()
.arg("change")
.arg("--store")
.arg(&repo)
.arg("--query-string")
.arg("query add($name: String, $age: I32) { insert Person { name: $name, age: $age } }")
@ -1395,6 +1422,7 @@ fn change_supports_inline_query_string() {
let verify = output_success(
cli()
.arg("read")
.arg("--store")
.arg(&repo)
.arg("-e")
.arg("query find($name: String) { match { $p: Person { name: $name } } return { $p.name } }")
@ -1416,6 +1444,7 @@ fn read_rejects_query_string_combined_with_query() {
let output = output_failure(
cli()
.arg("read")
.arg("--store")
.arg(&repo)
.arg("--query")
.arg(fixture("test.gq"))
@ -1436,7 +1465,7 @@ fn read_rejects_empty_query_string() {
init_graph(&repo);
load_fixture(&repo);
let output = output_failure(cli().arg("read").arg(&repo).arg("-e").arg(""));
let output = output_failure(cli().arg("read").arg("--store").arg(&repo).arg("-e").arg(""));
let stderr = String::from_utf8(output.stderr).unwrap();
assert!(
stderr.contains("must not be empty"),

View file

@ -83,7 +83,6 @@ fn parity_query() {
"query",
"--query",
query.to_str().unwrap(),
"--name",
"get_person",
"--params",
r#"{"name":"Alice"}"#,
@ -232,7 +231,6 @@ fn parity_errors_share_exit_codes() {
"query",
"--query",
query.to_str().unwrap(),
"--name",
"no_such_query",
"--json",
],
@ -252,7 +250,6 @@ fn parity_errors_share_exit_codes() {
"query",
"--query",
query.to_str().unwrap(),
"--name",
"get_person",
"--json",
],

View file

@ -231,10 +231,10 @@ fn local_cli_end_to_end_init_load_read_change_read_flow() {
let read_before = parse_stdout_json(&output_success(
cli()
.arg("read")
.arg("--store")
.arg(graph.path())
.arg("--query")
.arg(fixture("test.gq"))
.arg("--name")
.arg("get_person")
.arg("--params")
.arg(r#"{"name":"Alice"}"#)
@ -246,6 +246,7 @@ fn local_cli_end_to_end_init_load_read_change_read_flow() {
let change_payload = parse_stdout_json(&output_success(
cli()
.arg("change")
.arg("--store")
.arg(graph.path())
.arg("--query")
.arg(&mutation_file)
@ -259,10 +260,10 @@ fn local_cli_end_to_end_init_load_read_change_read_flow() {
let read_after = parse_stdout_json(&output_success(
cli()
.arg("read")
.arg("--store")
.arg(graph.path())
.arg("--query")
.arg(fixture("test.gq"))
.arg("--name")
.arg("get_person")
.arg("--params")
.arg(r#"{"name":"Eve"}"#)
@ -277,6 +278,7 @@ fn local_cli_end_to_end_init_load_read_change_read_flow() {
let inline_change = parse_stdout_json(&output_success(
cli()
.arg("change")
.arg("--store")
.arg(graph.path())
.arg("-e")
.arg("query add($name: String, $age: I32) { insert Person { name: $name, age: $age } }")
@ -291,6 +293,7 @@ fn local_cli_end_to_end_init_load_read_change_read_flow() {
let inline_read = parse_stdout_json(&output_success(
cli()
.arg("read")
.arg("--store")
.arg(graph.path())
.arg("--query-string")
.arg("query find($name: String) { match { $p: Person { name: $name } } return { $p.name, $p.age } }")
@ -322,6 +325,7 @@ fn local_cli_end_to_end_branch_change_merge_flow() {
let change_payload = parse_stdout_json(&output_success(
cli()
.arg("change")
.arg("--store")
.arg(graph.path())
.arg("--query")
.arg(&mutation_file)
@ -337,10 +341,10 @@ fn local_cli_end_to_end_branch_change_merge_flow() {
let feature_read = parse_stdout_json(&output_success(
cli()
.arg("read")
.arg("--store")
.arg(graph.path())
.arg("--query")
.arg(fixture("test.gq"))
.arg("--name")
.arg("get_person")
.arg("--branch")
.arg("feature")
@ -365,10 +369,10 @@ fn local_cli_end_to_end_branch_change_merge_flow() {
let main_read = parse_stdout_json(&output_success(
cli()
.arg("read")
.arg("--store")
.arg(graph.path())
.arg("--query")
.arg(fixture("test.gq"))
.arg("--name")
.arg("get_person")
.arg("--params")
.arg(r#"{"name":"Zoe"}"#)
@ -435,10 +439,10 @@ fn local_cli_ingest_creates_review_branch_and_keeps_it_readable() {
let zoe = parse_stdout_json(&output_success(
cli()
.arg("read")
.arg("--store")
.arg(graph.path())
.arg("--query")
.arg(fixture("test.gq"))
.arg("--name")
.arg("get_person")
.arg("--branch")
.arg("feature-ingest")
@ -452,10 +456,10 @@ fn local_cli_ingest_creates_review_branch_and_keeps_it_readable() {
let bob = parse_stdout_json(&output_success(
cli()
.arg("read")
.arg("--store")
.arg(graph.path())
.arg("--query")
.arg(fixture("test.gq"))
.arg("--name")
.arg("get_person")
.arg("--branch")
.arg("feature-ingest")
@ -629,10 +633,10 @@ fn local_cli_export_round_trips_full_branch_graph() {
let eve = parse_stdout_json(&output_success(
cli()
.arg("read")
.arg("--store")
.arg(&imported_graph)
.arg("--query")
.arg(fixture("test.gq"))
.arg("--name")
.arg("get_person")
.arg("--params")
.arg(r#"{"name":"Eve"}"#)
@ -644,10 +648,10 @@ fn local_cli_export_round_trips_full_branch_graph() {
let friends = parse_stdout_json(&output_success(
cli()
.arg("read")
.arg("--store")
.arg(&imported_graph)
.arg("--query")
.arg(fixture("test.gq"))
.arg("--name")
.arg("friends_of")
.arg("--params")
.arg(r#"{"name":"Alice"}"#)
@ -717,7 +721,6 @@ policy: {{}}
.arg(&config)
.arg("--query")
.arg("test.gq")
.arg("--name")
.arg("get_person")
.arg("--params")
.arg(r#"{"name":"Alice"}"#)
@ -779,6 +782,7 @@ fn local_cli_failed_change_keeps_target_state_unchanged() {
let output = output_failure(
cli()
.arg("change")
.arg("--store")
.arg(graph.path())
.arg("--query")
.arg(&mutation_file)
@ -791,10 +795,10 @@ fn local_cli_failed_change_keeps_target_state_unchanged() {
let friends_payload = parse_stdout_json(&output_success(
cli()
.arg("read")
.arg("--store")
.arg(graph.path())
.arg("--query")
.arg(fixture("test.gq"))
.arg("--name")
.arg("friends_of")
.arg("--params")
.arg(r#"{"name":"Alice"}"#)
@ -865,7 +869,6 @@ query get_person($name: String) {
.arg(&config)
.arg("--query")
.arg("local.gq")
.arg("--name")
.arg("get_person")
.arg("--params")
.arg(r#"{"name":"Alice"}"#)
@ -974,10 +977,10 @@ query get_task($slug: String) {
let filtered = parse_stdout_json(&output_success(
cli()
.arg("read")
.arg("--store")
.arg(&graph)
.arg("--query")
.arg(&queries)
.arg("--name")
.arg("due_with_tag")
.arg("--params")
.arg(r#"{"deadline":"2026-04-02T00:00:00Z","tag":"launch"}"#)
@ -999,10 +1002,10 @@ query get_task($slug: String) {
let insert_payload = parse_stdout_json(&output_success(
cli()
.arg("change")
.arg("--store")
.arg(&graph)
.arg("--query")
.arg(&queries)
.arg("--name")
.arg("insert_task")
.arg("--params")
.arg(
@ -1015,10 +1018,10 @@ query get_task($slug: String) {
let update_payload = parse_stdout_json(&output_success(
cli()
.arg("change")
.arg("--store")
.arg(&graph)
.arg("--query")
.arg(&queries)
.arg("--name")
.arg("update_task")
.arg("--params")
.arg(r#"{"slug":"gamma","due_at":"2026-04-04T10:45:00Z","tags":["embed","released"],"scores":[13,21],"active_days":["2026-04-04","2026-04-05"]}"#)
@ -1029,10 +1032,10 @@ query get_task($slug: String) {
let gamma = parse_stdout_json(&output_success(
cli()
.arg("read")
.arg("--store")
.arg(&graph)
.arg("--query")
.arg(&queries)
.arg("--name")
.arg("get_task")
.arg("--params")
.arg(r#"{"slug":"gamma"}"#)
@ -1112,10 +1115,10 @@ query vector_search($q: String) {
let result = parse_stdout_json(&output_success(
cli()
.arg("read")
.arg("--store")
.arg(&graph)
.arg("--query")
.arg(&queries)
.arg("--name")
.arg("vector_search")
.arg("--params")
.arg(r#"{"q":"alpha"}"#)
@ -1265,10 +1268,10 @@ fn local_cli_change_enforces_engine_layer_policy() {
let verify = parse_stdout_json(&output_success(
cli()
.arg("read")
.arg("--store")
.arg(graph.path())
.arg("--query")
.arg(fixture("test.gq"))
.arg("--name")
.arg("get_person")
.arg("--params")
.arg(r#"{"name":"RagnorOnMain"}"#)
@ -1292,7 +1295,7 @@ fn local_cli_positional_uri_does_not_inherit_default_graph_policy() {
.arg("change")
.arg("--config")
.arg(&config)
.arg("--uri")
.arg("--store")
.arg(graph.path())
.arg("--query")
.arg(&mutation_file)
@ -2343,7 +2346,6 @@ fn local_cli_keyed_credentials_authenticate_url_matched_server() {
.arg(&server.base_url)
.arg("--query")
.arg(fixture("test.gq"))
.arg("--name")
.arg("get_person")
.arg("--params")
.arg(r#"{"name":"Alice"}"#)
@ -2514,6 +2516,45 @@ fn local_cli_operator_alias_and_server_flag_invoke_stored_query() {
.unwrap();
assert!(output.status.success(), "{output:?}");
// RFC-011 D3: invoke the STORED query by name (catalog lane, served-only).
// No `-e`/`--query` — the positional `find_person` is the catalog name.
let output = cli()
.env("OMNIGRAPH_HOME", operator_home.path())
.arg("query")
.arg("find_person")
.arg("--server")
.arg("dev")
.arg("--graph")
.arg("local")
.arg("--params")
.arg(r#"{"name":"Alice"}"#)
.arg("--json")
.output()
.unwrap();
assert!(output.status.success(), "by-name catalog invocation: {output:?}");
let payload: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(payload["rows"][0]["p.name"], "Alice", "{payload}");
// The verb asserts kind: `mutate <a-read>` is rejected by the server.
let output = cli()
.env("OMNIGRAPH_HOME", operator_home.path())
.arg("mutate")
.arg("find_person")
.arg("--server")
.arg("dev")
.arg("--graph")
.arg("local")
.arg("--params")
.arg(r#"{"name":"Alice"}"#)
.output()
.unwrap();
assert!(!output.status.success(), "mutate on a read query must fail");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("'find_person' is a read — use omnigraph query find_person"),
"expected a kind-mismatch error; got: {stderr}"
);
// Unknown --server errors listing what IS defined.
let output = cli()
.env("OMNIGRAPH_HOME", operator_home.path())
@ -2528,10 +2569,14 @@ fn local_cli_operator_alias_and_server_flag_invoke_stored_query() {
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(stderr.contains("unknown server 'nope'") && stderr.contains("dev"), "{stderr}");
// --server is exclusive with a positional URI.
// --server is exclusive with --store (two ways to address the graph).
// (RFC-011 D3: there is no positional URI anymore — the positional is a
// query name — so the double-addressing contradiction now surfaces between
// the two scope primitives.)
let output = cli()
.env("OMNIGRAPH_HOME", operator_home.path())
.arg("query")
.arg("--store")
.arg(&server.base_url)
.arg("--server")
.arg("dev")

View file

@ -131,10 +131,10 @@ query insert_person($name: String, $age: I32) {
let local_read = parse_stdout_json(&output_success(
cli()
.arg("read")
.arg("--store")
.arg(graph.path())
.arg("--query")
.arg(fixture("test.gq"))
.arg("--name")
.arg("get_person")
.arg("--params")
.arg(r#"{"name":"Alice"}"#)
@ -147,7 +147,6 @@ query insert_person($name: String, $age: I32) {
.arg(&config)
.arg("--query")
.arg(fixture("test.gq"))
.arg("--name")
.arg("get_person")
.arg("--params")
.arg(r#"{"name":"Alice"}"#)
@ -191,10 +190,10 @@ query insert_person($name: String, $age: I32) {
let local_verify = parse_stdout_json(&output_success(
cli()
.arg("read")
.arg("--store")
.arg(graph.path())
.arg("--query")
.arg(fixture("test.gq"))
.arg("--name")
.arg("get_person")
.arg("--params")
.arg(r#"{"name":"Mina"}"#)
@ -394,7 +393,6 @@ query ordered_person($name: String) {
.arg(&config)
.arg("--query")
.arg(&ordered_query)
.arg("--name")
.arg("ordered_person")
.arg("--params")
.arg(r#"{"name":"Alice"}"#)
@ -415,7 +413,6 @@ query ordered_person($name: String) {
.arg(&config)
.arg("--query")
.arg(&ordered_query)
.arg("--name")
.arg("ordered_person")
.arg("--params")
.arg(r#"{"name":"Alice"}"#)
@ -514,7 +511,6 @@ query insert_person($name: String, $age: I32) {
.arg(&config)
.arg("--query")
.arg(fixture("test.gq"))
.arg("--name")
.arg("get_person")
.arg("--params")
.arg(r#"{"name":"Zoe"}"#)
@ -604,7 +600,6 @@ query add_friend($from: String, $to: String) {
.arg(&config)
.arg("--query")
.arg(&mutation_file)
.arg("--name")
.arg("insert_person")
.arg("--branch")
.arg("feature")
@ -619,7 +614,6 @@ query add_friend($from: String, $to: String) {
.arg(&config)
.arg("--query")
.arg(&mutation_file)
.arg("--name")
.arg("add_friend")
.arg("--branch")
.arg("feature")
@ -686,10 +680,10 @@ query add_friend($from: String, $to: String) {
let eve = parse_stdout_json(&output_success(
cli()
.arg("read")
.arg("--store")
.arg(&imported_graph)
.arg("--query")
.arg(fixture("test.gq"))
.arg("--name")
.arg("get_person")
.arg("--params")
.arg(r#"{"name":"Eve"}"#)
@ -747,7 +741,6 @@ fn remote_ingest_creates_review_branch_and_keeps_it_readable() {
.arg(&config)
.arg("--query")
.arg(fixture("test.gq"))
.arg("--name")
.arg("get_person")
.arg("--branch")
.arg("feature-ingest")
@ -875,7 +868,6 @@ fn remote_ingest_reuses_existing_branch_and_merges_updates() {
.arg(&config)
.arg("--query")
.arg(fixture("test.gq"))
.arg("--name")
.arg("get_person")
.arg("--branch")
.arg("feature-ingest")
@ -893,7 +885,6 @@ fn remote_ingest_reuses_existing_branch_and_merges_updates() {
.arg(&config)
.arg("--query")
.arg(fixture("test.gq"))
.arg("--name")
.arg("get_person")
.arg("--branch")
.arg("feature-ingest")
@ -1020,7 +1011,6 @@ query insert_person($name: String, $age: I32) {
.arg(&client_config)
.arg("--query")
.arg(fixture("test.gq"))
.arg("--name")
.arg("get_person")
.arg("--params")
.arg(r#"{"name":"PolicyRemote"}"#)