mirror of
https://github.com/ModernRelay/omnigraph.git
synced 2026-06-27 02:39:38 +02:00
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:
parent
1bc0ea6b51
commit
9ef5f90991
13 changed files with 331 additions and 120 deletions
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(¶ms)?;
|
||||
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(¶ms)?;
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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"}"#)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue