Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
2c305ca
fix(writes): charge Blob payload apart from the row around it
ragnorc Oct 8, 2026
9425d9f
feat(blob): put and clear one Blob cell from an embedded Session
ragnorc Oct 9, 2026
13572c1
test(matrix): read the photo baseline only for the Blob writers
ragnorc Oct 9, 2026
04b9e66
feat(server): PUT and DELETE one Blob cell over HTTP
ragnorc Oct 9, 2026
bbbd4e3
fix(loader): forecast Blob payload by the declared property type
ragnorc Oct 9, 2026
90b6688
Merge remote-tracking branch 'origin/main' into claude/blob-payload-b…
ragnorc Oct 9, 2026
5dd1b63
Merge branch 'claude/blob-payload-budget' into claude/blob-put-engine
ragnorc Oct 9, 2026
c9b4778
Merge branch 'claude/blob-put-engine' into claude/blob-put-http
ragnorc Oct 9, 2026
b5e3ea0
docs(server): say entity, not row, in the Blob write OpenAPI descript…
ragnorc Oct 9, 2026
2d9b84c
docs(server): keep the server guide within its page limit
ragnorc Oct 9, 2026
596c845
feat(cli): blob put and blob clear
ragnorc Oct 9, 2026
a4429d4
Merge remote-tracking branch 'origin/main' into claude/blob-payload-b…
ragnorc Oct 9, 2026
cffa791
Merge branch 'claude/blob-payload-budget' into claude/blob-put-engine
ragnorc Oct 9, 2026
6c6b455
Merge branch 'claude/blob-put-engine' into claude/blob-put-http
ragnorc Oct 9, 2026
f0aa601
Merge branch 'claude/blob-put-http' into claude/blob-put-cli
ragnorc Oct 9, 2026
b1b3cb7
Merge remote-tracking branch 'origin/main' into claude/blob-payload-b…
ragnorc Oct 10, 2026
d2b3860
Merge branch 'claude/blob-payload-budget' into claude/blob-put-engine
ragnorc Oct 10, 2026
1b9ae12
Merge branch 'claude/blob-put-engine' into claude/blob-put-http
ragnorc Oct 10, 2026
1c3e800
Merge branch 'claude/blob-put-http' into claude/blob-put-cli
ragnorc Oct 10, 2026
b14909b
ci: require the Blob write delivery-loss journey in Test Workspace
ragnorc Oct 10, 2026
ab4840d
Merge remote-tracking branch 'origin/main' into claude/blob-put-engine
ragnorc Oct 10, 2026
9392804
Merge branch 'claude/blob-put-engine' into claude/blob-put-http
ragnorc Oct 10, 2026
6a639d7
Merge branch 'claude/blob-put-http' into claude/blob-put-cli
ragnorc Oct 10, 2026
4a8a697
docs(changelog): rewrap the Blob put fragment
ragnorc Oct 10, 2026
3296e3b
Merge remote-tracking branch 'origin/main' into claude/blob-put-engine
ragnorc Oct 11, 2026
747be44
Merge branch 'claude/blob-put-engine' into claude/blob-put-http
ragnorc Oct 11, 2026
7126cf8
Merge branch 'claude/blob-put-http' into claude/blob-put-cli
ragnorc Oct 11, 2026
671641b
test(cli): observe the withheld Blob write reply instead of waiting o…
ragnorc Oct 11, 2026
c9ed133
docs(rfc-0033): route PUT refuses at the server's write_max_bytes; sp…
ragnorc Oct 11, 2026
3a10b70
Merge branch 'claude/blob-put-engine' into claude/blob-put-http
ragnorc Oct 11, 2026
2127c57
feat(server): refuse a Blob PUT past the server's write_max_bytes bef…
ragnorc Oct 11, 2026
398b124
Merge branch 'claude/blob-put-http' into claude/blob-put-cli
ragnorc Oct 11, 2026
86a056a
Merge remote-tracking branch 'origin/main' into claude/blob-put-http
ragnorc Oct 11, 2026
c3fef06
Merge branch 'claude/blob-put-http' into claude/blob-put-cli
ragnorc Oct 11, 2026
65f6518
Merge remote-tracking branch 'origin/main' into claude/blob-put-http
ragnorc Oct 11, 2026
5f2943f
Merge remote-tracking branch 'origin/main' into claude/blob-put-http
ragnorc Oct 11, 2026
af072a5
Merge branch 'claude/blob-put-http' into claude/blob-put-cli
ragnorc Oct 11, 2026
cd65a1f
Merge remote-tracking branch 'origin/main' into claude/blob-put-cli
ragnorc Oct 11, 2026
0128fe1
Merge remote-tracking branch 'origin/main' into claude/blob-put-cli
ragnorc Oct 11, 2026
e610d1d
test(cli): order the Blob delivery-loss imports for the integration b…
ragnorc Oct 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -752,6 +752,7 @@ jobs:
system_remote::remote_read_preserves_projection_order_in_json_and_csv \
system_remote::remote_branch_create_list_merge_flow \
system_remote::remote_merge_delivery_loss_never_replays_committed_effect \
system_remote::remote_blob_write_delivery_loss_never_replays_committed_effect \
system_remote::remote_merge_statement_delivery_loss_never_replays_committed_effect \
system_remote::remote_deployment_delivery_loss_preserves_owned_completion \
system_remote::remote_branch_delete_removes_branch \
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions changelog.d/blob-cell-put-clear.added.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@
is the one a read at that commit reports. One put holds at most the
session's `write_max_bytes` (32 MiB by default), inclusive; a larger one
fails before any table is opened with resource `Blob write payload bytes`.
The HTTP server exposes them as `PUT` and `DELETE /blob`; the CLI does not
expose them yet.
The HTTP server exposes them as `PUT` and `DELETE /blob`, and the CLI as
`blob put` and `blob clear`.
11 changes: 11 additions & 0 deletions changelog.d/blob-cli-put-clear.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
- `omnigraph blob put ENTITY TYPE ID PROPERTY` stores the raw bytes of
`--file PATH` or stdin in one Blob cell, and `omnigraph blob clear` sets a
nullable cell to null, on a `--store` graph or a served one. Both take
`--branch`, `--if-match TAG` (`*` or entity tags) and `--json`, and print the
receipt the server returns: the selector, branch, state, size and ETag of a
managed value, the actor and the exact commit, which is `null` for a clear of
a cell that was already null. Input over 32 MiB and a malformed `--if-match`
are refused before the graph is addressed, and an interactive stdin is
refused rather than waited on. `--as` names the actor of a `--store` write.
A served Blob precondition failure exits 4, as a graph-commit one does; an
embedded one exits 1. A write is sent once and never retried.
1 change: 1 addition & 0 deletions crates/omnigraph-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ name = "omnigraph"
path = "src/main.rs"

[dependencies]
bytes = { workspace = true }
omnigraph = { package = "omnigraph-engine", path = "../omnigraph", version = "0.13.0" }
omnigraph-compiler = { path = "../omnigraph-compiler", version = "0.13.0" }
omnigraph-api-types = { path = "../omnigraph-api-types", version = "0.13.0" }
Expand Down
108 changes: 103 additions & 5 deletions crates/omnigraph-cli/src/blob_cli.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
//! CLI-only shaping for graph-level Blob reads.
//! CLI-only shaping for graph-level Blob reads and writes.
//!
//! The engine and server own descriptor interpretation, snapshot fencing, and
//! bounded payload reads. This module keeps the command line's selector,
//! range, and error vocabulary identical across the embedded and served arms.

use std::fs::File;
use std::io::{self, Write};
use std::io::{self, IsTerminal, Read, Write};
use std::ops::Range;
use std::path::PathBuf;
use std::path::{Path, PathBuf};

use color_eyre::Report;
use color_eyre::eyre::{Result, eyre};
use omnigraph::db::{ReadTarget, SnapshotId};
use omnigraph::error::{ManifestErrorKind, OmniError};
use omnigraph::{BlobCell, EntityKind, ExternalBlobRef};
use omnigraph_api_types::{BlobEntityKind, BlobReadQuery};
use omnigraph::{BlobCell, BlobPrecondition, EntityKind, ExternalBlobRef};
use omnigraph_api_types::{BlobEntityKind, BlobReadQuery, BlobWriteQuery};
use reqwest::StatusCode;
use reqwest::header::{CONTENT_LENGTH, ETAG, HeaderMap, HeaderName};

Expand Down Expand Up @@ -164,6 +164,104 @@ pub(crate) fn blob_url(base_url: &str, query: &BlobReadQuery) -> Result<String>
crate::helpers::remote_url(base_url, &["blob"], &params)
}

pub(crate) fn blob_write_query(
entity: BlobEntityKind,
type_name: String,
id: String,
property: String,
branch: Option<String>,
) -> BlobWriteQuery {
BlobWriteQuery {
entity,
r#type: type_name,
id,
property,
branch,
}
}

pub(crate) fn blob_write_cell(query: &BlobWriteQuery) -> BlobCell {
BlobCell {
entity: match query.entity {
BlobEntityKind::Node => EntityKind::Node,
BlobEntityKind::Edge => EntityKind::Edge,
},
type_name: query.r#type.clone(),
id: query.id.clone(),
property: query.property.clone(),
}
}

pub(crate) fn blob_write_url(base_url: &str, query: &BlobWriteQuery) -> Result<String> {
let entity = match query.entity {
BlobEntityKind::Node => "node",
BlobEntityKind::Edge => "edge",
};
let mut params = vec![
("entity", entity),
("type", query.r#type.as_str()),
("id", query.id.as_str()),
("property", query.property.as_str()),
];
if let Some(branch) = query.branch.as_deref() {
params.push(("branch", branch));
}
crate::helpers::remote_url(base_url, &["blob"], &params)
}

/// Parse `--if-match` with the parser the server applies to the `If-Match`
/// header, so a malformed value is refused before any graph is addressed.
pub(crate) fn parse_if_match(raw: Option<&str>) -> Result<Option<BlobPrecondition>> {
omnigraph_api_types::parse_blob_if_match(raw.map(str::as_bytes)).map_err(|error| eyre!(error))
}

/// Read `blob put`'s bytes from `--file` or stdin, never both, before any
/// graph is addressed. Input over the put limit is refused with the resource
/// the engine and the server report, without reading past one more byte;
/// an interactive stdin is refused rather than waited on.
pub(crate) fn read_put_input(file: Option<&Path>) -> Result<bytes::Bytes> {
let limit = omnigraph::BLOB_WRITE_MAX_BYTES;
let too_large = |actual: u64| -> Report {
OmniError::resource_limit(omnigraph::BLOB_WRITE_PAYLOAD_RESOURCE, limit, actual).into()
};
let mut buffer = Vec::new();
match file {
Some(path) => {
let file = File::open(path)
.map_err(|error| eyre!("cannot read --file {}: {error}", path.display()))?;
let length = file
.metadata()
.map_err(|error| eyre!("cannot read --file {}: {error}", path.display()))?
.len();
if length > limit {
return Err(too_large(length));
}
buffer.reserve(usize::try_from(length).unwrap_or(0));
file.take(limit + 1)
.read_to_end(&mut buffer)
.map_err(|error| eyre!("cannot read --file {}: {error}", path.display()))?;
}
None => {
let stdin = io::stdin();
if stdin.is_terminal() {
return Err(eyre!(
"blob put reads its bytes from stdin; pipe them in or pass --file PATH"
));
}
stdin
.lock()
.take(limit + 1)
.read_to_end(&mut buffer)
.map_err(|error| eyre!("cannot read stdin: {error}"))?;
}
}
let read = buffer.len() as u64;
if read > limit {
return Err(too_large(read));
}
Ok(bytes::Bytes::from(buffer))
}

pub(crate) fn whole_external_uri(reference: &ExternalBlobRef) -> Result<&str> {
reference
.whole_object_uri()
Expand Down
57 changes: 56 additions & 1 deletion crates/omnigraph-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ pub(crate) enum Command {
#[arg(long = "type")]
type_names: Vec<String>,
},
/// Read one logical node or edge Blob cell.
/// Read, replace or clear one logical node or edge Blob cell.
Blob {
#[command(subcommand)]
command: BlobCommand,
Expand Down Expand Up @@ -516,6 +516,61 @@ pub(crate) enum BlobCommand {
#[arg(long)]
json: bool,
},
/// Replace one Blob value of an existing node or edge with raw bytes from
/// a file or stdin, as one graph commit.
Put {
/// Logical entity namespace.
#[arg(value_name = "ENTITY")]
entity: BlobEntityArg,
/// Accepted-schema node or edge type.
#[arg(value_name = "TYPE")]
type_name: String,
/// Logical entity id; the entity must exist.
#[arg(value_name = "ID")]
id: String,
/// Blob property name.
#[arg(value_name = "PROPERTY")]
property: String,
/// Write a named branch (defaults to main).
#[arg(long)]
branch: Option<String>,
/// Read the bytes from this file instead of stdin.
#[arg(long, value_name = "PATH")]
file: Option<PathBuf>,
/// Write only while the cell's ETag is one of these entity tags, or,
/// as `*`, while the cell holds any value.
#[arg(long, value_name = "TAG")]
if_match: Option<String>,
/// Emit the receipt as JSON.
#[arg(long)]
json: bool,
},
/// Set one nullable Blob value of an existing node or edge to null, as one
/// graph commit; a cell that is already null publishes nothing.
Clear {
/// Logical entity namespace.
#[arg(value_name = "ENTITY")]
entity: BlobEntityArg,
/// Accepted-schema node or edge type.
#[arg(value_name = "TYPE")]
type_name: String,
/// Logical entity id; the entity must exist.
#[arg(value_name = "ID")]
id: String,
/// Blob property name.
#[arg(value_name = "PROPERTY")]
property: String,
/// Write a named branch (defaults to main).
#[arg(long)]
branch: Option<String>,
/// Write only while the cell's ETag is one of these entity tags, or,
/// as `*`, while the cell holds any value.
#[arg(long, value_name = "TAG")]
if_match: Option<String>,
/// Emit the receipt as JSON.
#[arg(long)]
json: bool,
},
}

#[derive(Debug, Subcommand)]
Expand Down
Loading
Loading