mirror of
https://github.com/ModernRelay/omnigraph.git
synced 2026-06-18 02:24:27 +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
|
|
@ -325,6 +325,13 @@ pub struct InvokeStoredQueryRequest {
|
|||
/// mutation). Mutually exclusive with `branch`.
|
||||
#[serde(default)]
|
||||
pub snapshot: Option<String>,
|
||||
/// The kind the caller expects (RFC-011 Decision 3): `Some(false)` for
|
||||
/// `omnigraph query <name>`, `Some(true)` for `omnigraph mutate <name>`.
|
||||
/// When set and it disagrees with the stored query's actual kind, the
|
||||
/// server rejects the call (400) so the verb asserts the kind. `None`
|
||||
/// (the default) skips the check — preserving older clients and aliases.
|
||||
#[serde(default)]
|
||||
pub expect_mutation: Option<bool>,
|
||||
}
|
||||
|
||||
/// Response for `POST /queries/{name}`: the read envelope for a stored
|
||||
|
|
|
|||
|
|
@ -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"}"#)
|
||||
|
|
|
|||
|
|
@ -980,6 +980,22 @@ pub(crate) async fn server_invoke_query(
|
|||
let query_name = stored.name.clone();
|
||||
let is_mutation = stored.is_mutation();
|
||||
|
||||
// RFC-011 D3: the CLI verb asserts the stored query's kind. `query <name>`
|
||||
// sends `expect_mutation: false`, `mutate <name>` sends `true`; a mismatch
|
||||
// is rejected here so the wrong verb errors instead of silently running.
|
||||
if let Some(expected) = req.expect_mutation {
|
||||
if expected != is_mutation {
|
||||
let (actual, verb) = if is_mutation {
|
||||
("mutation", "mutate")
|
||||
} else {
|
||||
("read", "query")
|
||||
};
|
||||
return Err(ApiError::bad_request(format!(
|
||||
"'{query_name}' is a {actual} — use omnigraph {verb} {query_name}"
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
info!(
|
||||
graph = %handle.uri,
|
||||
actor = ?actor_ref.map(|a| a.actor_id.as_ref()),
|
||||
|
|
|
|||
|
|
@ -82,6 +82,58 @@ async fn invoke_stored_read_returns_rows() {
|
|||
assert!(body["rows"].is_array(), "read envelope shape; body: {body}");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn invoke_with_mismatched_expected_kind_is_rejected() {
|
||||
// RFC-011 D3: the CLI verb asserts the stored query's kind via
|
||||
// `expect_mutation`. Invoking a read with `expect_mutation: true`
|
||||
// (i.e. `omnigraph mutate <a-read>`) is a 400 naming the right verb.
|
||||
let (_temp, app) = app_with_stored_queries(
|
||||
&[("find_person", FIND_PERSON_GQ, false)],
|
||||
&[("act-invoke", "t-invoke")],
|
||||
INVOKE_POLICY_YAML,
|
||||
)
|
||||
.await;
|
||||
let (status, body) = json_response(
|
||||
&app,
|
||||
invoke_request(
|
||||
"find_person",
|
||||
"t-invoke",
|
||||
json!({ "expect_mutation": true, "params": { "name": "Alice" } }),
|
||||
),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(status, StatusCode::BAD_REQUEST, "body: {body}");
|
||||
assert!(
|
||||
body["error"]
|
||||
.as_str()
|
||||
.unwrap_or_default()
|
||||
.contains("'find_person' is a read — use omnigraph query find_person"),
|
||||
"expected a kind-mismatch error; body: {body}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn invoke_with_matching_expected_kind_runs() {
|
||||
// The matching assertion (`omnigraph query <a-read>`) passes through.
|
||||
let (_temp, app) = app_with_stored_queries(
|
||||
&[("find_person", FIND_PERSON_GQ, false)],
|
||||
&[("act-invoke", "t-invoke")],
|
||||
INVOKE_POLICY_YAML,
|
||||
)
|
||||
.await;
|
||||
let (status, body) = json_response(
|
||||
&app,
|
||||
invoke_request(
|
||||
"find_person",
|
||||
"t-invoke",
|
||||
json!({ "expect_mutation": false, "params": { "name": "Alice" } }),
|
||||
),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(status, StatusCode::OK, "matching kind should run; body: {body}");
|
||||
assert_eq!(body["query_name"], "find_person");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn invoke_stored_read_accepts_absent_or_empty_body() {
|
||||
let no_param_query = "query list_people() { match { $p: Person } return { $p.name } }";
|
||||
|
|
|
|||
|
|
@ -6,35 +6,43 @@
|
|||
omnigraph init --schema schema.pg graph.omni
|
||||
omnigraph load --data data.jsonl --mode overwrite graph.omni
|
||||
omnigraph snapshot graph.omni --branch main --json
|
||||
omnigraph query --uri graph.omni --query queries.gq --name get_person --params '{"name":"Alice"}'
|
||||
omnigraph mutate --uri graph.omni --query queries.gq --name insert_person --params '{"name":"Mina","age":28}'
|
||||
# Invoke a stored query BY NAME from the catalog (served — addressed by scope):
|
||||
omnigraph query get_person --params '{"name":"Alice"}'
|
||||
omnigraph mutate insert_person --params '{"name":"Mina","age":28}'
|
||||
```
|
||||
|
||||
`omnigraph query` is the canonical read command (pairs with `POST /query`);
|
||||
`omnigraph mutate` is the canonical write command (pairs with `POST /mutate`).
|
||||
The previous names `omnigraph read` and `omnigraph change` keep working as
|
||||
visible aliases — invocations emit a one-line deprecation warning to stderr
|
||||
and otherwise behave identically. See [Deprecated names](#deprecated-names)
|
||||
for the migration table.
|
||||
The positional argument is the **stored-query name**, invoked from the served
|
||||
catalog (RFC-011 D3) — the graph is addressed by scope (`--server` / `--profile`
|
||||
/ defaults), and the verb asserts the query's kind (`query` rejects a stored
|
||||
mutation, and vice-versa). The previous names `omnigraph read` and
|
||||
`omnigraph change` keep working as visible aliases — invocations emit a one-line
|
||||
deprecation warning to stderr. See [Deprecated names](#deprecated-names).
|
||||
|
||||
For ad-hoc reads and mutations (REPLs, AI agents, one-off scripts), pass the
|
||||
GQ source inline with `-e` / `--query-string` instead of a file path:
|
||||
For **ad-hoc** reads and mutations (REPLs, AI agents, one-off scripts, local dev),
|
||||
pass the GQ source with `-e` / `--query-string` (inline) or `--query <path>` (a
|
||||
file), and address a graph's storage directly with `--store`. By-name catalog
|
||||
invocation is served-only — a bare `--store` has no catalog, so it's the ad-hoc
|
||||
lane:
|
||||
|
||||
```bash
|
||||
omnigraph query --uri graph.omni \
|
||||
omnigraph query --store graph.omni \
|
||||
-e 'query find($name: String) { match { $p: Person { name: $name } } return { $p.name, $p.age } }' \
|
||||
--params '{"name":"Alice"}'
|
||||
|
||||
omnigraph mutate --uri graph.omni \
|
||||
omnigraph mutate --store graph.omni \
|
||||
-e 'query add($name: String, $age: I32) { insert Person { name: $name, age: $age } }' \
|
||||
--params '{"name":"Inline","age":42}'
|
||||
|
||||
# A multi-query file: the positional selects which query to run.
|
||||
omnigraph query --store graph.omni --query queries.gq get_person --params '{"name":"Alice"}'
|
||||
```
|
||||
|
||||
`-e` is mutually exclusive with `--query <path>`; exactly one of the two must be
|
||||
provided. (Operator aliases moved to their own `omnigraph alias <name>`
|
||||
subcommand — RFC-011 D4.) The inline source travels through the same
|
||||
parser, lint, params binding, and commit machinery as a file-based query —
|
||||
only the source loader changes.
|
||||
`-e` is mutually exclusive with `--query <path>`. With either, the positional
|
||||
name (optional) selects which query in the source to run. The inline source
|
||||
travels through the same parser, lint, params binding, and commit machinery as a
|
||||
file-based query — only the source loader changes.
|
||||
|
||||
## Branching And Reviewable Data Flows
|
||||
|
||||
|
|
@ -57,13 +65,11 @@ Serve a graph:
|
|||
omnigraph-server graph.omni --bind 127.0.0.1:8080
|
||||
```
|
||||
|
||||
Read through the HTTP API:
|
||||
Read through the HTTP API — invoke a stored query by name from the catalog:
|
||||
|
||||
```bash
|
||||
omnigraph query \
|
||||
omnigraph query get_person \
|
||||
--server http://127.0.0.1:8080 \
|
||||
--query queries.gq \
|
||||
--name get_person \
|
||||
--params '{"name":"Alice"}'
|
||||
```
|
||||
|
||||
|
|
@ -87,10 +93,10 @@ For config-driven clients, set the remote graph's `bearer_token_env` to an envir
|
|||
|
||||
Runtime add/remove is **not** in v0.6.0. To add a graph, stop the server, add a `graphs.<id>` entry to `omnigraph.yaml`, then restart. To remove, stop the server, delete the entry, restart.
|
||||
|
||||
Per-graph URLs: hit a graph's cluster route from any subcommand by pointing `--uri` at it:
|
||||
Per-graph addressing: select a graph on a multi-graph server with `--graph`:
|
||||
|
||||
```bash
|
||||
omnigraph read --uri http://server.example.com/graphs/beta --query q.gq ...
|
||||
omnigraph query get_person --server http://server.example.com --graph beta --params '{"name":"Ada"}'
|
||||
```
|
||||
|
||||
## Runs, Policy, And Diagnostics
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
A reference for the `omnigraph` binary's command surface and `omnigraph.yaml` schema. For a quick-start guide, see [cli.md](index.md).
|
||||
|
||||
Top-level command families and subcommands. Graph-targeting commands accept a positional `file://`/`s3://` URI, `--server <name|url>` (an operator-defined server from `~/.omnigraph/config.yaml` by name, or a literal `http(s)://` URL, optionally with `--graph <id>` for multi-graph servers; exclusive with a positional URI), `--store <uri>` (a single graph's storage directly), or `--profile <name>` / `$OMNIGRAPH_PROFILE` (a named scope bundle; see [Scopes & profiles](#scopes--profiles-rfc-011)); `cluster` commands use `--config <dir>`. A remote server is addressed only with `--server` — a positional `http(s)://` URI is rejected.
|
||||
Top-level command families and subcommands. Graph-targeting commands accept a positional `file://`/`s3://` URI, `--server <name|url>` (an operator-defined server from `~/.omnigraph/config.yaml` by name, or a literal `http(s)://` URL, optionally with `--graph <id>` for multi-graph servers; exclusive with a positional URI), `--store <uri>` (a single graph's storage directly), or `--profile <name>` / `$OMNIGRAPH_PROFILE` (a named scope bundle; see [Scopes & profiles](#scopes--profiles-rfc-011)); `cluster` commands use `--config <dir>`. A remote server is addressed only with `--server` — a positional `http(s)://` URI is rejected. **`query`/`mutate` are the exception**: their positional is a stored-query *name* (RFC-011 D3), not a graph URI, so they address the graph only via `--store`/`--server`/`--profile`/defaults.
|
||||
|
||||
## Top-level commands
|
||||
|
||||
|
|
@ -11,8 +11,8 @@ Top-level command families and subcommands. Graph-targeting commands accept a po
|
|||
| `init` | `--schema <pg>` → initialize a graph (no longer scaffolds `omnigraph.yaml`; start cluster configs from the [cluster.md](../clusters/index.md) quick-start or `config migrate`) |
|
||||
| `load` | bulk load a branch, local or remote (`--mode overwrite\|append\|merge` is **required** — overwrite is destructive, so there is no default). Without `--from` the target branch must exist; `--from <base>` forks a missing `--branch` from `<base>` first |
|
||||
| `ingest` | deprecated alias of `load --from <base>` (defaults: `--from main --mode merge`); prints a one-line warning to stderr |
|
||||
| `query` (alias: `read`) | run a read query; source via `--query <path>` or `-e`/`--query-string <GQ>`. `read` is the deprecated previous name and prints a one-line warning to stderr |
|
||||
| `mutate` (alias: `change`) | run a mutation query; same `--query` / `-e` source as `query`. `change` is the deprecated previous name and prints a one-line warning to stderr |
|
||||
| `query <name>` (alias: `read`) | run a read query. **Catalog lane** (default): `<name>` is a stored query invoked **by name** from the served catalog (served-only — address with `--server`/`--profile`; the verb asserts the query is a read). **Ad-hoc lane**: with `--query <path>` or `-e`/`--query-string <GQ>`, runs that source (the positional `<name>` then selects which query in it). No positional graph URI — address via `--store`/`--server`/`--profile`. `read` is the deprecated previous name (one-line stderr warning) |
|
||||
| `mutate <name>` (alias: `change`) | run a mutation query; same catalog (by-name, served-only, verb asserts mutation) / ad-hoc (`--query`/`-e`) lanes as `query`. `change` is the deprecated previous name (one-line stderr warning) |
|
||||
| `alias <name> [args]` | invoke an operator alias — a personal binding (under `aliases:` in `~/.omnigraph/config.yaml`) to a stored query on a named server (RFC-011 D4; replaces the removed `--alias` flag) |
|
||||
| `snapshot` | print current snapshot (per-table version + row count) |
|
||||
| `export` | dump to JSONL on stdout (`--type T`, `--table K` filters) |
|
||||
|
|
|
|||
|
|
@ -1891,6 +1891,13 @@
|
|||
],
|
||||
"description": "Branch to run against. Defaults to `main`; for a stored mutation the\nwrite targets this branch."
|
||||
},
|
||||
"expect_mutation": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
],
|
||||
"description": "The kind the caller expects (RFC-011 Decision 3): `Some(false)` for\n`omnigraph query <name>`, `Some(true)` for `omnigraph mutate <name>`.\nWhen set and it disagrees with the stored query's actual kind, the\nserver rejects the call (400) so the verb asserts the kind. `None`\n(the default) skips the check — preserving older clients and aliases."
|
||||
},
|
||||
"params": {
|
||||
"description": "JSON object whose keys match the stored query's declared parameters."
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue