diff --git a/changelog.d/write-max-bytes.added.md b/changelog.d/write-max-bytes.added.md new file mode 100644 index 000000000..9e9305570 --- /dev/null +++ b/changelog.d/write-max-bytes.added.md @@ -0,0 +1,10 @@ +- Bounded writes now accept the [`write_max_bytes` session setting][write-max-bytes], + from `1` through `33554432` bytes (the unchanged 32 MiB default), with + `OMNIGRAPH_WRITE_MAX_BYTES` supplying the process default. + Row data and logical Blob payloads have independent allowances, so a Blob exactly + at the limit can accompany scalar metadata that fits its own allowance. + The default now admits up to 32 MiB of row data plus 32 MiB of Blob payload + per operation, where the previous single 32 MiB allowance covered both. + HTTP body, Blob read, compaction and URI metadata limits remain independent. + +[write-max-bytes]: ../docs/user/queries/settings.md diff --git a/crates/omnigraph-api-types/src/lib.rs b/crates/omnigraph-api-types/src/lib.rs index 2ba5d0151..a87eec643 100644 --- a/crates/omnigraph-api-types/src/lib.rs +++ b/crates/omnigraph-api-types/src/lib.rs @@ -109,6 +109,10 @@ pub struct SettingsRequest { #[serde(default, skip_serializing_if = "Option::is_none")] #[schema(schema_with = traversal_work_limit_schema)] pub traversal_work_limit: Option, + /// `write_max_bytes`: independent row-data and Blob-payload allowances, `1..=33554432`. + #[serde(default, skip_serializing_if = "Option::is_none")] + #[schema(schema_with = write_max_bytes_schema)] + pub write_max_bytes: Option, /// `history_release_bytes`: the byte budget of a branch's buffer of /// unreleased commits for this request's mutate or merge publishes, `1024..=262144`; an /// `i64` like `traversal_work_limit`. @@ -140,6 +144,9 @@ impl SettingsRequest { if let Some(limit) = self.traversal_work_limit { assignments.push((SettingId::TraversalWorkLimit, SettingValue::Integer(limit))); } + if let Some(bytes) = self.write_max_bytes { + assignments.push((SettingId::WriteMaxBytes, SettingValue::Integer(bytes))); + } if let Some(bytes) = self.history_release_bytes { assignments.push((SettingId::HistoryReleaseBytes, SettingValue::Integer(bytes))); } @@ -181,6 +188,10 @@ fn traversal_work_limit_schema() -> utoipa::openapi::schema::Object { setting_schema(SettingId::TraversalWorkLimit) } +fn write_max_bytes_schema() -> utoipa::openapi::schema::Object { + setting_schema(SettingId::WriteMaxBytes) +} + fn history_release_bytes_schema() -> utoipa::openapi::schema::Object { setting_schema(SettingId::HistoryReleaseBytes) } @@ -2209,13 +2220,19 @@ mod tests { merge_lineage: Some(MergeLineage::Off), ann_nprobes: Some(7), traversal_work_limit: Some(123), + write_max_bytes: Some(4093), history_release_bytes: Some(2048), }; let expected = format!( - "{{\"{}\":\"v2\",\"{}\":\"off\",\"{}\":7,\"{}\":123,\"{}\":2048}}", - request_rows[0], request_rows[1], request_rows[2], request_rows[3], request_rows[4] + "{{\"{}\":\"v2\",\"{}\":\"off\",\"{}\":7,\"{}\":123,\"{}\":4093,\"{}\":2048}}", + request_rows[0], + request_rows[1], + request_rows[2], + request_rows[3], + request_rows[4], + request_rows[5] ); - assert_eq!(request_rows.len(), 5); + assert_eq!(request_rows.len(), 6); assert_eq!(serde_json::to_string(&populated).unwrap(), expected); assert_eq!( populated diff --git a/crates/omnigraph-cli/src/client.rs b/crates/omnigraph-cli/src/client.rs index 37f0c63a9..19a9f5cc0 100644 --- a/crates/omnigraph-cli/src/client.rs +++ b/crates/omnigraph-cli/src/client.rs @@ -518,6 +518,9 @@ impl GraphClient { request.traversal_work_limit = Some(given.get(SettingId::TraversalWorkLimit).parse()?) } + SettingId::WriteMaxBytes => { + request.write_max_bytes = Some(given.get(SettingId::WriteMaxBytes).parse()?) + } SettingId::HistoryReleaseBytes => { request.history_release_bytes = Some(given.get(SettingId::HistoryReleaseBytes).parse()?) @@ -2654,9 +2657,12 @@ mod tests { ]); let client = GraphClient::managed(&server.origin, "knowledge", "data-credential".into()).unwrap(); - let settings = - parse_set_flags(&["merge_lineage=off".to_string(), "ann_nprobes=1".to_string()]) - .unwrap(); + let settings = parse_set_flags(&[ + "merge_lineage=off".to_string(), + "ann_nprobes=1".to_string(), + "write_max_bytes=4093".to_string(), + ]) + .unwrap(); client .query( ReadTarget::branch("main"), @@ -2699,7 +2705,7 @@ mod tests { assert!(requests[4].body.get("query_source").is_none()); assert!(requests[4].body.get("query_name").is_none()); assert!(requests[4].body.get("settings").is_none()); - let field = json!({"merge_lineage": "off", "ann_nprobes": 1}); + let field = json!({"merge_lineage": "off", "ann_nprobes": 1, "write_max_bytes": 4093}); assert_eq!(requests[0].path, "/graphs/knowledge/query"); assert_eq!(requests[0].body["settings"], field); assert_eq!( diff --git a/crates/omnigraph-compiler/src/settings.rs b/crates/omnigraph-compiler/src/settings.rs index 7aa17255e..4769fd047 100644 --- a/crates/omnigraph-compiler/src/settings.rs +++ b/crates/omnigraph-compiler/src/settings.rs @@ -110,6 +110,17 @@ pub const DEFINITIONS: &[SettingSpec] = &[ env: "OMNIGRAPH_TRAVERSAL_WORK_LIMIT", doc: "the finite work budget shared by edge selections in one query; exhaustion terminates the query with an error", }, + SettingSpec { + name: "write_max_bytes", + kind: SettingKind::Integer { + min: 1, + max: Some(33_554_432), + }, + default: "33554432", + scope: SettingScope::Request, + env: "OMNIGRAPH_WRITE_MAX_BYTES", + doc: "the byte allowance for bounded writes by mutate, load and branch merge; entity data and materialized Blob payloads each have this independent allowance; lowering it preserves the existing per-type, per-operation and per-chunk scopes", + }, SettingSpec { name: "history_release_bytes", kind: SettingKind::Integer { @@ -132,6 +143,7 @@ pub enum SettingId { AnnNprobes, StageWriteConcurrency, TraversalWorkLimit, + WriteMaxBytes, HistoryReleaseBytes, } @@ -144,6 +156,7 @@ impl SettingId { SettingId::AnnNprobes, SettingId::StageWriteConcurrency, SettingId::TraversalWorkLimit, + SettingId::WriteMaxBytes, SettingId::HistoryReleaseBytes, ]; @@ -355,6 +368,7 @@ pub struct SessionSettings { ann_nprobes: Option, stage_write_concurrency: usize, traversal_work_limit: u64, + write_max_bytes: u64, history_release_bytes: usize, } @@ -370,6 +384,7 @@ impl Default for SessionSettings { ann_nprobes: None, stage_write_concurrency: 1, traversal_work_limit: 1, + write_max_bytes: 1, history_release_bytes: 1024, }; for id in SettingId::ALL { @@ -436,6 +451,10 @@ impl SessionSettings { self.stage_write_concurrency = usize::try_from(width).expect("invariant: the row's range is 1..=64"); } + SettingId::WriteMaxBytes => { + self.write_max_bytes = u64::try_from(parse_integer(id, value)?) + .expect("invariant: the row's range is 1..=33554432"); + } SettingId::HistoryReleaseBytes => { self.history_release_bytes = usize::try_from(parse_integer(id, value)?) .expect("invariant: the row's range is 1024..=262144"); @@ -456,6 +475,7 @@ impl SessionSettings { SettingId::StageWriteConcurrency => { self.stage_write_concurrency = from.stage_write_concurrency; } + SettingId::WriteMaxBytes => self.write_max_bytes = from.write_max_bytes, SettingId::HistoryReleaseBytes => { self.history_release_bytes = from.history_release_bytes; } @@ -471,6 +491,7 @@ impl SessionSettings { SettingId::AnnNprobes => self.ann_nprobes.unwrap_or(0).to_string(), SettingId::StageWriteConcurrency => self.stage_write_concurrency.to_string(), SettingId::TraversalWorkLimit => self.traversal_work_limit.to_string(), + SettingId::WriteMaxBytes => self.write_max_bytes.to_string(), SettingId::HistoryReleaseBytes => self.history_release_bytes.to_string(), } } @@ -524,6 +545,11 @@ impl SessionSettings { self.stage_write_concurrency } + /// The independent row-data and Blob-payload allowances for bounded writes. + pub fn write_max_bytes(&self) -> u64 { + self.write_max_bytes + } + /// The byte budget of a branch's buffer of unreleased commits for this /// session's publishes; the default is the production budget and the /// ceiling, so a session may only lower it. @@ -730,6 +756,44 @@ mod tests { assert_eq!(copied, upper); } + #[test] + fn write_max_bytes_row_bounds_and_environment() { + let id = SettingId::parse("write_max_bytes").unwrap(); + let defaults = SessionSettings::default(); + assert_eq!(defaults.get(id), "33554432"); + id.refuse_from_request().unwrap(); + for value in ["1", "4093", "33554432"] { + let changed = defaults.clone().with("write_max_bytes", value).unwrap(); + assert_eq!(changed.get(id), value); + let mut copied = defaults.clone(); + copied.copy_field(&changed, id); + assert_eq!(copied, changed); + } + for value in ["0", "-1", "33554433", "9223372036854775808"] { + let error = defaults.clone().with("write_max_bytes", value).unwrap_err(); + assert!(error.to_string().contains("an integer in 1..=33554432")); + } + for value in ["many", "\"4093\""] { + assert!(defaults.clone().with("write_max_bytes", value).is_err()); + } + let (environment, sources) = + from_env_with(|name| (name == "OMNIGRAPH_WRITE_MAX_BYTES").then(|| "4093".to_string())) + .unwrap(); + assert_eq!(environment.get(id), "4093"); + assert_eq!(sources[id as usize], Source::Env); + assert_eq!( + environment + .with("write_max_bytes", "33554432") + .unwrap() + .get(id), + "33554432" + ); + let error = + from_env_with(|name| (name == "OMNIGRAPH_WRITE_MAX_BYTES").then(|| "0".to_string())) + .unwrap_err(); + assert!(error.to_string().contains("OMNIGRAPH_WRITE_MAX_BYTES")); + } + #[test] fn history_release_bytes_row_bounds() { let defaults = SessionSettings::default(); @@ -802,7 +866,7 @@ mod tests { let err = SettingId::parse("merge_linage").unwrap_err(); assert_eq!( err.to_string(), - "unknown setting `merge_linage`; expected one of engine, rrf_plan, merge_lineage, ann_nprobes, stage_write_concurrency, traversal_work_limit, history_release_bytes" + "unknown setting `merge_linage`; expected one of engine, rrf_plan, merge_lineage, ann_nprobes, stage_write_concurrency, traversal_work_limit, write_max_bytes, history_release_bytes" ); let err = SessionSettings::default().with("engine", "v1").unwrap_err(); assert_eq!( diff --git a/crates/omnigraph-gqt/cases/load_write_max_bytes_edge_blob_value.gqt b/crates/omnigraph-gqt/cases/load_write_max_bytes_edge_blob_value.gqt new file mode 100644 index 000000000..00cf7ae5c --- /dev/null +++ b/crates/omnigraph-gqt/cases/load_write_max_bytes_edge_blob_value.gqt @@ -0,0 +1,70 @@ +# issue: none +# notes: Decoded base64: bytes on an edge property are bounded by the session's write_max_bytes: 4,097 bytes are refused at 4096. +# notes: Exactly one Cites row at the end shows the refused load wrote no edge. +# notes: No omnigraph-server environment: served runs refuse settings steps and the HTTP load routes take no setting. + +--- runner +timeout_ms: 10000 +environments: + - target: omnigraph-engine + storage: local-filesystem + - target: omnigraph-engine-dst + storage: in-memory-object-store + seeds: [0, 42] + +--- schema +node Doc { + slug: String @key +} +edge Cites: Doc -> Doc { + attachment: Blob? +} + +--- seed +{"type":"Doc","data":{"slug":"a"}} +{"type":"Doc","data":{"slug":"b"}} + +--- mutate +set write_max_bytes = 4096; + +--- expect ok + +--- load generate: v1 seed: 1 mode: append +tables: + - kind: edge + name: Cites + rows: 1 + commits: 1 + from: {kind: literal, value: a} + to: {kind: literal, value: b} + columns: + attachment: + kind: literal + value: "base64:QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUE=" +--- expect error: decoded blob input bytes + +--- load generate: v1 seed: 1 mode: append +tables: + - kind: edge + name: Cites + rows: 1 + commits: 1 + from: {kind: literal, value: a} + to: {kind: literal, value: b} + columns: + attachment: {kind: literal, value: "base64:QUFB"} +--- expect ok + +--- query +query cites() { + match { + $a: Doc + $a cites $b + } + return { $a.slug, $b.slug } +} +--- expect unordered +{"a.slug": "a", "b.slug": "b"} +--- expect shape +a.slug: String +b.slug: String diff --git a/crates/omnigraph-gqt/cases/load_write_max_bytes_merge_blob_refusal.gqt b/crates/omnigraph-gqt/cases/load_write_max_bytes_merge_blob_refusal.gqt new file mode 100644 index 000000000..0d8cc28d1 --- /dev/null +++ b/crates/omnigraph-gqt/cases/load_write_max_bytes_merge_blob_refusal.gqt @@ -0,0 +1,72 @@ +# issue: none +# notes: A merge-mode load uses the session's write_max_bytes: a 4,097-byte base64: value is refused at 4096 and leaves the row unchanged, a 4,096-byte value loads. +# notes: Blob bytes cannot be projected, so the observable is the title written in the same upsert. +# notes: No omnigraph-server environment: served runs refuse settings steps and the HTTP load routes take no setting. + +--- runner +timeout_ms: 10000 +environments: + - target: omnigraph-engine + storage: local-filesystem + - target: omnigraph-engine-dst + storage: in-memory-object-store + seeds: [0, 42] + +--- schema +node Doc { + slug: String @key + title: String + content: Blob? +} + +--- seed +{"type":"Doc","data":{"slug":"a","title":"old","content":"base64:QUFB"}} + +--- mutate +set write_max_bytes = 4096; + +--- expect ok + +--- load generate: v1 seed: 1 mode: merge +tables: + - kind: node + name: Doc + rows: 1 + commits: 1 + columns: + slug: {kind: literal, value: a} + title: {kind: literal, value: new} + content: + kind: literal + value: "base64:QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUE=" +--- expect error: decoded blob input bytes + +--- query +query after_refusal() { match { $d: Doc } return { $d.slug, $d.title } } +--- expect unordered +{"d.slug": "a", "d.title": "old"} +--- expect shape +d.slug: String +d.title: String + +--- load generate: v1 seed: 1 mode: merge +tables: + - kind: node + name: Doc + rows: 1 + commits: 1 + columns: + slug: {kind: literal, value: a} + title: {kind: literal, value: new} + content: + kind: literal + value: "base64:QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQQ==" +--- expect ok + +--- query +query after_exact() { match { $d: Doc } return { $d.slug, $d.title } } +--- expect unordered +{"d.slug": "a", "d.title": "new"} +--- expect shape +d.slug: String +d.title: String diff --git a/crates/omnigraph-gqt/cases/settings_show_set_reset.gqt b/crates/omnigraph-gqt/cases/settings_show_set_reset.gqt index 40c1fd27a..516258afc 100644 --- a/crates/omnigraph-gqt/cases/settings_show_set_reset.gqt +++ b/crates/omnigraph-gqt/cases/settings_show_set_reset.gqt @@ -31,6 +31,7 @@ show all; {"name": "ann_nprobes", "value": "20", "default": "20", "source": "default", "scope": "request"} {"name": "stage_write_concurrency", "value": "8", "default": "8", "source": "default", "scope": "process"} {"name": "traversal_work_limit", "value": "1000000", "default": "1000000", "source": "default", "scope": "request"} +{"name": "write_max_bytes", "value": "33554432", "default": "33554432", "source": "default", "scope": "request"} {"name": "history_release_bytes", "value": "262144", "default": "262144", "source": "default", "scope": "request"} --- query @@ -119,3 +120,25 @@ show history_release_bytes; --- expect unordered {"name": "history_release_bytes", "value": "262144", "default": "262144", "source": "default", "scope": "request"} + +--- mutate +set write_max_bytes = 4093; + +--- expect ok + +--- query +show write_max_bytes; + +--- expect unordered +{"name": "write_max_bytes", "value": "4093", "default": "33554432", "source": "file", "scope": "request"} + +--- mutate +reset write_max_bytes; + +--- expect ok + +--- query +show write_max_bytes; + +--- expect unordered +{"name": "write_max_bytes", "value": "33554432", "default": "33554432", "source": "default", "scope": "request"} diff --git a/crates/omnigraph-gqt/cases/settings_survive_restart.gqt b/crates/omnigraph-gqt/cases/settings_survive_restart.gqt index e34c23377..6fb6f048f 100644 --- a/crates/omnigraph-gqt/cases/settings_survive_restart.gqt +++ b/crates/omnigraph-gqt/cases/settings_survive_restart.gqt @@ -20,6 +20,7 @@ node Person { --- mutate set merge_lineage = off; +set write_max_bytes = 4093; --- expect ok @@ -31,6 +32,12 @@ show merge_lineage; --- expect unordered {"name": "merge_lineage", "value": "off", "default": "on", "source": "file", "scope": "request"} +--- query +show write_max_bytes; + +--- expect unordered +{"name": "write_max_bytes", "value": "4093", "default": "33554432", "source": "file", "scope": "request"} + --- mutate reset all; @@ -41,3 +48,9 @@ show merge_lineage; --- expect unordered {"name": "merge_lineage", "value": "verify", "default": "on", "source": "default", "scope": "request"} + +--- query +show write_max_bytes; + +--- expect unordered +{"name": "write_max_bytes", "value": "33554432", "default": "33554432", "source": "default", "scope": "request"} diff --git a/crates/omnigraph-gqt/cases/write_max_bytes.gqt b/crates/omnigraph-gqt/cases/write_max_bytes.gqt new file mode 100644 index 000000000..148527d01 --- /dev/null +++ b/crates/omnigraph-gqt/cases/write_max_bytes.gqt @@ -0,0 +1,138 @@ +# issue: none +# notes: The configured write allowance independently bounds row data and Blob payloads. +# notes: Exact payloads coexist with scalar metadata; refused writes leave the accepted row intact. + +--- runner +timeout_ms: 10000 +environments: + - target: omnigraph-engine + storage: local-filesystem + - target: omnigraph-engine-dst + storage: in-memory-object-store + seeds: [0, 42] + - target: omnigraph-server + storage: local-filesystem + +--- schema +node Doc { + slug: String @key + content: Blob? + note: String +} + +node Image { + slug: String @key + content: Blob? +} + +--- seed + +--- mutate +set write_max_bytes = 4093; +query exact($payload: Blob) { + insert Doc { slug: "accepted", content: $payload, note: "scalar" } +} + +--- params +{"payload": "base64:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="} + +--- expect affected: nodes=1 edges=0 + +--- mutate +set write_max_bytes = 4093; +query too_large($payload: Blob) { + insert Doc { slug: "refused", content: $payload, note: "scalar" } +} + +--- params +{"payload": "base64:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="} + +--- expect error: resource limit exceeded + +--- mutate +set write_max_bytes = 4093; +query aggregate($payload: Blob) { + insert Doc { slug: "a", content: $payload, note: "scalar" } + insert Doc { slug: "b", content: $payload, note: "scalar" } +} + +--- params +{"payload": "base64:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="} + +--- expect error: resource limit exceeded + +--- mutate +set write_max_bytes = 4093; +query cross_table($payload: Blob) { + insert Doc { slug: "cross-doc", content: $payload, note: "scalar" } + insert Image { slug: "cross-image", content: $payload } +} + +--- params +{"payload": "base64:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="} + +--- expect error: decoded blob input bytes per operation + +--- query +query images() { + match { $i: Image } + return { $i.slug } +} + +--- expect unordered + +--- expect shape +i.slug: String + +--- mutate +set write_max_bytes = 4093; +query wide_scalar($note: String) { + insert Doc { slug: "wide", note: $note } +} + +--- params +{"note": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"} + +--- expect error: resource limit exceeded + +--- mutate +set write_max_bytes = 4093; +query carry_payload() { + update Doc set { note: "updated" } where slug = "accepted" +} + +--- expect affected: nodes=1 edges=0 + +--- mutate +set write_max_bytes = 1; +query removed_ids() { + delete Doc where slug = "accepted" +} + +--- expect error: retained removed-id bytes per operation + +--- query +query rows() { + match { $d: Doc } + return { $d.slug, $d.note } +} + +--- expect unordered +{"d.slug":"accepted","d.note":"updated"} + +--- expect shape +d.slug: String +d.note: String + +--- mutate +set write_max_bytes = 8190; +query three_payloads($payload: Blob) { + insert Doc { slug: "capacity-a", content: $payload, note: "first" } + insert Doc { slug: "capacity-b", content: $payload, note: "second" } + insert Doc { slug: "capacity-c", content: $payload, note: "third" } +} + +--- params +{"payload": "base64:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"} + +--- expect affected: nodes=3 edges=0 diff --git a/crates/omnigraph-gqt/cases/write_max_bytes_insert_then_update_same_row.gqt b/crates/omnigraph-gqt/cases/write_max_bytes_insert_then_update_same_row.gqt new file mode 100644 index 000000000..0bdbf8d84 --- /dev/null +++ b/crates/omnigraph-gqt/cases/write_max_bytes_insert_then_update_same_row.gqt @@ -0,0 +1,45 @@ +# issue: none +# notes: An insert and an update of the same key in one mutation are two staged batches until the last-write-wins fold; the update carries the pending row's Blob, so the payload counts twice as input. +# notes: The 2,115-byte payload reaches 4,230 at write_max_bytes = 4093 and is refused as `materialized blob payload bytes` before any write; the table stays empty. + +--- runner +timeout_ms: 10000 +environments: + - target: omnigraph-engine + storage: local-filesystem + - target: omnigraph-engine-dst + storage: in-memory-object-store + seeds: [0, 42] + +--- schema +node Doc { + slug: String @key + content: Blob? + note: String +} + +--- seed + +--- mutate +set write_max_bytes = 4093; +query insert_then_update($payload: Blob) { + insert Doc { slug: "twice", content: $payload, note: "n" } + update Doc set { note: "u" } where slug = "twice" +} + +--- params +{"payload": "base64:ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567"} + +--- expect error: materialized blob payload bytes + +--- query +query docs() { + match { $d: Doc } + return { $d.slug, $d.note } +} + +--- expect unordered + +--- expect shape +d.slug: String +d.note: String diff --git a/crates/omnigraph-gqt/cases/write_max_bytes_merge.gqt b/crates/omnigraph-gqt/cases/write_max_bytes_merge.gqt new file mode 100644 index 000000000..2447087ab --- /dev/null +++ b/crates/omnigraph-gqt/cases/write_max_bytes_merge.gqt @@ -0,0 +1,82 @@ +# issue: none +# notes: A materializing merge captures the selected write allowance through publication. +# notes: A refused merge leaves target rows unchanged; the exact payload fits with scalar metadata. + +--- runner +timeout_ms: 10000 +environments: + - target: omnigraph-engine + storage: local-filesystem + - target: omnigraph-engine-dst + storage: in-memory-object-store + seeds: [0, 42] + +--- schema +node Doc { + slug: String @key + content: Blob? + note: String +} + +--- seed + +--- mutate +branch create source; + +--- expect ok + +--- mutate branch: source +query insert_source($payload: Blob) { + insert Doc { slug: "source", content: $payload, note: "source" } +} + +--- params +{"payload": "base64:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="} + +--- expect affected: nodes=1 edges=0 + +--- mutate +query insert_target() { + insert Doc { slug: "target", note: "target" } +} + +--- expect affected: nodes=1 edges=0 + +--- mutate +set write_max_bytes = 4092; +branch merge source into main; + +--- expect error: materialized blob payload bytes + +--- query +query target_before_merge() { + match { $d: Doc } + return { $d.slug, $d.note } +} + +--- expect unordered +{"d.slug":"target","d.note":"target"} + +--- expect shape +d.slug: String +d.note: String + +--- mutate +set write_max_bytes = 4093; +branch merge source into main; + +--- expect outcome: merged + +--- query +query target_after_merge() { + match { $d: Doc } + return { $d.slug, $d.note } +} + +--- expect unordered +{"d.slug":"source","d.note":"source"} +{"d.slug":"target","d.note":"target"} + +--- expect shape +d.slug: String +d.note: String diff --git a/crates/omnigraph-gqt/cases/write_max_bytes_merge_across_types.gqt b/crates/omnigraph-gqt/cases/write_max_bytes_merge_across_types.gqt new file mode 100644 index 000000000..ddba957fa --- /dev/null +++ b/crates/omnigraph-gqt/cases/write_max_bytes_merge_across_types.gqt @@ -0,0 +1,86 @@ +# issue: none +# notes: A merge that writes rows counts Blob payloads across types together: Doc and Image each carry 4,500 bytes, 9,000 in all. +# notes: Main diverges on both tables, so neither is adopted by pointer; write_max_bytes = 8190 refuses the merge and leaves main unchanged, and the default merge succeeds. + +--- runner +timeout_ms: 10000 +environments: + - target: omnigraph-engine + storage: local-filesystem + - target: omnigraph-engine-dst + storage: in-memory-object-store + seeds: [0, 42] + +--- schema +node Doc { + slug: String @key + content: Blob? + note: String +} + +node Image { + slug: String @key + content: Blob? +} + +--- seed + +--- mutate +branch create source; + +--- expect ok + +--- mutate branch: source +query insert_source($payload: Blob) { + insert Doc { slug: "d", content: $payload, note: "source" } + insert Image { slug: "i", content: $payload } +} + +--- params +{"payload": "base64:AQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQIDAQID"} + +--- expect affected: nodes=2 edges=0 + +--- mutate +query diverge_main() { + insert Doc { slug: "target", note: "target" } + insert Image { slug: "target-image" } +} + +--- expect affected: nodes=2 edges=0 + +--- mutate +set write_max_bytes = 8190; +branch merge source into main; + +--- expect error: materialized blob payload bytes + +--- query +query images_after_refusal() { + match { $i: Image } + return { $i.slug } +} + +--- expect unordered +{"i.slug":"target-image"} + +--- expect shape +i.slug: String + +--- mutate +branch merge source into main; + +--- expect outcome: merged + +--- query +query images_after_merge() { + match { $i: Image } + return { $i.slug } +} + +--- expect unordered +{"i.slug":"i"} +{"i.slug":"target-image"} + +--- expect shape +i.slug: String diff --git a/crates/omnigraph-gqt/cases/write_max_bytes_merge_pointer_adoption.gqt b/crates/omnigraph-gqt/cases/write_max_bytes_merge_pointer_adoption.gqt new file mode 100644 index 000000000..31c261109 --- /dev/null +++ b/crates/omnigraph-gqt/cases/write_max_bytes_merge_pointer_adoption.gqt @@ -0,0 +1,58 @@ +# issue: none +# notes: Merging main into feature adopts main's node:Doc table by pointer and writes no row, so write_max_bytes does not bound it. +# notes: Under write_max_bytes = 128, below one row's Arrow size, the merge still fast-forwards and feature reads main's update. + +--- runner +timeout_ms: 10000 +environments: + - target: omnigraph-engine + storage: local-filesystem + - target: omnigraph-engine-dst + storage: in-memory-object-store + seeds: [0, 42] + +--- schema +node Doc { + slug: String @key + note: String +} + +--- seed + +--- mutate +query seed_doc() { + insert Doc { slug: "w", note: "short" } +} + +--- expect affected: nodes=1 edges=0 + +--- mutate +branch create feature; + +--- expect ok + +--- mutate +query lengthen() { + update Doc set { note: "long" } where slug = "w" +} + +--- expect affected: nodes=1 edges=0 + +--- mutate +set write_max_bytes = 128; +branch merge main into feature; + +--- expect outcome: fast_forward + +--- query branch: feature +query feature_rows() { + match { $d: Doc } + return { $d.slug, $d.note } +} + +--- expect unordered +{"d.slug":"w","d.note":"long"} + +--- expect shape +d.slug: String +d.note: String diff --git a/crates/omnigraph-gqt/cases/write_max_bytes_removed_id_charge.gqt b/crates/omnigraph-gqt/cases/write_max_bytes_removed_id_charge.gqt new file mode 100644 index 000000000..74580f0cb --- /dev/null +++ b/crates/omnigraph-gqt/cases/write_max_bytes_removed_id_charge.gqt @@ -0,0 +1,125 @@ +# issue: none +# notes: A removed ID is charged its UTF-8 length plus 24 bytes, and a @key node's ID is its key value: "日" (3 bytes) costs 27, so 26 refuses and 27 admits. +# notes: "a" and "b" cost 50: 49 refuses and 50 admits. "x" plus its one edge with a generated 26-byte ID costs 75: 74 refuses with node and edge intact, 75 admits and cascades. + +--- runner +timeout_ms: 10000 +environments: + - target: omnigraph-engine + storage: local-filesystem + - target: omnigraph-engine-dst + storage: in-memory-object-store + seeds: [0, 42] + +--- schema +node Doc { + slug: String @key + grp: String +} +edge Cites: Doc -> Doc + +--- seed +{"type":"Doc","data":{"slug":"日","grp":"solo"}} +{"type":"Doc","data":{"slug":"a","grp":"pair"}} +{"type":"Doc","data":{"slug":"b","grp":"pair"}} +{"type":"Doc","data":{"slug":"x","grp":"hub"}} +{"type":"Doc","data":{"slug":"y","grp":"leaf"}} +{"edge":"Cites","from":"x","to":"y"} + +--- mutate +set write_max_bytes = 26; +query delete_multibyte_under() { + delete Doc where slug = "日" +} + +--- expect error: retained removed-id bytes per operation + +--- mutate +set write_max_bytes = 27; +query delete_multibyte_exact() { + delete Doc where slug = "日" +} + +--- expect affected: nodes=1 edges=0 + +--- mutate +set write_max_bytes = 49; +query delete_pair_under() { + delete Doc where grp = "pair" +} + +--- expect error: retained removed-id bytes per operation + +--- mutate +set write_max_bytes = 50; +query delete_pair_exact() { + delete Doc where grp = "pair" +} + +--- expect affected: nodes=2 edges=0 + +--- mutate +set write_max_bytes = 74; +query delete_hub_under() { + delete Doc where slug = "x" +} + +--- expect error: retained removed-id bytes per operation + +--- query +query docs_after_refused_cascade() { + match { $d: Doc } + return { $d.slug } +} + +--- expect unordered +{"d.slug":"x"} +{"d.slug":"y"} + +--- expect shape +d.slug: String + +--- query +query cites_after_refused_cascade() { + match { $a: Doc $a $e:cites $b } + return { $a.slug, $b.slug } +} + +--- expect unordered +{"a.slug":"x","b.slug":"y"} + +--- expect shape +a.slug: String +b.slug: String + +--- mutate +set write_max_bytes = 75; +query delete_hub_exact() { + delete Doc where slug = "x" +} + +--- expect affected: nodes=1 edges=1 + +--- query +query docs_after_cascade() { + match { $d: Doc } + return { $d.slug } +} + +--- expect unordered +{"d.slug":"y"} + +--- expect shape +d.slug: String + +--- query +query cites_after_cascade() { + match { $a: Doc $a $e:cites $b } + return { $a.slug, $b.slug } +} + +--- expect unordered + +--- expect shape +a.slug: String +b.slug: String diff --git a/crates/omnigraph-gqt/cases/write_max_bytes_session_value_reaches_writes.gqt b/crates/omnigraph-gqt/cases/write_max_bytes_session_value_reaches_writes.gqt new file mode 100644 index 000000000..d2cf641f8 --- /dev/null +++ b/crates/omnigraph-gqt/cases/write_max_bytes_session_value_reaches_writes.gqt @@ -0,0 +1,85 @@ +# issue: none +# notes: Under write_max_bytes = 1 every row exceeds the row allowance, so each refusal is forced by the setting; the same insert under the default is admitted. +# notes: A set prefix lowers only its own step; a settings-only step lowers every later write, survives a restart, and reset restores the default; a refused insert leaves no row, so its retry inserts the key once. + +--- runner +timeout_ms: 10000 +environments: + - target: omnigraph-engine + storage: local-filesystem + - target: omnigraph-engine-dst + storage: in-memory-object-store + seeds: [0, 42] + +--- schema +node Doc { + slug: String @key +} + +--- seed + +--- mutate +set write_max_bytes = 1; +query add_first() { + insert Doc { slug: "first" } +} + +--- expect error: resource limit exceeded + +--- mutate +query retry_first() { + insert Doc { slug: "first" } +} + +--- expect affected: nodes=1 edges=0 + +--- mutate +set write_max_bytes = 1; + +--- expect ok + +--- restart + +--- mutate +query add_second() { + insert Doc { slug: "second" } +} + +--- expect error: resource limit exceeded + +--- query +query docs_while_lowered() { + match { $d: Doc } + return { $d.slug } +} + +--- expect unordered +{"d.slug":"first"} + +--- expect shape +d.slug: String + +--- mutate +reset write_max_bytes; + +--- expect ok + +--- mutate +query retry_second() { + insert Doc { slug: "second" } +} + +--- expect affected: nodes=1 edges=0 + +--- query +query docs_after_reset() { + match { $d: Doc } + return { $d.slug } +} + +--- expect unordered +{"d.slug":"first"} +{"d.slug":"second"} + +--- expect shape +d.slug: String diff --git a/crates/omnigraph-gqt/cases/write_max_bytes_superseded_upsert_payload.gqt b/crates/omnigraph-gqt/cases/write_max_bytes_superseded_upsert_payload.gqt new file mode 100644 index 000000000..afd163e9e --- /dev/null +++ b/crates/omnigraph-gqt/cases/write_max_bytes_superseded_upsert_payload.gqt @@ -0,0 +1,33 @@ +# issue: none +# notes: Versions of one key written in the same operation count as input until the last-write-wins fold. +# notes: Two upserts of key "same" each carry 15,999 bytes: the fold keeps one, but the 31,998-byte input exceeds write_max_bytes = 30000 and is refused as `decoded blob input bytes per operation`. + +--- runner +timeout_ms: 10000 +environments: + - target: omnigraph-engine + storage: local-filesystem + - target: omnigraph-engine-dst + storage: in-memory-object-store + seeds: [0, 42] + +--- schema +node Doc { + slug: String @key + content: Blob? + note: String +} + +--- seed + +--- mutate +set write_max_bytes = 30000; +query upsert_twice($payload: Blob) { + insert Doc { slug: "same", content: $payload, note: "first" } + insert Doc { slug: "same", content: $payload, note: "second" } +} + +--- params +{"payload": "base64:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"} + +--- expect error: decoded blob input bytes per operation diff --git a/crates/omnigraph-gqt/cases/write_max_bytes_update_after_duplicate_upserts.gqt b/crates/omnigraph-gqt/cases/write_max_bytes_update_after_duplicate_upserts.gqt new file mode 100644 index 000000000..3081edab8 --- /dev/null +++ b/crates/omnigraph-gqt/cases/write_max_bytes_update_after_duplicate_upserts.gqt @@ -0,0 +1,34 @@ +# issue: none +# notes: Versions of one key written in the same operation count as input until the last-write-wins fold. +# notes: Two upserts of key "same" leave two pending versions; the update matches both, so its 15,999-byte assignment counts twice (31,998) and is refused under write_max_bytes = 30000 as `decoded blob input bytes per operation`. + +--- runner +timeout_ms: 10000 +environments: + - target: omnigraph-engine + storage: local-filesystem + - target: omnigraph-engine-dst + storage: in-memory-object-store + seeds: [0, 42] + +--- schema +node Doc { + slug: String @key + content: Blob? + note: String +} + +--- seed + +--- mutate +set write_max_bytes = 30000; +query upsert_then_assign($payload: Blob) { + insert Doc { slug: "same", note: "first" } + insert Doc { slug: "same", note: "second" } + update Doc set { content: $payload } where slug = "same" +} + +--- params +{"payload": "base64:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"} + +--- expect error: decoded blob input bytes per operation diff --git a/crates/omnigraph-gqt/cases/write_max_bytes_update_assignment_aggregate_name.gqt b/crates/omnigraph-gqt/cases/write_max_bytes_update_assignment_aggregate_name.gqt new file mode 100644 index 000000000..1b5b6e671 --- /dev/null +++ b/crates/omnigraph-gqt/cases/write_max_bytes_update_assignment_aggregate_name.gqt @@ -0,0 +1,66 @@ +# issue: none +# notes: An update charges each assigned base64: value once per matched row, and an insert sums its Blob properties, under `decoded blob input bytes per operation`. +# notes: Each 15,999-byte value fits write_max_bytes = 30000; the two-row update and the two-property insert each total 31,998 and are refused, leaving the rows unchanged. + +--- runner +timeout_ms: 10000 +environments: + - target: omnigraph-engine + storage: local-filesystem + - target: omnigraph-engine-dst + storage: in-memory-object-store + seeds: [0, 42] + +--- schema +node Doc { + slug: String @key + content: Blob? + extra: Blob? + note: String +} + +--- seed + +--- mutate +query seed_docs() { + insert Doc { slug: "a", note: "n" } + insert Doc { slug: "b", note: "n" } +} + +--- expect affected: nodes=2 edges=0 + +--- mutate +set write_max_bytes = 30000; +query assign_both_rows($payload: Blob) { + update Doc set { content: $payload } where note = "n" +} + +--- params +{"payload": "base64:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"} + +--- expect error: decoded blob input bytes per operation + +--- mutate +set write_max_bytes = 30000; +query two_properties($payload: Blob) { + insert Doc { slug: "c", content: $payload, extra: $payload, note: "m" } +} + +--- params +{"payload": "base64:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"} + +--- expect error: decoded blob input bytes per operation + +--- query +query rows() { + match { $d: Doc } + return { $d.slug, $d.note } +} + +--- expect unordered +{"d.slug":"a","d.note":"n"} +{"d.slug":"b","d.note":"n"} + +--- expect shape +d.slug: String +d.note: String diff --git a/crates/omnigraph-gqt/cases/write_max_bytes_update_carried_blob.gqt b/crates/omnigraph-gqt/cases/write_max_bytes_update_carried_blob.gqt new file mode 100644 index 000000000..6dd252c04 --- /dev/null +++ b/crates/omnigraph-gqt/cases/write_max_bytes_update_carried_blob.gqt @@ -0,0 +1,85 @@ +# issue: none +# notes: An update carries every Blob it does not assign and counts it as `materialized blob payload bytes`; assigning a Blob (here to null) carries no payload. +# notes: The seeded payload is 2,048 bytes: write_max_bytes = 2047 refuses the carry and leaves the title old, 2048 admits it exactly. + +--- runner +timeout_ms: 10000 +environments: + - target: omnigraph-engine + storage: local-filesystem + +--- schema +node Doc { + slug: String @key + content: Blob? + title: String +} + +--- seed +{"type":"Doc","data":{"slug":"big","content":"base64:0001000200030004000500060007000800090010001100120013001400150016001700180019002000210022002300240025002600270028002900300031003200330034003500360037003800390040004100420043004400450046004700480049005000510052005300540055005600570058005900600061006200630064006500660067006800690070007100720073007400750076007700780079008000810082008300840085008600870088008900900091009200930094009500960097009800990100010101020103010401050106010701080109011001110112011301140115011601170118011901200121012201230124012501260127012801290130013101320133013401350136013701380139014001410142014301440145014601470148014901500151015201530154015501560157015801590160016101620163016401650166016701680169017001710172017301740175017601770178017901800181018201830184018501860187018801890190019101920193019401950196019701980199020002010202020302040205020602070208020902100211021202130214021502160217021802190220022102220223022402250226022702280229023002310232023302340235023602370238023902400241024202430244024502460247024802490250025102520253025402550256025702580259026002610262026302640265026602670268026902700271027202730274027502760277027802790280028102820283028402850286028702880289029002910292029302940295029602970298029903000301030203030304030503060307030803090310031103120313031403150316031703180319032003210322032303240325032603270328032903300331033203330334033503360337033803390340034103420343034403450346034703480349035003510352035303540355035603570358035903600361036203630364036503660367036803690370037103720373037403750376037703780379038003810382038303840385038603870388038903900391039203930394039503960397039803990400040104020403040404050406040704080409041004110412041304140415041604170418041904200421042204230424042504260427042804290430043104320433043404350436043704380439044004410442044304440445044604470448044904500451045204530454045504560457045804590460046104620463046404650466046704680469047004710472047304740475047604770478047904800481048204830484048504860487048804890490049104920493049404950496049704980499050005010502050305040505050605070508050905100511051205130514051505160517051805190520052105220523052405250526052705280529053005310532053305340535053605370538053905400541054205430544054505460547054805490550055105520553055405550556055705580559056005610562056305640565056605670568056905700571057205730574057505760577057805790580058105820583058405850586058705880589059005910592059305940595059605970598059906000601060206030604060506060607060806090610061106120613061406150616061706180619062006210622062306240625062606270628062906300631063206330634063506360637063806390640064106420643064406450646064706480649065006510652065306540655065606570658065906600661066206630664066506660667066806690670067106720673067406750676067706780679068006810682AAA=","title":"old"}} + +--- mutate +set write_max_bytes = 2047; +query retitle_one_byte_short() { + update Doc set { title: "new" } where slug = "big" +} + +--- expect error: materialized blob payload bytes + +--- query +query titles_after_refusal() { + match { $d: Doc } + return { $d.slug, $d.title } +} + +--- expect unordered +{"d.slug": "big", "d.title": "old"} + +--- expect shape +d.slug: String +d.title: String + +--- mutate +set write_max_bytes = 2048; +query retitle_exact() { + update Doc set { title: "new" } where slug = "big" +} + +--- expect affected: nodes=1 edges=0 + +--- query +query titles_after_carry() { + match { $d: Doc } + return { $d.slug, $d.title } +} + +--- expect unordered +{"d.slug": "big", "d.title": "new"} + +--- expect shape +d.slug: String +d.title: String + +--- mutate +set write_max_bytes = 2047; +query clear_and_retitle($c: Blob?) { + update Doc set { content: $c, title: "cleared" } where slug = "big" +} + +--- params +{"c": null} + +--- expect affected: nodes=1 edges=0 + +--- query +query titles_after_clear() { + match { $d: Doc } + return { $d.slug, $d.title } +} + +--- expect unordered +{"d.slug": "big", "d.title": "cleared"} + +--- expect shape +d.slug: String +d.title: String diff --git a/crates/omnigraph-gqt/cases/write_max_bytes_update_oversize_value_no_match.gqt b/crates/omnigraph-gqt/cases/write_max_bytes_update_oversize_value_no_match.gqt new file mode 100644 index 000000000..26efbb4fd --- /dev/null +++ b/crates/omnigraph-gqt/cases/write_max_bytes_update_oversize_value_no_match.gqt @@ -0,0 +1,39 @@ +# issue: none +# notes: An update refuses an assigned base64: value over write_max_bytes before it scans, so the refusal does not depend on a matched row. +# notes: The 4,095-byte value exceeds write_max_bytes = 4093 and is refused although no Doc has slug "missing". + +--- runner +timeout_ms: 10000 +environments: + - target: omnigraph-engine + storage: local-filesystem + - target: omnigraph-engine-dst + storage: in-memory-object-store + seeds: [0, 42] + +--- schema +node Doc { + slug: String @key + content: Blob? + note: String +} + +--- seed + +--- mutate +query seed_doc() { + insert Doc { slug: "present", note: "n" } +} + +--- expect affected: nodes=1 edges=0 + +--- mutate +set write_max_bytes = 4093; +query assign_missing($payload: Blob) { + update Doc set { content: $payload } where slug = "missing" +} + +--- params +{"payload": "base64:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"} + +--- expect error: decoded blob input bytes per operation diff --git a/crates/omnigraph-server/tests/data_routes.rs b/crates/omnigraph-server/tests/data_routes.rs index 22974c333..743a070f8 100644 --- a/crates/omnigraph-server/tests/data_routes.rs +++ b/crates/omnigraph-server/tests/data_routes.rs @@ -2891,6 +2891,13 @@ async fn settings_show_all_lists_the_definition_in_order() { "default", "request" ), + show_row( + "write_max_bytes", + "33554432", + "33554432", + "default", + "request" + ), show_row( "history_release_bytes", "262144", @@ -3043,6 +3050,86 @@ async fn settings_reset_all_at_the_http_door_returns_to_the_process_defaults() { ); } +#[tokio::test(flavor = "multi_thread")] +async fn write_max_bytes_request_override_reset_and_load_process_default() { + let (settings, sources) = omnigraph::settings::from_env_with(|variable| { + (variable == "OMNIGRAPH_WRITE_MAX_BYTES").then(|| "4093".to_string()) + }) + .unwrap(); + let (_temp, app) = + app_for_loaded_graph_with_process_defaults(ProcessDefaults { settings, sources }).await; + for (query, value, source) in [ + ("show write_max_bytes;", "8191", "request"), + ( + "set write_max_bytes = 6143; show write_max_bytes;", + "6143", + "file", + ), + ( + "reset write_max_bytes; show write_max_bytes;", + "4093", + "env", + ), + ] { + let (status, body) = json_response( + &app, + json_post( + "/query", + &json!({ + "query":query, "settings":{"write_max_bytes":8191} + }), + ), + ) + .await; + assert_eq!(status, StatusCode::OK, "{body}"); + assert_eq!( + body, + show_output(&[show_row( + "write_max_bytes", + value, + "33554432", + source, + "request" + )]) + ); + } + let head = main_head_commit_id(&app).await; + for limit in [0, 33554433] { + let (status, body) = json_response(&app, json_post("/mutate", &json!({ + "query":MUTATION_QUERIES,"name":"insert_person","params":{"name":"refused","age":1}, + "settings":{"write_max_bytes":limit} + }))).await; + assert_settings_refusal_needle(status, &body, "1..=33554432"); + } + let data = json!({"type":"Person","data":{"name":"x".repeat(4094),"age":1}}).to_string(); + for path in ["/load", "/load/ndjson"] { + let request = if path == "/load/ndjson" { + Request::builder() + .header(HTTP_API_CONTRACT_HEADER, HTTP_API_CONTRACT) + .uri(g("/load/ndjson?branch=main&mode=append")) + .method(Method::POST) + .header("content-type", "application/x-ndjson") + .body(Body::from(data.clone())) + .unwrap() + } else { + json_post(path, &json!({"branch":"main","mode":"append","data":data})) + }; + let (status, body) = json_response(&app, request).await; + assert!(!status.is_success(), "{path}: {body}"); + assert!(body.to_string().contains("4093"), "{path}: {body}"); + assert_eq!(main_head_commit_id(&app).await, head); + } + let (status, body) = json_response(&app, json_post("/mutate", &json!({ + "query":MUTATION_QUERIES,"name":"insert_person","params":{"name":"x".repeat(4094),"age":1}, + "settings":{"write_max_bytes":33554432} + }))).await; + assert_eq!( + status, + StatusCode::OK, + "a request may raise an environment default: {body}" + ); +} + /// Both carriers are accepted at `branch merge` and the merge runs; that the /// mode they name selects a classifier is the mode oracle's claim, pinned by /// `crates/omnigraph/tests/merge_cost.rs::merge_lineage_setting_selects_the_completed_classifier`. diff --git a/crates/omnigraph/src/exec/merge.rs b/crates/omnigraph/src/exec/merge.rs index d5a158291..0ec7dd71f 100644 --- a/crates/omnigraph/src/exec/merge.rs +++ b/crates/omnigraph/src/exec/merge.rs @@ -6,10 +6,11 @@ use crate::ordered_cursor::{ }; use crate::seams::{decide_seam, fail}; use crate::session::Session; +use crate::storage_layer::WriteBudget; use crate::storage_layer::{ KEYED_WRITE_MAX_BYTES, KEYED_WRITE_MAX_ROWS, KeyedWriteSemantics, ProvenInsertChunk, }; -use crate::table_store::certified_insert_absence_rows; +use crate::table_store::{MATERIALIZED_BLOB_PAYLOAD_BYTES, certified_insert_absence_rows}; use futures::StreamExt; use omnigraph_compiler::settings::MergeLineage; @@ -117,6 +118,7 @@ struct StagedTable { /// consumes these, rather than assuming Lance's scanner will emit a /// particular physical batch shape. chunk_rows: Vec, + write_budget: WriteBudget, } #[derive(Debug)] @@ -140,6 +142,7 @@ struct DeleteIdChunks { /// bookkeeping. Per-chunk bounds alone would still permit a 1,024-chunk /// plan to retain tens of GiB before the first effect. retained_bytes: u64, + write_budget: WriteBudget, } #[derive(Debug)] @@ -149,20 +152,21 @@ struct DeleteIdChunk { } impl DeleteIdChunks { - fn new(id_col: &'static str) -> Self { + fn new(id_col: &'static str, write_budget: WriteBudget) -> Self { Self { id_col, chunks: Vec::new(), retained_bytes: 0, + write_budget, } } fn push(&mut self, id: String) -> Result<()> { - self.push_bounded(id, KEYED_WRITE_MAX_ROWS, KEYED_WRITE_MAX_BYTES) + self.push_bounded(id, KEYED_WRITE_MAX_ROWS, self.write_budget.bytes()) } fn push_bounded(&mut self, id: String, max_rows: usize, max_bytes: u64) -> Result<()> { - self.push_with_bounds(id, max_rows, max_bytes, KEYED_WRITE_MAX_BYTES) + self.push_with_bounds(id, max_rows, max_bytes, self.write_budget.bytes()) } fn push_with_bounds( @@ -408,6 +412,7 @@ struct ProvenPureInsertAdopt { /// Exact pre-effect row boundaries observed from the bounded /// source-interval stream. Each boundary becomes one filtered transaction. chunk_rows: Vec, + write_budget: WriteBudget, } #[derive(Debug, Clone)] @@ -609,14 +614,16 @@ struct StagedTableWriter { dataset: Option, buffered_rows: usize, buffered_bytes: u64, + buffered_payload_bytes: u64, row_count: u64, chunk_rows: Vec, + write_budget: WriteBudget, batches: Vec, external_payloads: crate::table_store::ExternalBlobPayloadCache, } impl StagedTableWriter { - fn new(table_key: &str, schema: SchemaRef) -> Result { + fn new(table_key: &str, schema: SchemaRef, write_budget: WriteBudget) -> Result { let dir = merge_stage_tempdir(table_key)?; let dataset_uri = dir.path().join("table.lance").to_string_lossy().to_string(); let materialize_blobs = schema_has_blob(&schema)?; @@ -628,8 +635,10 @@ impl StagedTableWriter { dataset: None, buffered_rows: 0, buffered_bytes: 0, + buffered_payload_bytes: 0, row_count: 0, chunk_rows: Vec::new(), + write_budget, batches: Vec::new(), external_payloads: crate::table_store::ExternalBlobPayloadCache::new(), }) @@ -646,58 +655,61 @@ impl StagedTableWriter { let indices = UInt64Array::from(vec![row.row_index as u64]); let input = arrow_select::take::take_record_batch(&row.batch, &indices) .map_err(OmniError::arrow_internal)?; - let predicted_row_bytes = if self.materialize_blobs { + let predicted = if self.materialize_blobs { materializer.predicted_materialized_blob_batch_bytes( &row.dataset, &input, external_preflight, - KEYED_WRITE_MAX_BYTES, + self.write_budget.bytes(), )? } else { - u64::try_from(input.get_array_memory_size()).map_err(|_| { - OmniError::manifest_internal("branch merge row memory size exceeds u64") - })? + crate::table_store::write_batch_bytes(&input)? }; - if predicted_row_bytes > KEYED_WRITE_MAX_BYTES { - return Err(OmniError::resource_limit( - "branch-merge fenced entity bytes", - KEYED_WRITE_MAX_BYTES, - predicted_row_bytes, - )); - } + self.write_budget + .check("branch-merge fenced entity bytes", predicted.rows)?; + self.write_budget + .check(MATERIALIZED_BLOB_PAYLOAD_BYTES, predicted.payload)?; let predicted_overflow = self .buffered_bytes - .checked_add(predicted_row_bytes) - .is_none_or(|bytes| bytes > KEYED_WRITE_MAX_BYTES); + .checked_add(predicted.rows) + .is_none_or(|bytes| bytes > self.write_budget.bytes()) + || self + .buffered_payload_bytes + .checked_add(predicted.payload) + .is_none_or(|bytes| bytes > self.write_budget.bytes()); if self.buffered_rows > 0 && (self.buffered_rows >= KEYED_WRITE_MAX_ROWS || predicted_overflow) { - // Flush before payload I/O, not after. Otherwise two individually - // valid rows can transiently retain more than the chunk ceiling. self.flush().await?; } let batch = self .row_batch(row, input, materializer, external_preflight) .await?; - let row_bytes = u64::try_from(batch.get_array_memory_size()).map_err(|_| { - OmniError::manifest_internal("branch merge row memory size exceeds u64") - })?; - if row_bytes > KEYED_WRITE_MAX_BYTES { - return Err(OmniError::resource_limit( - "branch-merge fenced entity bytes", - KEYED_WRITE_MAX_BYTES, - row_bytes, - )); - } + let usage = crate::table_store::write_batch_bytes(&batch)?; + let row_bytes = usage.rows; + self.write_budget + .check("branch-merge fenced entity bytes", row_bytes)?; + self.write_budget + .check(MATERIALIZED_BLOB_PAYLOAD_BYTES, usage.payload)?; let would_exceed_bytes = self .buffered_bytes .checked_add(row_bytes) - .is_none_or(|bytes| bytes > KEYED_WRITE_MAX_BYTES); + .is_none_or(|bytes| bytes > self.write_budget.bytes()) + || self + .buffered_payload_bytes + .checked_add(usage.payload) + .is_none_or(|bytes| bytes > self.write_budget.bytes()); if self.buffered_rows > 0 && (self.buffered_rows >= KEYED_WRITE_MAX_ROWS || would_exceed_bytes) { self.flush().await?; } + self.buffered_payload_bytes = self + .buffered_payload_bytes + .checked_add(usage.payload) + .ok_or_else(|| { + OmniError::manifest_internal("branch merge payload byte count overflow") + })?; self.row_count = self .row_count .checked_add(1) @@ -709,7 +721,8 @@ impl StagedTableWriter { .ok_or_else(|| OmniError::manifest_internal("branch merge byte count overflow"))?; self.batches.push(batch); if self.buffered_rows >= KEYED_WRITE_MAX_ROWS - || self.buffered_bytes >= KEYED_WRITE_MAX_BYTES + || self.buffered_bytes >= self.write_budget.bytes() + || self.buffered_payload_bytes >= self.write_budget.bytes() { self.flush().await?; } @@ -728,7 +741,7 @@ impl StagedTableWriter { .materialize_blob_batch_bounded_with_preflight_cache( &row.dataset, batch, - KEYED_WRITE_MAX_BYTES, + self.write_budget.bytes(), external_preflight, &mut self.external_payloads, ) @@ -763,6 +776,7 @@ impl StagedTableWriter { dataset: self.dataset.unwrap(), row_count: self.row_count, chunk_rows: self.chunk_rows, + write_budget: self.write_budget, }) } @@ -780,6 +794,7 @@ impl StagedTableWriter { }; self.buffered_rows = 0; self.buffered_bytes = 0; + self.buffered_payload_bytes = 0; self.chunk_rows.push(batch.num_rows()); let chunk_count = u64::try_from(self.chunk_rows.len()) .map_err(|_| OmniError::manifest_internal("branch merge chunk count exceeds u64"))?; @@ -846,6 +861,7 @@ async fn try_proven_pure_insert_history( base_snapshot: &Snapshot, source_snapshot: &Snapshot, id_col: &'static str, + write_budget: WriteBudget, ) -> Result> { let Some(base_entry) = base_snapshot.dataset(table_key) else { return Ok(None); @@ -955,6 +971,7 @@ async fn try_proven_pure_insert_history( fragments, }, chunk_rows: Vec::new(), + write_budget, })) } @@ -1030,6 +1047,7 @@ async fn finalize_proven_pure_insert_adopt( mut proven: ProvenPureInsertAdopt, external_preflight: &crate::table_store::ExternalBlobPreflight, system_columns: SystemColumns, + write_budget: WriteBudget, ) -> Result> { let Some(chunk_rows) = plan_proven_pure_insert_chunks( db, @@ -1039,6 +1057,7 @@ async fn finalize_proven_pure_insert_adopt( proven.inserted_rows, external_preflight, system_columns, + write_budget, ) .await? else { @@ -1054,12 +1073,14 @@ async fn try_proven_pure_insert_adopt( base_snapshot: &Snapshot, source_snapshot: &Snapshot, system_columns: SystemColumns, + write_budget: WriteBudget, ) -> Result> { let Some(proven) = try_proven_pure_insert_history( table_key, base_snapshot, source_snapshot, system_columns.id, + write_budget, ) .await? else { @@ -1072,6 +1093,7 @@ async fn try_proven_pure_insert_adopt( proven, &empty_external_preflight, system_columns, + write_budget, ) .await } @@ -1321,6 +1343,7 @@ async fn plan_proven_pure_insert_chunks( expected_rows: u64, external_preflight: &crate::table_store::ExternalBlobPreflight, system_columns: SystemColumns, + write_budget: WriteBudget, ) -> Result>> { let scan_timing = crate::instrumentation::start_merge_timing( crate::instrumentation::MergeTimingPhase::ProvenInsertPlanScan, @@ -1334,6 +1357,7 @@ async fn plan_proven_pure_insert_chunks( interval, external_preflight, system_columns, + write_budget, ) .await?; let mut chunk_rows = Vec::new(); @@ -1456,6 +1480,7 @@ async fn compute_adopt_delta( source_snapshot: &Snapshot, materialize_blobs: bool, external_preflight: &crate::table_store::ExternalBlobPreflight, + write_budget: WriteBudget, ) -> Result> { let full_schema = schema_for_table_key(catalog, table_key)?; let schema = if materialize_blobs { @@ -1464,11 +1489,15 @@ async fn compute_adopt_delta( validation_schema(catalog, table_key, &full_schema)? }; let materializer = target_db.blob_materializer(); - let mut append_writer = - StagedTableWriter::new(&format!("{}_adopt_append", table_key), schema.clone())?; - let mut upsert_writer = StagedTableWriter::new(&format!("{}_adopt_upsert", table_key), schema)?; + let mut append_writer = StagedTableWriter::new( + &format!("{}_adopt_append", table_key), + schema.clone(), + write_budget, + )?; + let mut upsert_writer = + StagedTableWriter::new(&format!("{}_adopt_upsert", table_key), schema, write_budget)?; let id_col = catalog.system_columns.id; - let mut deleted_ids = DeleteIdChunks::new(id_col); + let mut deleted_ids = DeleteIdChunks::new(id_col, write_budget); let mut base = OrderedTableCursor::from_snapshot_lazy(base_snapshot, table_key, "base", id_col).await?; let mut source = @@ -1750,15 +1779,20 @@ async fn stage_streaming_table_merge_walk( conflicts: &mut Vec, external_preflight: &crate::table_store::ExternalBlobPreflight, outcome_log: Option<&mut Vec<(String, RowOutcomeKind)>>, + write_budget: WriteBudget, ) -> Result> { let schema = schema_for_table_key(catalog, table_key)?; let prior_conflict_count = conflicts.len(); let materializer = target_db.blob_materializer(); - let mut insert_writer = - StagedTableWriter::new(&format!("{}_inserts", table_key), schema.clone())?; - let mut update_writer = StagedTableWriter::new(&format!("{}_updates", table_key), schema)?; + let mut insert_writer = StagedTableWriter::new( + &format!("{}_inserts", table_key), + schema.clone(), + write_budget, + )?; + let mut update_writer = + StagedTableWriter::new(&format!("{}_updates", table_key), schema, write_budget)?; let id_col = catalog.system_columns.id; - let mut deleted_ids = DeleteIdChunks::new(id_col); + let mut deleted_ids = DeleteIdChunks::new(id_col, write_budget); let mut base = OrderedTableCursor::from_snapshot(base_snapshot, table_key, "base", id_col).await?; let mut source = @@ -2419,7 +2453,8 @@ async fn plan_lineage_merge( "lineage merge: candidate discovery complete" ); - let mut candidate_chunks = DeleteIdChunks::new(catalog.system_columns.id); + let mut candidate_chunks = + DeleteIdChunks::new(catalog.system_columns.id, WriteBudget::default()); for id in candidate_ids { match candidate_chunks.push_bounded(id, LINEAGE_FILTER_MAX_IDS, KEYED_WRITE_MAX_BYTES) { Ok(()) => {} @@ -2460,15 +2495,20 @@ async fn stage_lineage_table_merge( conflicts: &mut Vec, external_preflight: &crate::table_store::ExternalBlobPreflight, mut outcome_log: Option<&mut Vec<(String, RowOutcomeKind)>>, + write_budget: WriteBudget, ) -> Result> { let schema = schema_for_table_key(catalog, table_key)?; let prior_conflict_count = conflicts.len(); let materializer = target_db.blob_materializer(); - let mut insert_writer = - StagedTableWriter::new(&format!("{}_inserts", table_key), schema.clone())?; - let mut update_writer = StagedTableWriter::new(&format!("{}_updates", table_key), schema)?; + let mut insert_writer = StagedTableWriter::new( + &format!("{}_inserts", table_key), + schema.clone(), + write_budget, + )?; + let mut update_writer = + StagedTableWriter::new(&format!("{}_updates", table_key), schema, write_budget)?; let id_col = catalog.system_columns.id; - let mut deleted_ids = DeleteIdChunks::new(id_col); + let mut deleted_ids = DeleteIdChunks::new(id_col, write_budget); let mut needs_update = false; for chunk in &plan.candidate_chunks.chunks { @@ -2645,6 +2685,7 @@ fn stage_streaming_table_merge<'a>( conflicts: &'a mut Vec, external_preflight: &'a crate::table_store::ExternalBlobPreflight, lineage: MergeLineage, + write_budget: WriteBudget, ) -> std::pin::Pin< Box>> + Send + 'a>, > { @@ -2661,6 +2702,7 @@ fn stage_streaming_table_merge<'a>( conflicts, external_preflight, None, + write_budget, ) .await?; crate::instrumentation::record_completed_merge_classification(false); @@ -2685,6 +2727,7 @@ fn stage_streaming_table_merge<'a>( conflicts, external_preflight, None, + write_budget, ) .await?; crate::instrumentation::record_completed_merge_classification(true); @@ -2701,6 +2744,7 @@ fn stage_streaming_table_merge<'a>( conflicts, external_preflight, None, + write_budget, ) .await?; crate::instrumentation::record_completed_merge_classification(false); @@ -2720,6 +2764,7 @@ fn stage_streaming_table_merge<'a>( conflicts, external_preflight, Some(&mut walk_log), + write_budget, ) .await?; crate::instrumentation::record_completed_merge_classification(false); @@ -2744,6 +2789,7 @@ fn stage_streaming_table_merge<'a>( &mut lineage_conflicts, external_preflight, Some(&mut lineage_log), + write_budget, ) .await?; crate::instrumentation::record_completed_merge_classification(true); @@ -3446,6 +3492,7 @@ async fn classify_adopt( table_key: &str, target_active: Option<&str>, external_preflight: &crate::table_store::ExternalBlobPreflight, + write_budget: WriteBudget, ) -> Result> { let Some(source_entry) = source_snapshot.dataset(table_key) else { // Source has no such table — nothing to adopt or validate. @@ -3468,6 +3515,7 @@ async fn classify_adopt( base_snapshot, source_snapshot, catalog.system_columns, + write_budget, ) .await? { @@ -3478,6 +3526,7 @@ async fn classify_adopt( base_snapshot, source_snapshot, catalog.system_columns.id, + write_budget, ) .await? { @@ -3494,6 +3543,7 @@ async fn classify_adopt( table_key, advances_head, external_preflight, + write_budget, ) .await?; @@ -3516,7 +3566,13 @@ async fn classify_general_adopt( table_key: &str, advances_head: bool, external_preflight: &crate::table_store::ExternalBlobPreflight, + write_budget: WriteBudget, ) -> Result { + let delta_budget = if advances_head { + write_budget + } else { + WriteBudget::default() + }; let validation_delta = compute_adopt_delta( target_db, table_key, @@ -3525,6 +3581,7 @@ async fn classify_general_adopt( source_snapshot, advances_head, external_preflight, + delta_budget, ) .await?; match (advances_head, validation_delta) { @@ -3933,6 +3990,7 @@ mod chain_limit_tests { inserts: None, updates: None, deleted_ids: DeleteIdChunks { + write_budget: WriteBudget::default(), id_col: SYSTEM_COLUMNS_LEGACY.id, chunks: (0..chunk_count) .map(|_| DeleteIdChunk { @@ -3945,9 +4003,68 @@ mod chain_limit_tests { }) } + #[tokio::test] + async fn write_max_bytes_staged_publication_compacts_the_planned_prefix() { + let schema = Arc::new(arrow_schema::Schema::new(vec![arrow_schema::Field::new( + "id", + arrow_schema::DataType::Utf8, + false, + )])); + let parent = RecordBatch::try_new( + schema.clone(), + vec![Arc::new(arrow_array::StringArray::from(vec![ + "small".to_string(), + "x".repeat(10_000), + ]))], + ) + .unwrap(); + let reader = arrow_array::RecordBatchIterator::new([Ok(parent)], schema.clone()); + let mut stream = lance_datafusion::utils::reader_to_stream(Box::new(reader)); + let settings = omnigraph_compiler::settings::SessionSettings::default() + .with("write_max_bytes", "4093") + .unwrap(); + let budget = WriteBudget::from_settings(&settings); + let mut pending = None; + let chunk = next_exact_staged_chunk(&mut stream, &mut pending, &schema, 1, budget) + .await + .unwrap(); + assert_eq!(chunk.num_rows(), 1); + assert!(crate::table_store::write_batch_bytes(&chunk).unwrap().rows <= budget.bytes()); + } + + #[tokio::test] + async fn write_max_bytes_staged_publication_preserves_an_exact_whole_batch() { + let schema = Arc::new(arrow_schema::Schema::new(vec![arrow_schema::Field::new( + "id", + arrow_schema::DataType::Utf8, + false, + )])); + let batch = RecordBatch::try_new( + schema.clone(), + vec![Arc::new(arrow_array::StringArray::from(vec![ + "exact".to_string(), + ]))], + ) + .unwrap(); + let bytes = crate::table_store::write_batch_bytes(&batch).unwrap().rows; + let settings = omnigraph_compiler::settings::SessionSettings::default() + .with("write_max_bytes", &bytes.to_string()) + .unwrap(); + let budget = WriteBudget::from_settings(&settings); + let reader = arrow_array::RecordBatchIterator::new([Ok(batch)], schema.clone()); + let mut stream = lance_datafusion::utils::reader_to_stream(Box::new(reader)); + let chunk = next_exact_staged_chunk(&mut stream, &mut None, &schema, 1, budget) + .await + .unwrap(); + assert_eq!( + crate::table_store::write_batch_bytes(&chunk).unwrap().rows, + bytes + ); + } + #[test] fn branch_merge_delete_ids_split_on_row_and_escaped_byte_bounds() { - let mut row_bounded = DeleteIdChunks::new(SYSTEM_COLUMNS_LEGACY.id); + let mut row_bounded = DeleteIdChunks::new(SYSTEM_COLUMNS_LEGACY.id, WriteBudget::default()); for row in 0..=KEYED_WRITE_MAX_ROWS { row_bounded.push(format!("id-{row}")).unwrap(); } @@ -3963,7 +4080,8 @@ mod chain_limit_tests { // `a'b` is six bytes as an escaped SQL literal (`'a''b'`) and the // `id IN (` / `)` framing is another eight. The exact 14-byte filter // fits; adding a second id starts a new chunk rather than exceeding it. - let mut byte_bounded = DeleteIdChunks::new(SYSTEM_COLUMNS_LEGACY.id); + let mut byte_bounded = + DeleteIdChunks::new(SYSTEM_COLUMNS_LEGACY.id, WriteBudget::default()); byte_bounded .push_bounded("a'b".to_string(), KEYED_WRITE_MAX_ROWS, 14) .unwrap(); @@ -3984,7 +4102,7 @@ mod chain_limit_tests { "id IN ('x')" ); - let error = DeleteIdChunks::new(SYSTEM_COLUMNS_LEGACY.id) + let error = DeleteIdChunks::new(SYSTEM_COLUMNS_LEGACY.id, WriteBudget::default()) .push_bounded("a''b".to_string(), KEYED_WRITE_MAX_ROWS, 14) .unwrap_err(); assert!(matches!( @@ -3996,7 +4114,8 @@ mod chain_limit_tests { } if resource == "branch-merge delete filter bytes" )); - let mut retained_bounded = DeleteIdChunks::new(SYSTEM_COLUMNS_LEGACY.id); + let mut retained_bounded = + DeleteIdChunks::new(SYSTEM_COLUMNS_LEGACY.id, WriteBudget::default()); retained_bounded .push_with_bounds("a".to_string(), KEYED_WRITE_MAX_ROWS, 1024, 256) .unwrap(); @@ -4023,7 +4142,7 @@ mod chain_limit_tests { #[test] fn branch_merge_delete_chunks_chain_one_link_each() { - let mut deleted_ids = DeleteIdChunks::new(SYSTEM_COLUMNS_LEGACY.id); + let mut deleted_ids = DeleteIdChunks::new(SYSTEM_COLUMNS_LEGACY.id, WriteBudget::default()); for row in 0..=KEYED_WRITE_MAX_ROWS { deleted_ids.push(format!("id-{row}")).unwrap(); } @@ -4280,6 +4399,7 @@ async fn next_exact_staged_chunk( carry: &mut Option, schema: &SchemaRef, expected_rows: usize, + write_budget: WriteBudget, ) -> Result { if expected_rows == 0 { return Err(OmniError::manifest_internal( @@ -4288,6 +4408,7 @@ async fn next_exact_staged_chunk( } let mut remaining = expected_rows; let mut slices = Vec::new(); + let mut partial_batch = false; while remaining > 0 { let batch = match carry.take() { Some(batch) => batch, @@ -4308,6 +4429,7 @@ async fn next_exact_staged_chunk( }, }; let take = remaining.min(batch.num_rows()); + partial_batch |= take < batch.num_rows(); slices.push(batch.slice(0, take)); remaining -= take; if take < batch.num_rows() { @@ -4315,13 +4437,31 @@ async fn next_exact_staged_chunk( } } let chunk = if slices.len() == 1 { - slices.pop().expect("one slice") + let slice = slices.pop().expect("one slice"); + let usage = crate::table_store::write_batch_bytes(&slice)?; + let retained = slice.get_array_memory_size() as u64; + let logical = crate::table_store::batch_slice_memory_size(&slice, 0, slice.num_rows())?; + if !partial_batch + && usage.rows <= write_budget.bytes() + && usage.payload <= write_budget.bytes() + && retained <= write_budget.bytes().saturating_mul(2) + && retained <= logical.saturating_add(64 * 1024) + { + slice + } else { + let indices = UInt64Array::from_iter_values(0..slice.num_rows() as u64); + arrow_select::take::take_record_batch(&slice, &indices) + .map_err(OmniError::arrow_internal)? + } } else { arrow_select::concat::concat_batches(schema, &slices).map_err(OmniError::arrow_internal)? }; - let chunk_bytes = u64::try_from(chunk.get_array_memory_size()) - .map_err(|_| OmniError::manifest_internal("branch merge chunk bytes exceed u64"))?; - if chunk.num_rows() > KEYED_WRITE_MAX_ROWS || chunk_bytes > KEYED_WRITE_MAX_BYTES { + let usage = crate::table_store::write_batch_bytes(&chunk)?; + let chunk_bytes = usage.rows; + if chunk.num_rows() > KEYED_WRITE_MAX_ROWS + || chunk_bytes > write_budget.bytes() + || usage.payload > write_budget.bytes() + { return Err(OmniError::manifest_internal(format!( "branch merge reconstructed a keyed chunk outside its planned bound: {} rows / {chunk_bytes} bytes", chunk.num_rows() @@ -4340,10 +4480,11 @@ async fn commit_staged_keyed_chunks( between_chunk_failpoint: Option<&'static crate::seams::DecideSeam>, system_columns: SystemColumns, ) -> Result { + let write_budget = table.write_budget; let source = SnapshotHandle::new(table.dataset.clone()); let stream = target_db .storage() - .scan_stream_for_rewrite_bounded(&source, KEYED_WRITE_MAX_ROWS, KEYED_WRITE_MAX_BYTES) + .scan_stream_for_rewrite_bounded(&source, KEYED_WRITE_MAX_ROWS, write_budget.bytes()) .await?; let schema: SchemaRef = Arc::new(table.dataset.schema().into()); commit_keyed_stream_chunks( @@ -4358,6 +4499,7 @@ async fn commit_staged_keyed_chunks( chain, between_chunk_failpoint, system_columns, + write_budget, ) .await } @@ -4384,11 +4526,14 @@ async fn commit_keyed_stream_chunks( chain: &mut MergeChain, between_chunk_failpoint: Option<&'static crate::seams::DecideSeam>, system_columns: SystemColumns, + write_budget: WriteBudget, ) -> Result { let mut carry = None; let mut observed_rows = 0_u64; for (chunk_index, expected_rows) in chunk_rows.iter().copied().enumerate() { - let batch = next_exact_staged_chunk(&mut stream, &mut carry, schema, expected_rows).await?; + let batch = + next_exact_staged_chunk(&mut stream, &mut carry, schema, expected_rows, write_budget) + .await?; observed_rows = observed_rows .checked_add(batch.num_rows() as u64) .ok_or_else(|| OmniError::manifest_internal("branch merge row count overflow"))?; @@ -4399,7 +4544,14 @@ async fn commit_keyed_stream_chunks( KeyedChunkStage::General(semantics) => { target_db .storage() - .stage_keyed_write(current.clone(), table_key, batch, semantics, system_columns) + .stage_keyed_write( + current.clone(), + table_key, + batch, + semantics, + system_columns, + write_budget, + ) .await? } KeyedChunkStage::ProvenStrictInsert => { @@ -4411,7 +4563,12 @@ async fn commit_keyed_stream_chunks( )?; target_db .storage() - .stage_proven_strict_insert(current.clone(), chunk, system_columns) + .stage_proven_strict_insert( + current.clone(), + chunk, + system_columns, + write_budget, + ) .await? } }; @@ -4474,6 +4631,7 @@ async fn publish_proven_pure_insert_adopt( expected_version: u64, system_columns: SystemColumns, ) -> Result<(crate::db::DatasetUpdate, MergeChain)> { + let write_budget = proven.write_budget; let (current, full_path, table_branch) = prepared_target.into_parts(); let source = SnapshotHandle::new(proven.source.clone()); let stream = target_db @@ -4484,6 +4642,7 @@ async fn publish_proven_pure_insert_adopt( &proven.interval, external_preflight, system_columns, + write_budget, ) .await?; let schema: SchemaRef = Arc::new(proven.source.schema().into()); @@ -4500,6 +4659,7 @@ async fn publish_proven_pure_insert_adopt( &mut chain, Some(&BRANCH_MERGE_ADOPT_BETWEEN_INSERT_CHUNKS), system_columns, + write_budget, ) .await?; let final_state = target_db @@ -4786,6 +4946,7 @@ impl Session { actor_id, self.settings().merge_lineage(), HistoryReleaseBytes(self.settings().history_release_bytes()), + WriteBudget::from_settings(self.settings()), )) .await; if let Err(error) = fail(&BRANCH_MERGE_PRE_RETURN) { @@ -4833,6 +4994,7 @@ impl Omnigraph { actor_id: Option<&str>, lineage: MergeLineage, history_release_bytes: HistoryReleaseBytes, + write_budget: WriteBudget, ) -> Result { let outer_prepare_timing = crate::instrumentation::start_merge_timing( crate::instrumentation::MergeTimingPhase::OuterPrepare, @@ -4956,6 +5118,7 @@ impl Omnigraph { actor_id, lineage, history_release_bytes, + write_budget, )) .await; if !merge_result @@ -5002,6 +5165,7 @@ impl Omnigraph { actor_id: Option<&str>, lineage: MergeLineage, history_release_bytes: HistoryReleaseBytes, + write_budget: WriteBudget, ) -> Result { let source_snapshot = &source_txn.base; let target_snapshot = &target_txn.base; @@ -5061,6 +5225,7 @@ impl Omnigraph { table_key, target_active.as_deref(), &empty_external_preflight, + write_budget, ) .await? { @@ -5083,6 +5248,7 @@ impl Omnigraph { &mut conflicts, &empty_external_preflight, lineage, + write_budget, ) .await?; table_walk_timing.finish(); @@ -5109,6 +5275,7 @@ impl Omnigraph { base_snapshot, source_snapshot, catalog.system_columns.id, + write_budget, ) .await? { @@ -5169,13 +5336,7 @@ impl Omnigraph { .preflight_persisted_blob_selection(&blob_selection) .await?; let carried_blob_bytes = blob_selection.materialized_payload_bytes(&external_preflight)?; - if carried_blob_bytes > KEYED_WRITE_MAX_BYTES { - return Err(OmniError::resource_limit( - "materialized blob payload bytes", - KEYED_WRITE_MAX_BYTES, - carried_blob_bytes, - )); - } + write_budget.check(MATERIALIZED_BLOB_PAYLOAD_BYTES, carried_blob_bytes)?; for table_key in &ordered_table_keys { if !blob_table_keys.contains(table_key) { @@ -5218,6 +5379,7 @@ impl Omnigraph { proven, &external_preflight, catalog.system_columns, + write_budget, ) .await? { @@ -5231,6 +5393,7 @@ impl Omnigraph { table_key, true, &external_preflight, + write_budget, ) .await? } @@ -5244,6 +5407,7 @@ impl Omnigraph { table_key, true, &external_preflight, + write_budget, ) .await? } @@ -5273,6 +5437,7 @@ impl Omnigraph { table_key, target_active.as_deref(), &external_preflight, + write_budget, ) .await? }; @@ -5298,6 +5463,7 @@ impl Omnigraph { &mut conflicts, &external_preflight, lineage, + write_budget, ) .await?; table_walk_timing.finish(); diff --git a/crates/omnigraph/src/exec/mutation.rs b/crates/omnigraph/src/exec/mutation.rs index f4016b1cb..5fbb6e428 100644 --- a/crates/omnigraph/src/exec/mutation.rs +++ b/crates/omnigraph/src/exec/mutation.rs @@ -8,7 +8,7 @@ use crate::instrumentation::record_mutation_table_open; use crate::loader::append_blob_value; use crate::seams::{decide_seam, fail}; use crate::session::Session; -use crate::storage_layer::{DeletedIdBudget, PendingScanBudget, SnapshotHandle}; +use crate::storage_layer::{DeletedIdBudget, PendingScanBudget, SnapshotHandle, WriteBudget}; use datafusion::prelude::Expr; use futures::TryStreamExt; @@ -342,9 +342,9 @@ fn typed_list_literal_to_array( } /// Build a single-element blob array from a URI or base64 value string. -fn build_blob_array_from_value(value: &str) -> Result { +fn build_blob_array_from_value(value: &str, write_budget: WriteBudget) -> Result { let mut builder = BlobArrayBuilder::new(1); - append_blob_value(&mut builder, value)?; + append_blob_value(&mut builder, value, write_budget)?; builder.finish().map_err(OmniError::lance_internal) } @@ -357,6 +357,33 @@ fn build_null_blob_array(num_rows: usize) -> Result { builder.finish().map_err(OmniError::lance_internal) } +fn preflight_blob_assignments( + assignments: &HashMap, + blob_properties: &HashSet, + rows: usize, + write_budget: WriteBudget, +) -> Result<()> { + let mut bytes = 0_u64; + for property in blob_properties { + if let Some(Literal::String(value)) = assignments.get(property) + && let Some(encoded) = value.strip_prefix("base64:") + { + bytes = bytes + .checked_add(crate::loader::decoded_blob_bytes(encoded)?) + .ok_or_else(|| { + OmniError::manifest_internal("Blob assignment byte count overflow") + })?; + } + } + let bytes = + bytes + .checked_mul(u64::try_from(rows).map_err(|_| { + OmniError::manifest_internal("Blob assignment row count exceeds u64") + })?) + .ok_or_else(|| OmniError::manifest_internal("Blob assignment byte count overflow"))?; + write_budget.check("decoded blob input bytes per operation", bytes) +} + /// Build a single-row RecordBatch from resolved assignments. fn build_insert_batch( schema: &SchemaRef, @@ -364,7 +391,9 @@ fn build_insert_batch( assignments: &HashMap, blob_properties: &HashSet, system_columns: SystemColumns, + write_budget: WriteBudget, ) -> Result { + preflight_blob_assignments(assignments, blob_properties, 1, write_budget)?; let mut columns: Vec = Vec::with_capacity(schema.fields().len()); for field in schema.fields() { @@ -372,7 +401,7 @@ fn build_insert_batch( columns.push(Arc::new(StringArray::from(vec![id]))); } else if blob_properties.contains(field.name()) { if let Some(Literal::String(uri)) = assignments.get(field.name()) { - columns.push(build_blob_array_from_value(uri)?); + columns.push(build_blob_array_from_value(uri, write_budget)?); } else if field.is_nullable() { columns.push(build_null_blob_array(1)?); } else { @@ -457,7 +486,9 @@ fn apply_assignments( batch: &RecordBatch, assignments: &HashMap, blob_properties: &HashSet, + write_budget: WriteBudget, ) -> Result { + preflight_blob_assignments(assignments, blob_properties, batch.num_rows(), write_budget)?; let mut columns: Vec = Vec::with_capacity(full_schema.fields().len()); for field in full_schema.fields().iter() { if blob_properties.contains(field.name()) { @@ -465,7 +496,7 @@ fn apply_assignments( Some(Literal::String(uri)) => { let mut builder = BlobArrayBuilder::new(batch.num_rows()); for _ in 0..batch.num_rows() { - append_blob_value(&mut builder, uri)?; + append_blob_value(&mut builder, uri, write_budget)?; } builder.finish().map_err(OmniError::lance_internal)? } @@ -832,6 +863,7 @@ impl Session { expected_head, settings.stage_write_concurrency(), HistoryReleaseBytes(settings.history_release_bytes()), + WriteBudget::from_settings(&settings), ) .await } @@ -874,6 +906,7 @@ impl Omnigraph { expected_head: Option<&str>, stage_write_concurrency: usize, history_release_bytes: HistoryReleaseBytes, + write_budget: WriteBudget, ) -> Result { const MAX_PRE_EFFECT_REPREPARES: usize = 32; @@ -894,6 +927,7 @@ impl Omnigraph { history_release_bytes, attempt == 0, &mut retryable, + write_budget, ) .await { @@ -928,6 +962,7 @@ impl Omnigraph { history_release_bytes: HistoryReleaseBytes, first_attempt: bool, retryable: &mut bool, + write_budget: WriteBudget, ) -> Result { let requested = Self::normalize_branch_name(branch)?; // Capture one branch-wide write authority: native branch identity, @@ -969,7 +1004,7 @@ impl Omnigraph { // base (RFC 0067). The publisher then makes the complete result // graph-visible in one manifest CAS. Branch is threaded explicitly — no // coordinator swap. - let mut staging = MutationStaging::default(); + let mut staging = MutationStaging::new(write_budget); // Lower + validate up front so the touched-dataset set is known before // execution. A lowering/validation error returns exactly as it did @@ -1207,6 +1242,7 @@ impl Omnigraph { &resolved, &blob_props, catalog.system_columns, + staging.write_budget(), )?; let has_key = node_type.key.is_some(); let table_key = format!("node:{}", type_name); @@ -1295,6 +1331,7 @@ impl Omnigraph { &resolved, &blob_props, catalog.system_columns, + staging.write_budget(), )?; let has_key = edge_type.key.is_some(); let table_key = format!("edge:{}", type_name); @@ -1376,6 +1413,7 @@ impl Omnigraph { // property is refused even when the predicate matches no row. let resolved = resolve_assignments(type_name, &schema, assignments, params)?; let blob_props = node_type.blob_properties.clone(); + preflight_blob_assignments(&resolved, &blob_props, 1, staging.write_budget())?; // Catalog order is kept: `concat_match_batches_to_schema` binds by position. let assigned_blobs = schema .fields() @@ -1420,8 +1458,11 @@ impl Omnigraph { // batches via DataFusion `MemTable` (read-your-writes for prior ops in // this query). The pending side may include rows from earlier // `insert` / `update` ops on the same table. - let (pending_rows, pending_bytes) = staging.pending_resource_usage(&table_key)?; - let scan_budget = PendingScanBudget::new(&table_key, pending_rows, pending_bytes); + let scan_budget = PendingScanBudget::new( + &table_key, + staging.pending_resource_usage(&table_key)?, + staging.write_budget(), + ); let pending_batches = staging.pending_batches(&table_key); let pending_schema = staging.pending_schema(&table_key); // Use merge semantics on the union: a committed row whose `id` @@ -1473,7 +1514,13 @@ impl Omnigraph { let affected_count = matched.num_rows(); - let updated = apply_assignments(&schema, &matched, &resolved, &blob_props)?; + let updated = apply_assignments( + &schema, + &matched, + &resolved, + &blob_props, + staging.write_budget(), + )?; // Validation (value/enum/unique) runs end-of-query via the evaluator. // Accumulate the updated batch into the Merge-mode pending stream. diff --git a/crates/omnigraph/src/exec/staging.rs b/crates/omnigraph/src/exec/staging.rs index f6ea5d1d0..422975de5 100644 --- a/crates/omnigraph/src/exec/staging.rs +++ b/crates/omnigraph/src/exec/staging.rs @@ -27,8 +27,8 @@ use std::collections::{HashMap, HashSet}; use std::sync::Arc; use crate::storage_layer::{ - DeletedIdBudget, KEYED_WRITE_MAX_BYTES, KEYED_WRITE_MAX_ROWS, KeyedWriteSemantics, - SnapshotHandle, StagedHandle, retain_keyed_batch, retained_keyed_bytes, + DeletedIdBudget, KEYED_WRITE_MAX_ROWS, KeyedWriteSemantics, PendingUsage, SnapshotHandle, + StagedHandle, WriteBudget, retain_keyed_batch, retained_keyed_bytes, }; use arrow_array::{Array, RecordBatch, StringArray, UInt32Array}; use arrow_schema::SchemaRef; @@ -78,9 +78,7 @@ impl PendingTable { fn total_bytes(&self) -> Result { self.batches.iter().try_fold(0_u64, |bytes, batch| { - let batch_bytes = u64::try_from(batch.get_array_memory_size()).map_err(|_| { - OmniError::manifest_internal("pending keyed batch bytes exceed u64") - })?; + let batch_bytes = crate::table_store::write_batch_bytes(batch)?.rows; bytes .checked_add(batch_bytes) .ok_or_else(|| OmniError::manifest_internal("pending keyed byte count overflow")) @@ -121,6 +119,8 @@ pub(crate) struct MutationStaging { pub(crate) pending: HashMap, /// Monotonic retained-batch accounting, updated only by `append_batch`. pending_bytes: u64, + pending_payload_bytes: u64, + write_budget: WriteBudget, /// Per-table delete predicates from delete-touching ops. D₂ guarantees a /// table is write-XOR-delete within one query, so this never overlaps /// `pending`. Staged as one combined `stage_delete` per table at @@ -152,6 +152,18 @@ decide_seam! { } impl MutationStaging { + pub(crate) fn new(write_budget: WriteBudget) -> Self { + Self { + write_budget, + deleted_id_budget: DeletedIdBudget::new(write_budget), + ..Self::default() + } + } + + pub(crate) fn write_budget(&self) -> WriteBudget { + self.write_budget + } + /// Capture pre-write metadata on first touch of a table. Subsequent /// touches preserve the original `paths` and `expected_versions` /// entries; `op_kinds` upgrades to the strictest kind seen so far so @@ -259,8 +271,14 @@ impl MutationStaging { ))); } } - let batch_bytes = u64::try_from(batch.get_array_memory_size()) - .map_err(|_| OmniError::manifest_internal("pending keyed batch bytes exceed u64"))?; + let batch_usage = crate::table_store::write_batch_bytes(&batch)?; + let batch_bytes = batch_usage.rows; + let pending_payload_bytes = self + .pending_payload_bytes + .checked_add(batch_usage.payload) + .ok_or_else(|| { + OmniError::manifest_internal("pending Blob payload byte count overflow") + })?; let pending_bytes = if matches!(mode, PendingMode::StrictInsert | PendingMode::Upsert) { let existing_rows = self .pending @@ -284,14 +302,13 @@ impl MutationStaging { let bytes = existing_bytes .checked_add(batch_bytes) .ok_or_else(|| OmniError::manifest_internal("pending keyed byte count overflow"))?; - if bytes > KEYED_WRITE_MAX_BYTES { - return Err(OmniError::resource_limit( - format!("keyed entity bytes for {table_key}"), - KEYED_WRITE_MAX_BYTES, - bytes, - )); - } - retained_keyed_bytes(self.pending_bytes, batch_bytes)? + self.write_budget + .check(format!("keyed entity bytes for {table_key}"), bytes)?; + self.write_budget.check( + "decoded blob input bytes per operation", + pending_payload_bytes, + )?; + retained_keyed_bytes(self.pending_bytes, batch_bytes, self.write_budget)? } else { self.pending_bytes .checked_add(batch_bytes) @@ -308,6 +325,7 @@ impl MutationStaging { } entry.batches.push(batch); self.pending_bytes = pending_bytes; + self.pending_payload_bytes = pending_payload_bytes; Ok(()) } @@ -402,7 +420,7 @@ impl MutationStaging { /// keyed-write fence, while bytes include every pending table so one graph /// mutation cannot multiply the 32 MiB retained-memory budget by touching /// several tables. - pub(crate) fn pending_resource_usage(&self, table_key: &str) -> Result<(u64, u64)> { + pub(crate) fn pending_resource_usage(&self, table_key: &str) -> Result { let rows = u64::try_from( self.pending .get(table_key) @@ -410,7 +428,11 @@ impl MutationStaging { .unwrap_or(0), ) .map_err(|_| OmniError::manifest_internal("pending keyed row count exceeds u64"))?; - Ok((rows, self.pending_bytes)) + Ok(PendingUsage { + rows, + bytes: self.pending_bytes, + payload_bytes: self.pending_payload_bytes, + }) } /// `true` if neither pending writes nor delete predicates have any state — @@ -455,6 +477,8 @@ impl MutationStaging { paths, pending, pending_bytes: _, + pending_payload_bytes: _, + write_budget, delete_predicates, deleted_ids, deleted_id_budget: _, @@ -478,7 +502,12 @@ impl MutationStaging { table_key )) })?; - let table = prepare_pending_table(&table_key, table, db.catalog().system_columns)?; + let table = prepare_pending_table( + &table_key, + table, + db.catalog().system_columns, + write_budget, + )?; // Finish the per-table last-write-wins fold before looking at URI // inputs. A superseded row is not part of the graph operation and // must not trigger policy checks, source probes, or byte charges. @@ -512,21 +541,29 @@ impl MutationStaging { .collect::>(); let copied_external_blob_bytes = external_blob_preflight.materialized_payload_bytes(&copied_external_blob_uris)?; - if copied_external_blob_bytes > KEYED_WRITE_MAX_BYTES { - return Err(OmniError::resource_limit( - "materialized external blob payload bytes", - KEYED_WRITE_MAX_BYTES, - copied_external_blob_bytes, - )); - } - let retained_batch_bytes = stage_inputs.iter().try_fold(0, |bytes, (_, table, _, _)| { - if table.mode == PendingMode::Overwrite { - Ok(bytes) - } else { - retain_keyed_batch(bytes, &table.batch) + write_budget.check( + "materialized external blob payload bytes", + copied_external_blob_bytes, + )?; + let mut retained_batch_bytes = 0; + let mut payload_bytes = copied_external_blob_bytes; + for (_, table, _, _) in &stage_inputs { + if table.mode != PendingMode::Overwrite { + retained_batch_bytes = + retain_keyed_batch(retained_batch_bytes, &table.batch, write_budget)?; + payload_bytes = payload_bytes + .checked_add(crate::table_store::write_batch_bytes(&table.batch)?.payload) + .ok_or_else(|| { + OmniError::manifest_internal( + "materialized Blob payload byte count overflow", + ) + })?; } - })?; - retained_keyed_bytes(retained_batch_bytes, copied_external_blob_bytes)?; + } + write_budget.check( + crate::table_store::MATERIALIZED_BLOB_PAYLOAD_BYTES, + payload_bytes, + )?; // Only after the complete operation has passed policy, source, and // aggregate-copy admission do we read payload bytes. Reuse remains @@ -541,6 +578,7 @@ impl MutationStaging { table.batch.clone(), &external_blob_preflight, db.catalog().system_columns, + write_budget, ) .await? } @@ -555,7 +593,7 @@ impl MutationStaging { }; if table.mode != PendingMode::Overwrite { materialized_keyed_bytes = - retain_keyed_batch(materialized_keyed_bytes, &table.batch)?; + retain_keyed_batch(materialized_keyed_bytes, &table.batch, write_budget)?; } } let concurrency = concurrency.min(stage_inputs.len()).max(1); @@ -568,7 +606,7 @@ impl MutationStaging { if stage_idx > 0 { fail(&LOAD_BETWEEN_TABLE_STAGES)?; } - stage_pending_table(db, table_key, table, path, expected).await + stage_pending_table(db, table_key, table, path, expected, write_budget).await }, )) .buffered(concurrency) @@ -654,6 +692,7 @@ fn prepare_pending_table( table_key: &str, table: PendingTable, system_columns: SystemColumns, + write_budget: WriteBudget, ) -> Result { if table.batches.is_empty() { return Err(OmniError::manifest_internal(format!( @@ -669,10 +708,7 @@ fn prepare_pending_table( .try_fold((0_u64, 0_u64), |(rows, bytes), batch| { let batch_rows = u64::try_from(batch.num_rows()) .map_err(|_| OmniError::manifest_internal("keyed batch rows exceed u64"))?; - let batch_bytes = - u64::try_from(batch.get_array_memory_size()).map_err(|_| { - OmniError::manifest_internal("keyed batch bytes exceed u64") - })?; + let batch_bytes = crate::table_store::write_batch_bytes(batch)?.rows; Ok::<_, OmniError>(( rows.checked_add(batch_rows).ok_or_else(|| { OmniError::manifest_internal("keyed row count overflow") @@ -689,13 +725,7 @@ fn prepare_pending_table( rows, )); } - if bytes > KEYED_WRITE_MAX_BYTES { - return Err(OmniError::resource_limit( - format!("keyed entity bytes for {table_key}"), - KEYED_WRITE_MAX_BYTES, - bytes, - )); - } + write_budget.check(format!("keyed entity bytes for {table_key}"), bytes)?; } let mode = table.mode; @@ -721,6 +751,7 @@ async fn stage_pending_table( table: PreparedPendingTable, path: StagedTablePath, expected: u64, + write_budget: WriteBudget, ) -> Result> { let ds = db .open_pinned_for_write(&path.full_path, &path.entry) @@ -744,6 +775,7 @@ async fn stage_pending_table( combined, KeyedWriteSemantics::StrictInsert, db.catalog().system_columns, + write_budget, ) .await? } @@ -755,6 +787,7 @@ async fn stage_pending_table( combined, KeyedWriteSemantics::Upsert, db.catalog().system_columns, + write_budget, ) .await? } diff --git a/crates/omnigraph/src/loader/mod.rs b/crates/omnigraph/src/loader/mod.rs index 06fcc2ce7..1ba09fc85 100644 --- a/crates/omnigraph/src/loader/mod.rs +++ b/crates/omnigraph/src/loader/mod.rs @@ -28,7 +28,9 @@ use crate::error::{OmniError, Result, missing_graph_type_at_snapshot}; use crate::exec::staging::{MutationStaging, PendingMode}; use crate::seams::{catalog, decide_seam, fail}; use crate::session::Session; -use crate::storage_layer::{DeletedIdBudget, KEYED_WRITE_MAX_BYTES, retain_keyed_batch}; +use crate::storage_layer::{ + DeletedIdBudget, KEYED_WRITE_MAX_BYTES, WriteBudget, retain_keyed_batch, +}; /// Result of a load operation. #[derive(Debug, Clone, Default)] @@ -248,6 +250,7 @@ impl Session { LoadInputShape::LoaderCompatible, self.settings().stage_write_concurrency(), HistoryReleaseBytes(self.settings().history_release_bytes()), + WriteBudget::from_settings(self.settings()), ) .await } @@ -292,6 +295,7 @@ impl Session { LoadInputShape::StrictGraphBatch, self.settings().stage_write_concurrency(), HistoryReleaseBytes(self.settings().history_release_bytes()), + WriteBudget::from_settings(self.settings()), ) .await } @@ -321,6 +325,7 @@ impl Omnigraph { input_shape: LoadInputShape, stage_write_concurrency: usize, history_release_bytes: HistoryReleaseBytes, + write_budget: WriteBudget, ) -> Result { // Engine-layer policy gate (MR-722 fan-out / PR #3). Scope is // `Branch(branch)` to match the HTTP-layer Change convention. @@ -345,6 +350,7 @@ impl Omnigraph { input_shape, stage_write_concurrency, history_release_bytes, + write_budget, )) .await } @@ -360,6 +366,7 @@ impl Omnigraph { input_shape: LoadInputShape, stage_write_concurrency: usize, history_release_bytes: HistoryReleaseBytes, + write_budget: WriteBudget, ) -> Result { // Schema/catalog authority is captured once via the `WriteTxn` (plus its // cheap trailing identity-marker fence); the only second full validation @@ -406,6 +413,7 @@ impl Omnigraph { input_shape, stage_write_concurrency, history_release_bytes, + write_budget, ) .await .map_err(|error| { @@ -466,6 +474,7 @@ impl Omnigraph { input_shape: LoadInputShape, stage_write_concurrency: usize, history_release_bytes: HistoryReleaseBytes, + write_budget: WriteBudget, ) -> Result { load_jsonl_data( self, @@ -476,6 +485,7 @@ impl Omnigraph { input_shape, stage_write_concurrency, history_release_bytes, + write_budget, ) .await } @@ -534,6 +544,7 @@ async fn load_jsonl_data( input_shape: LoadInputShape, stage_write_concurrency: usize, history_release_bytes: HistoryReleaseBytes, + write_budget: WriteBudget, ) -> Result { const MAX_PRE_EFFECT_REPREPARES: usize = 32; @@ -554,6 +565,7 @@ async fn load_jsonl_data( stage_write_concurrency, history_release_bytes, attempt == 0, + write_budget, ) .await { @@ -585,6 +597,7 @@ async fn load_jsonl_reader_once( stage_write_concurrency: usize, history_release_bytes: HistoryReleaseBytes, first_attempt: bool, + write_budget: WriteBudget, ) -> Result { // Capture the manifest/schema authority before interpreting any input. The // catalog rides the WriteTxn and was built from the exact accepted IR named @@ -604,7 +617,10 @@ async fn load_jsonl_reader_once( let mut node_rows: HashMap> = HashMap::new(); let mut edge_rows: HashMap> = HashMap::new(); let mut strict_rows = StrictGraphRows::default(); - let mut keyed_input_budget = KeyedInputBudget::default(); + let mut keyed_input_budget = KeyedInputBudget { + write_budget, + ..KeyedInputBudget::default() + }; // Strict syntax is independent of the keyed-write transaction ceiling. // Append/Merge route through the bounded keyed adapter; Overwrite stages a // Lance replacement transaction and must retain the bulk-replacement @@ -663,6 +679,7 @@ async fn load_jsonl_reader_once( &format!("node:{type_name}"), &data, 0, + &catalog.node_types[&type_name].properties, &mut keyed_input_budget, )?; } @@ -714,6 +731,7 @@ async fn load_jsonl_reader_once( &format!("edge:{canonical}"), &data, from.len().saturating_add(to.len()), + &edge_type.properties, &mut keyed_input_budget, )?; } @@ -743,7 +761,7 @@ async fn load_jsonl_reader_once( // open and the manifest publish, so the parsed batches, validation catalog, // base snapshot, native branch identity, exact graph head, and schema // identity form one immutable authority unit. - let mut staging = MutationStaging::default(); + let mut staging = MutationStaging::new(write_budget); let pending_mode = match mode { LoadMode::Merge => PendingMode::Upsert, // Append mode is a strict exact-id insert. Every physical graph table @@ -778,9 +796,15 @@ async fn load_jsonl_reader_once( __dst_nr.sort_by(|a, b| a.0.cmp(&b.0)); for (type_name, rows) in __dst_nr { let node_type = &catalog.node_types[&type_name]; - let batch = build_node_batch(node_type, &rows, &mut node_id_remap, catalog.system_columns)?; + let batch = build_node_batch( + node_type, + &rows, + &mut node_id_remap, + catalog.system_columns, + write_budget, + )?; if bounded_keyed_input { - prepared_keyed_bytes = retain_keyed_batch(prepared_keyed_bytes, &batch)?; + prepared_keyed_bytes = retain_keyed_batch(prepared_keyed_bytes, &batch, write_budget)?; } // Validation (value/enum/unique) runs end-of-load via the evaluator. let loaded_count = batch.num_rows(); @@ -797,9 +821,9 @@ async fn load_jsonl_reader_once( let _entry = snapshot .dataset(&table_key) .ok_or_else(|| OmniError::manifest(missing_graph_type_at_snapshot(&table_key)))?; - let batch = normalize_strict_json_rows(&catalog, &table_key, &rows)?; + let batch = normalize_strict_json_rows(&catalog, &table_key, &rows, write_budget)?; if bounded_keyed_input { - prepared_keyed_bytes = retain_keyed_batch(prepared_keyed_bytes, &batch)?; + prepared_keyed_bytes = retain_keyed_batch(prepared_keyed_bytes, &batch, write_budget)?; } let loaded_count = batch.num_rows(); prepared_nodes.push((type_name, table_key, vec![batch], loaded_count)); @@ -840,9 +864,15 @@ async fn load_jsonl_reader_once( __dst_er.sort_by(|a, b| a.0.cmp(&b.0)); for (edge_name, rows) in __dst_er { let edge_type = &catalog.edge_types[&edge_name]; - let batch = build_edge_batch(edge_type, &rows, &node_id_remap, catalog.system_columns)?; + let batch = build_edge_batch( + edge_type, + &rows, + &node_id_remap, + catalog.system_columns, + write_budget, + )?; if bounded_keyed_input { - prepared_keyed_bytes = retain_keyed_batch(prepared_keyed_bytes, &batch)?; + prepared_keyed_bytes = retain_keyed_batch(prepared_keyed_bytes, &batch, write_budget)?; } // Validation (enum/unique, edge-RI, @card) runs end-of-load via the evaluator. let loaded_count = batch.num_rows(); @@ -859,9 +889,9 @@ async fn load_jsonl_reader_once( let _entry = snapshot .dataset(&table_key) .ok_or_else(|| OmniError::manifest(missing_graph_type_at_snapshot(&table_key)))?; - let batch = normalize_strict_json_rows(&catalog, &table_key, &rows)?; + let batch = normalize_strict_json_rows(&catalog, &table_key, &rows, write_budget)?; if bounded_keyed_input { - prepared_keyed_bytes = retain_keyed_batch(prepared_keyed_bytes, &batch)?; + prepared_keyed_bytes = retain_keyed_batch(prepared_keyed_bytes, &batch, write_budget)?; } let loaded_count = batch.num_rows(); prepared_edges.push((edge_name, table_key, vec![batch], loaded_count)); @@ -907,7 +937,7 @@ async fn load_jsonl_reader_once( // Bob while a retained `edge:Knows(Alice->Bob)` would otherwise publish an // orphan. (Per-table, like the rest of Overwrite handling.) if mode == LoadMode::Overwrite { - let mut removed_id_budget = DeletedIdBudget::default(); + let mut removed_id_budget = DeletedIdBudget::new(write_budget); let keys: Vec = changeset.keys().cloned().collect(); for table_key in keys { let removed = crate::validate::overwrite_removed_ids( @@ -1248,7 +1278,13 @@ fn parse_strict_graph_rows( let table_key = format!("node:{type_name}"); let row = JsonValue::Object(data); if bounded_keyed_input { - account_keyed_json_row(&table_key, &row, 0, keyed_input_budget)?; + account_keyed_json_row( + &table_key, + &row, + 0, + &catalog.node_types[&type_name].properties, + keyed_input_budget, + )?; } rows.nodes.entry(type_name).or_default().push(row); } @@ -1289,6 +1325,7 @@ fn parse_strict_graph_rows( &table_key, &JsonValue::Object(data.clone()), from.len().saturating_add(to.len()), + &edge_type.properties, keyed_input_budget, )?; } @@ -1434,6 +1471,8 @@ fn take_object_or_empty( struct KeyedInputBudget { tables: HashMap, bytes: u64, + payload_bytes: u64, + write_budget: WriteBudget, } #[derive(Default)] @@ -1449,6 +1488,7 @@ fn account_keyed_json_row( table_key: &str, data: &JsonValue, structural_string_bytes: usize, + properties: &HashMap, budgets: &mut KeyedInputBudget, ) -> Result<()> { let entry = budgets.tables.entry(table_key.to_string()).or_default(); @@ -1463,7 +1503,44 @@ fn account_keyed_json_row( entry.rows as u64, )); } - let row_bytes = estimate_json_arrow_bytes(data)? + let object = data + .as_object() + .ok_or_else(|| OmniError::manifest("input data must be an object"))?; + let mut estimated = 0_u64; + let mut payload = 0_u64; + for (name, value) in object { + let row_bytes = if properties.get(name).is_some_and(|property| { + property.scalar == omnigraph_compiler::types::ScalarType::Blob && !property.list + }) { + let encoded = value + .as_str() + .and_then(|value| value.strip_prefix("base64:")); + if let Some(encoded) = encoded { + payload = payload + .checked_add(decoded_blob_bytes(encoded)?) + .ok_or_else(|| { + OmniError::manifest_internal("keyed input Blob payload overflow") + })?; + 16 + } else { + estimate_json_arrow_bytes(value)?.saturating_add(16) + } + } else { + estimate_json_arrow_bytes(value)? + }; + estimated = estimated + .checked_add(row_bytes) + .ok_or_else(|| OmniError::manifest_internal("keyed input row bytes overflow"))?; + } + let payload_bytes = budgets + .payload_bytes + .checked_add(payload) + .ok_or_else(|| OmniError::manifest_internal("keyed input Blob payload overflow"))?; + budgets + .write_budget + .check("decoded blob input bytes per operation", payload_bytes)?; + budgets.payload_bytes = payload_bytes; + let row_bytes = estimated .checked_add( u64::try_from(structural_string_bytes) .map_err(|_| OmniError::manifest_internal("keyed string bytes exceed u64"))?, @@ -1473,20 +1550,20 @@ fn account_keyed_json_row( .bytes .checked_add(row_bytes) .ok_or_else(|| OmniError::manifest_internal("keyed parsed byte count overflow"))?; - if entry.bytes > KEYED_WRITE_MAX_BYTES { + if entry.bytes > budgets.write_budget.bytes() { return Err(OmniError::resource_limit( format!("keyed parsed entity bytes for {table_key}"), - KEYED_WRITE_MAX_BYTES, + budgets.write_budget.bytes(), entry.bytes, )); } let total = budgets.bytes.checked_add(row_bytes).ok_or_else(|| { OmniError::manifest_internal("keyed parsed operation byte count overflow") })?; - if total > KEYED_WRITE_MAX_BYTES { + if total > budgets.write_budget.bytes() { return Err(OmniError::resource_limit( "keyed parsed entity bytes per operation", - KEYED_WRITE_MAX_BYTES, + budgets.write_budget.bytes(), total, )); } @@ -1501,21 +1578,8 @@ fn estimate_json_arrow_bytes(value: &JsonValue) -> Result { // Four bytes avoids rejecting valid Float32/Int32 input early. Wider // physical scalars are charged exactly by the later Arrow batch check. JsonValue::Number(_) => Ok(4), - JsonValue::String(value) => { - let bytes = match value.strip_prefix("base64:") { - Some(encoded) => base64::decoded_len_estimate(encoded.len()).saturating_sub( - encoded - .as_bytes() - .iter() - .rev() - .take_while(|&&byte| byte == b'=') - .count(), - ), - None => value.len(), - }; - u64::try_from(bytes) - .map_err(|_| OmniError::manifest_internal("JSON string bytes exceed u64")) - } + JsonValue::String(value) => u64::try_from(value.len()) + .map_err(|_| OmniError::manifest_internal("JSON string bytes exceed u64")), JsonValue::Array(values) => { let offsets = u64::try_from(values.len()) .map_err(|_| OmniError::manifest_internal("JSON array length exceeds u64"))? @@ -1578,6 +1642,7 @@ fn build_node_batch( rows: &[JsonValue], node_id_remap: &mut TypedNodeIdRemap, system_columns: SystemColumns, + write_budget: WriteBudget, ) -> Result { let schema = node_type.arrow_schema.clone(); let row_refs = rows.iter().collect::>(); @@ -1585,6 +1650,7 @@ fn build_node_batch( &format!("node:{}", node_type.name), node_type.blob_properties.iter().map(String::as_str), &row_refs, + write_budget, )?; // Materialize the typed property columns before deriving physical ids. A @@ -1594,7 +1660,7 @@ fn build_node_batch( let mut property_columns: Vec = Vec::with_capacity(schema.fields().len() - 1); for field in schema.fields().iter().skip(1) { if node_type.blob_properties.contains(field.name()) { - let col = build_blob_column(field.name(), field.is_nullable(), rows)?; + let col = build_blob_column(field.name(), field.is_nullable(), rows, write_budget)?; property_columns.push(col); } else { let col = build_column_from_json( @@ -1603,6 +1669,7 @@ fn build_node_batch( field.is_nullable(), rows, JsonConversionMode::LoaderCompat, + write_budget, )?; property_columns.push(col); } @@ -1707,6 +1774,7 @@ fn build_edge_batch( rows: &[(String, String, JsonValue)], node_id_remap: &TypedNodeIdRemap, system_columns: SystemColumns, + write_budget: WriteBudget, ) -> Result { let schema = edge_type.arrow_schema.clone(); let row_refs = rows.iter().map(|(_, _, data)| data).collect::>(); @@ -1714,6 +1782,7 @@ fn build_edge_batch( &format!("edge:{}", edge_type.name), edge_type.blob_properties.iter().map(String::as_str), &row_refs, + write_budget, )?; let srcs: Vec = rows @@ -1743,7 +1812,12 @@ fn build_edge_batch( Vec::with_capacity(schema.fields().len().saturating_sub(3)); for field in schema.fields().iter().skip(3) { if edge_type.blob_properties.contains(field.name()) { - let col = build_blob_column(field.name(), field.is_nullable(), &data_values)?; + let col = build_blob_column( + field.name(), + field.is_nullable(), + &data_values, + write_budget, + )?; property_columns.push(col); } else { let col = build_column_from_json( @@ -1752,6 +1826,7 @@ fn build_edge_batch( field.is_nullable(), &data_values, JsonConversionMode::LoaderCompat, + write_budget, )?; property_columns.push(col); } @@ -1866,6 +1941,7 @@ pub(crate) fn normalize_strict_json_rows( catalog: &Catalog, table_key: &str, rows: &[JsonValue], + write_budget: WriteBudget, ) -> Result { if rows.is_empty() { return Err(OmniError::manifest_internal( @@ -1877,13 +1953,13 @@ pub(crate) fn normalize_strict_json_rows( .node_types .get(type_name) .ok_or_else(|| OmniError::manifest(format!("unknown node type '{type_name}'")))?; - normalize_strict_node_rows(node_type, rows, catalog.system_columns) + normalize_strict_node_rows(node_type, rows, catalog.system_columns, write_budget) } else if let Some(type_name) = table_key.strip_prefix("edge:") { let edge_type = catalog .edge_types .get(type_name) .ok_or_else(|| OmniError::manifest(format!("unknown edge type '{type_name}'")))?; - normalize_strict_edge_rows(edge_type, rows, catalog.system_columns) + normalize_strict_edge_rows(edge_type, rows, catalog.system_columns, write_budget) } else { Err(OmniError::manifest(format!( "invalid table key '{table_key}'" @@ -1895,6 +1971,7 @@ fn normalize_strict_node_rows( node_type: &NodeType, rows: &[JsonValue], system_columns: SystemColumns, + write_budget: WriteBudget, ) -> Result { let objects = strict_row_objects(rows)?; let table_key = format!("node:{}", node_type.name); @@ -1914,18 +1991,19 @@ fn normalize_strict_node_rows( &schema, &objects, &node_type.blob_properties, - KEYED_WRITE_MAX_BYTES, + write_budget.bytes(), )?; preflight_blob_decode_budget( &table_key, node_type.blob_properties.iter().map(String::as_str), &rows.iter().collect::>(), + write_budget, )?; let mut property_columns = Vec::with_capacity(schema.fields().len().saturating_sub(1)); for field in schema.fields().iter().skip(1) { let column = if node_type.blob_properties.contains(field.name()) { - build_blob_column(field.name(), field.is_nullable(), rows)? + build_blob_column(field.name(), field.is_nullable(), rows, write_budget)? } else { build_column_from_json( field.name(), @@ -1933,6 +2011,7 @@ fn normalize_strict_node_rows( field.is_nullable(), rows, JsonConversionMode::Strict, + write_budget, )? }; property_columns.push(column); @@ -2001,6 +2080,7 @@ fn normalize_strict_edge_rows( edge_type: &EdgeType, rows: &[JsonValue], system_columns: SystemColumns, + write_budget: WriteBudget, ) -> Result { let objects = strict_row_objects(rows)?; let table_key = format!("edge:{}", edge_type.name); @@ -2022,12 +2102,13 @@ fn normalize_strict_edge_rows( &schema, &objects, &edge_type.blob_properties, - KEYED_WRITE_MAX_BYTES, + write_budget.bytes(), )?; preflight_blob_decode_budget( &table_key, edge_type.blob_properties.iter().map(String::as_str), &rows.iter().collect::>(), + write_budget, )?; let srcs = objects @@ -2044,7 +2125,7 @@ fn normalize_strict_edge_rows( let mut property_columns = Vec::with_capacity(schema.fields().len().saturating_sub(3)); for field in schema.fields().iter().skip(3) { let column = if edge_type.blob_properties.contains(field.name()) { - build_blob_column(field.name(), field.is_nullable(), rows)? + build_blob_column(field.name(), field.is_nullable(), rows, write_budget)? } else { build_column_from_json( field.name(), @@ -2052,6 +2133,7 @@ fn normalize_strict_edge_rows( field.is_nullable(), rows, JsonConversionMode::Strict, + write_budget, )? }; property_columns.push(column); @@ -2236,7 +2318,10 @@ fn preflight_strict_rows_arrow_bytes( for field in schema.fields() { let value = object.get(field.name()).unwrap_or(&JsonValue::Null); let field_bytes = if blob_properties.contains(field.name()) { - 16_u64.saturating_add(estimate_json_arrow_bytes(value)?) + 16_u64.saturating_add(match value.as_str() { + Some(value) if value.starts_with("base64:") => 0, + _ => estimate_json_arrow_bytes(value)?, + }) } else { projected_strict_column_bytes(field.data_type(), value)? }; @@ -2327,6 +2412,7 @@ fn preflight_blob_decode_budget<'a>( table_key: &str, blob_properties: impl Iterator, rows: &[&JsonValue], + write_budget: WriteBudget, ) -> Result<()> { let mut decoded_bytes = 0_u64; for property in blob_properties { @@ -2338,21 +2424,14 @@ fn preflight_blob_decode_budget<'a>( else { continue; }; - let estimate = base64::decoded_len_estimate(encoded.len()).saturating_sub( - encoded - .as_bytes() - .iter() - .rev() - .take_while(|&&byte| byte == b'=') - .count(), - ) as u64; + let estimate = decoded_blob_bytes(encoded)?; decoded_bytes = decoded_bytes.checked_add(estimate).ok_or_else(|| { OmniError::manifest_internal("decoded blob input byte count overflow") })?; - if decoded_bytes > KEYED_WRITE_MAX_BYTES { + if decoded_bytes > write_budget.bytes() { return Err(OmniError::resource_limit( format!("decoded blob input bytes for {table_key}"), - KEYED_WRITE_MAX_BYTES, + write_budget.bytes(), decoded_bytes, )); } @@ -2361,22 +2440,32 @@ fn preflight_blob_decode_budget<'a>( Ok(()) } +pub(crate) fn decoded_blob_bytes(encoded: &str) -> Result { + let bytes = base64::decoded_len_estimate(encoded.len()).saturating_sub( + encoded + .as_bytes() + .iter() + .rev() + .take_while(|&&byte| byte == b'=') + .take(2) + .count(), + ); + u64::try_from(bytes).map_err(|_| OmniError::manifest_internal("decoded Blob bytes exceed u64")) +} + /// Append a blob value (URI or base64 bytes) to a BlobArrayBuilder. -pub(crate) fn append_blob_value(builder: &mut BlobArrayBuilder, value: &str) -> Result<()> { +pub(crate) fn append_blob_value( + builder: &mut BlobArrayBuilder, + value: &str, + write_budget: WriteBudget, +) -> Result<()> { if let Some(encoded) = value.strip_prefix("base64:") { - let decoded_estimate = base64::decoded_len_estimate(encoded.len()).saturating_sub( - encoded - .as_bytes() - .iter() - .rev() - .take_while(|&&b| b == b'=') - .count(), - ); - if decoded_estimate as u64 > KEYED_WRITE_MAX_BYTES { + let decoded_estimate = decoded_blob_bytes(encoded)?; + if decoded_estimate > write_budget.bytes() { return Err(OmniError::resource_limit( "decoded blob input bytes", - KEYED_WRITE_MAX_BYTES, - decoded_estimate as u64, + write_budget.bytes(), + decoded_estimate, )); } let bytes = base64::engine::general_purpose::STANDARD @@ -2393,12 +2482,17 @@ pub(crate) fn append_blob_value(builder: &mut BlobArrayBuilder, value: &str) -> } /// Build a blob column from JSON values using Lance BlobArrayBuilder. -fn build_blob_column(name: &str, nullable: bool, rows: &[JsonValue]) -> Result { +fn build_blob_column( + name: &str, + nullable: bool, + rows: &[JsonValue], + write_budget: WriteBudget, +) -> Result { let mut builder = BlobArrayBuilder::new(rows.len()); for row in rows { match row.get(name) { Some(JsonValue::String(s)) => { - append_blob_value(&mut builder, s)?; + append_blob_value(&mut builder, s, write_budget)?; } Some(JsonValue::Null) | None if nullable => { builder.push_null().map_err(OmniError::lance_internal)?; @@ -2432,6 +2526,7 @@ fn build_column_from_json( nullable: bool, rows: &[JsonValue], mode: JsonConversionMode, + write_budget: WriteBudget, ) -> Result { let array: ArrayRef = match data_type { DataType::Utf8 => { @@ -2657,10 +2752,10 @@ fn build_column_from_json( .and_then(|rows| rows.checked_mul(u64::try_from(dim_usize).ok()?)) .and_then(|values| values.checked_mul(4)) .unwrap_or(u64::MAX); - if allocation_bytes > KEYED_WRITE_MAX_BYTES { + if allocation_bytes > write_budget.bytes() { return Err(OmniError::resource_limit( "strict_input_arrow_bytes", - KEYED_WRITE_MAX_BYTES, + write_budget.bytes(), allocation_bytes, )); } @@ -3581,8 +3676,15 @@ edge WorksAt: Person -> Company let rows = vec![wrong.clone()]; for mode in modes { for nullable in [true, false] { - let err = build_column_from_json("day", &data_type, nullable, &rows, mode) - .expect_err("a wrong-typed date is refused, never stored as NULL"); + let err = build_column_from_json( + "day", + &data_type, + nullable, + &rows, + mode, + WriteBudget::default(), + ) + .expect_err("a wrong-typed date is refused, never stored as NULL"); assert!( err.to_string().contains(expected), "{mode:?} {data_type:?} nullable={nullable} {wrong}: {err}" @@ -3597,8 +3699,15 @@ edge WorksAt: Person -> Company let list_rows = vec![serde_json::json!({"days": [19723, wrong["day"].clone()]})]; let list_expected = expected.replace("'day'", "'days'"); for mode in modes { - let err = build_column_from_json("days", &list_type, true, &list_rows, mode) - .expect_err("a wrong-typed date list item is refused, never stored as NULL"); + let err = build_column_from_json( + "days", + &list_type, + true, + &list_rows, + mode, + WriteBudget::default(), + ) + .expect_err("a wrong-typed date list item is refused, never stored as NULL"); assert!( err.to_string().contains(&list_expected), "{mode:?} {data_type:?}: {err}" @@ -3635,6 +3744,7 @@ edge WorksAt: Person -> Company true, std::slice::from_ref(&good), JsonConversionMode::LoaderCompat, + WriteBudget::default(), ) .expect("integer counts, date strings, and null still load"); let value = match data_type { @@ -3805,6 +3915,7 @@ edge WorksAt: Person -> Company true, &wrong_scalar, JsonConversionMode::LoaderCompat, + WriteBudget::default(), ) .unwrap(); assert!( @@ -3817,6 +3928,7 @@ edge WorksAt: Person -> Company true, &wrong_scalar, JsonConversionMode::Strict, + WriteBudget::default(), ) .expect_err("strict normalization must not turn a wrong type into null"); assert!(strict.to_string().contains("expects Int32"), "{strict:?}"); @@ -3833,6 +3945,7 @@ edge WorksAt: Person -> Company false, &wrong_list, JsonConversionMode::LoaderCompat, + WriteBudget::default(), ) .expect("bulk-load compatibility retains nullable list-item coercion"); let strict = build_column_from_json( @@ -3841,6 +3954,7 @@ edge WorksAt: Person -> Company false, &wrong_list, JsonConversionMode::Strict, + WriteBudget::default(), ) .expect_err("strict normalization must reject a wrong list item"); assert!( @@ -3855,6 +3969,7 @@ edge WorksAt: Person -> Company true, &missing_nullable, JsonConversionMode::Strict, + WriteBudget::default(), ) .expect("a missing nullable strict-row property remains null"); assert!(strict.is_null(0)); @@ -3869,6 +3984,7 @@ edge WorksAt: Person -> Company true, &missing_nullable, JsonConversionMode::Strict, + WriteBudget::default(), ) .expect_err("strict normalization must bound vector allocation before building"); assert!( @@ -3949,8 +4065,8 @@ edge WorksAt: Person -> Company fn operation_byte_allowances_span_graph_types_and_include_their_ceiling() { let row = serde_json::json!({"payload": "x".repeat(17 * 1024 * 1024)}); let mut budget = KeyedInputBudget::default(); - account_keyed_json_row("node:Person", &row, 0, &mut budget).unwrap(); - let error = account_keyed_json_row("node:Company", &row, 0, &mut budget) + account_keyed_json_row("node:Person", &row, 0, &HashMap::new(), &mut budget).unwrap(); + let error = account_keyed_json_row("node:Company", &row, 0, &HashMap::new(), &mut budget) .expect_err("keyed parse bytes must be aggregated across types"); assert!(matches!(error, OmniError::ResourceLimitExceeded { ref resource, limit: KEYED_WRITE_MAX_BYTES, actual } @@ -3959,10 +4075,22 @@ edge WorksAt: Person -> Company )); assert_eq!( - crate::storage_layer::retained_keyed_bytes(KEYED_WRITE_MAX_BYTES - 1, 1).unwrap(), + crate::storage_layer::retained_keyed_bytes( + KEYED_WRITE_MAX_BYTES - 1, + 1, + WriteBudget::default() + ) + .unwrap(), KEYED_WRITE_MAX_BYTES ); - assert!(crate::storage_layer::retained_keyed_bytes(KEYED_WRITE_MAX_BYTES, 1).is_err()); + assert!( + crate::storage_layer::retained_keyed_bytes( + KEYED_WRITE_MAX_BYTES, + 1, + WriteBudget::default() + ) + .is_err() + ); let id = &row["payload"].as_str().unwrap()[..16 * 1024 * 1024 - std::mem::size_of::()]; let mut removed = DeletedIdBudget::default(); @@ -4879,12 +5007,12 @@ node Doc { "x".repeat(crate::blob::EXTERNAL_BLOB_URI_MAX_BYTES as usize - prefix.len()) ); let mut builder = BlobArrayBuilder::new(1); - append_blob_value(&mut builder, &exact).unwrap(); + append_blob_value(&mut builder, &exact, WriteBudget::default()).unwrap(); let oversized = format!("{exact}x"); let mut builder = BlobArrayBuilder::new(1); assert!(matches!( - append_blob_value(&mut builder, &oversized), + append_blob_value(&mut builder, &oversized, WriteBudget::default(),), Err(OmniError::ResourceLimitExceeded { resource, limit: crate::blob::EXTERNAL_BLOB_URI_MAX_BYTES, @@ -4895,13 +5023,26 @@ node Doc { let mut builder = BlobArrayBuilder::new(1); assert!(matches!( - append_blob_value(&mut builder, ""), + append_blob_value(&mut builder, "", WriteBudget::default(),), Err(OmniError::ExternalBlobPolicy { uri, reason }) if uri == "" && reason == "external Blob URI must be non-empty" )); } + #[test] + fn write_max_bytes_refuses_excess_base64_padding_before_decode_allocation() { + let settings = omnigraph_compiler::settings::SessionSettings::default() + .with("write_max_bytes", "8") + .unwrap(); + let mut builder = BlobArrayBuilder::new(1); + let value = format!("base64:{}", "=".repeat(40)); + assert!(matches!( + append_blob_value(&mut builder, &value, WriteBudget::from_settings(&settings)), + Err(OmniError::ResourceLimitExceeded { limit: 8, actual, .. }) if actual > 8 + )); + } + #[test] fn checked_json_f32_accepts_boundary_and_rejects_overflow_and_nonfinite() { assert_eq!(checked_json_f32(f32::MAX as f64, "test").unwrap(), f32::MAX); diff --git a/crates/omnigraph/src/session.rs b/crates/omnigraph/src/session.rs index eac97eaa7..cb1d08a01 100644 --- a/crates/omnigraph/src/session.rs +++ b/crates/omnigraph/src/session.rs @@ -277,6 +277,25 @@ mod tests { (rows[0].value.clone(), rows[0].source) } + #[test] + fn write_max_bytes_default_is_the_existing_ceiling() { + assert_eq!( + SessionSettings::default().write_max_bytes(), + crate::storage_layer::KEYED_WRITE_MAX_BYTES + ); + assert_eq!( + SettingId::WriteMaxBytes.spec().default, + crate::storage_layer::KEYED_WRITE_MAX_BYTES.to_string() + ); + assert!(matches!( + SettingId::WriteMaxBytes.spec().kind, + omnigraph_compiler::settings::SettingKind::Integer { + min: 1, + max: Some(33_554_432) + } + )); + } + #[test] fn history_release_bytes_default_is_the_catalog_constant() { assert_eq!( diff --git a/crates/omnigraph/src/storage_layer.rs b/crates/omnigraph/src/storage_layer.rs index dc9dcf623..96c7ccc4e 100644 --- a/crates/omnigraph/src/storage_layer.rs +++ b/crates/omnigraph/src/storage_layer.rs @@ -73,27 +73,60 @@ use crate::table_store::{ pub(crate) const KEYED_WRITE_MAX_ROWS: usize = 8192; pub(crate) const KEYED_WRITE_MAX_BYTES: u64 = 32 * 1024 * 1024; -/// The operation-wide sibling of the per-table keyed ceiling. This uses the -/// existing Arrow accounting (including its conservative shared-buffer count), -/// not a second allocator or a claim about native execution/RSS. -pub(crate) fn retained_keyed_bytes(current: u64, additional: u64) -> Result { +/// Immutable allowance captured from the effective session once per operation. +/// Each existing row, payload, parser and removed-id account has this ceiling; +/// the accounts remain independent and keep their original scopes. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct WriteBudget(u64); + +impl WriteBudget { + pub(crate) fn from_settings(settings: &omnigraph_compiler::settings::SessionSettings) -> Self { + Self(settings.write_max_bytes()) + } + + pub(crate) fn bytes(self) -> u64 { + self.0 + } + + pub(crate) fn check(self, resource: impl Into, actual: u64) -> Result<()> { + if actual > self.0 { + return Err(OmniError::resource_limit(resource, self.0, actual)); + } + Ok(()) + } +} + +impl Default for WriteBudget { + fn default() -> Self { + Self::from_settings(&omnigraph_compiler::settings::SessionSettings::default()) + } +} + +/// The operation-wide sibling of the per-table row-data ceiling. Arrow +/// bookkeeping and shared buffers retain their conservative accounting; +/// logical Blob payload buffers have a separate allowance. +pub(crate) fn retained_keyed_bytes( + current: u64, + additional: u64, + budget: WriteBudget, +) -> Result { let actual = current .checked_add(additional) .ok_or_else(|| OmniError::manifest_internal("retained keyed batch byte count overflow"))?; - if actual > KEYED_WRITE_MAX_BYTES { - return Err(OmniError::resource_limit( - "retained keyed batch bytes per operation", - KEYED_WRITE_MAX_BYTES, - actual, - )); - } + budget.check("retained keyed batch bytes per operation", actual)?; Ok(actual) } -pub(crate) fn retain_keyed_batch(current: u64, batch: &RecordBatch) -> Result { - let bytes = u64::try_from(batch.get_array_memory_size()) - .map_err(|_| OmniError::manifest_internal("retained keyed batch bytes exceed u64"))?; - retained_keyed_bytes(current, bytes) +pub(crate) fn retain_keyed_batch( + current: u64, + batch: &RecordBatch, + budget: WriteBudget, +) -> Result { + retained_keyed_bytes( + current, + crate::table_store::write_batch_bytes(batch)?.rows, + budget, + ) } /// One allowance per mutation (all tables and cascades) or load (all replacement @@ -102,9 +135,14 @@ pub(crate) fn retain_keyed_batch(current: u64, batch: &RecordBatch) -> Result Self { + Self { bytes: 0, budget } + } + pub(crate) fn retain(&mut self, id: &str) -> Result<()> { let bytes = id .len() @@ -114,13 +152,8 @@ impl DeletedIdBudget { .ok_or_else(|| { OmniError::manifest_internal("retained removed-id byte count overflow") })?; - if bytes > KEYED_WRITE_MAX_BYTES { - return Err(OmniError::resource_limit( - "retained removed-id bytes per operation", - KEYED_WRITE_MAX_BYTES, - bytes, - )); - } + self.budget + .check("retained removed-id bytes per operation", bytes)?; self.bytes = bytes; Ok(()) } @@ -133,11 +166,20 @@ impl DeletedIdBudget { /// time holds one batch of read-ahead. pub(crate) const BLOB_READ_IO_BUFFER_BYTES: u64 = 8 * 1024 * 1024; +/// Resources a mutation already retains before a pending-aware update scan: +/// `rows` on the scanned table, `bytes` and `payload_bytes` across every table. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub(crate) struct PendingUsage { + pub(crate) rows: u64, + pub(crate) bytes: u64, + pub(crate) payload_bytes: u64, +} + /// Resource budget for a pending-aware keyed scan that will feed one mutation /// table transaction. /// -/// `initial_rows` accounts for batches already accumulated on this table; -/// `initial_bytes` accounts for every table already retained by the graph +/// `initial.rows` accounts for batches already accumulated on this table; +/// `initial.bytes` accounts for every table already retained by the graph /// mutation. A later `update` allocates another full-row batch before the /// end-of-query dedupe, so its matched committed/pending view must fit in the /// remaining operation byte budget rather than receiving a fresh 32-MiB @@ -147,16 +189,20 @@ pub(crate) const BLOB_READ_IO_BUFFER_BYTES: u64 = 8 * 1024 * 1024; #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) struct PendingScanBudget { pub(crate) table_key: String, - pub(crate) initial_rows: u64, - pub(crate) initial_bytes: u64, + pub(crate) initial: PendingUsage, + pub(crate) write_budget: WriteBudget, } impl PendingScanBudget { - pub(crate) fn new(table_key: impl Into, initial_rows: u64, initial_bytes: u64) -> Self { + pub(crate) fn new( + table_key: impl Into, + initial: PendingUsage, + write_budget: WriteBudget, + ) -> Self { Self { table_key: table_key.into(), - initial_rows, - initial_bytes, + initial, + write_budget, } } } @@ -626,6 +672,7 @@ pub trait TableStorage: sealed::Sealed + Send + Sync + Debug { table_key: &str, batch: RecordBatch, system_columns: SystemColumns, + write_budget: WriteBudget, ) -> Result; /// Authorize and probe the complete operation's distinct external Blob @@ -640,6 +687,7 @@ pub trait TableStorage: sealed::Sealed + Send + Sync + Debug { batch: RecordBatch, preflight: &ExternalBlobPreflight, system_columns: SystemColumns, + write_budget: WriteBudget, ) -> Result; /// Rewrite retained Overwrite URI cells to the exact normalized targets @@ -685,6 +733,7 @@ pub trait TableStorage: sealed::Sealed + Send + Sync + Debug { batch: RecordBatch, semantics: KeyedWriteSemantics, system_columns: SystemColumns, + write_budget: WriteBudget, ) -> Result; /// Stage a provenance-proven strict insert without re-running Lance's @@ -698,6 +747,7 @@ pub trait TableStorage: sealed::Sealed + Send + Sync + Debug { snapshot: SnapshotHandle, chunk: ProvenInsertChunk, system_columns: SystemColumns, + write_budget: WriteBudget, ) -> Result; /// Blob-aware full-row stream with an explicit batch ceiling. Branch @@ -720,6 +770,7 @@ pub trait TableStorage: sealed::Sealed + Send + Sync + Debug { interval: &crate::table_store::ProvenInsertInterval, external_preflight: &ExternalBlobPreflight, system_columns: SystemColumns, + write_budget: WriteBudget, ) -> Result; #[cfg(test)] @@ -1117,8 +1168,10 @@ impl TableStorage for TableStore { table_key: &str, batch: RecordBatch, system_columns: SystemColumns, + write_budget: WriteBudget, ) -> Result { - TableStore::prepare_keyed_write_batch(self, table_key, batch, system_columns).await + TableStore::prepare_keyed_write_batch(self, table_key, batch, system_columns, write_budget) + .await } async fn preflight_external_blob_uris(&self, uris: &[String]) -> Result { @@ -1131,6 +1184,7 @@ impl TableStorage for TableStore { batch: RecordBatch, preflight: &ExternalBlobPreflight, system_columns: SystemColumns, + write_budget: WriteBudget, ) -> Result { TableStore::prepare_keyed_write_batch_with_preflight( self, @@ -1138,6 +1192,7 @@ impl TableStorage for TableStore { batch, preflight, system_columns, + write_budget, ) .await } @@ -1187,11 +1242,20 @@ impl TableStorage for TableStore { batch: RecordBatch, semantics: KeyedWriteSemantics, system_columns: SystemColumns, + write_budget: WriteBudget, ) -> Result { let ds = Arc::try_unwrap(snapshot.into_arc()).unwrap_or_else(|arc| (*arc).clone()); - TableStore::stage_keyed_write(self, ds, table_key, batch, semantics, system_columns) - .await - .map(StagedHandle::new) + TableStore::stage_keyed_write( + self, + ds, + table_key, + batch, + semantics, + system_columns, + write_budget, + ) + .await + .map(StagedHandle::new) } async fn stage_proven_strict_insert( @@ -1199,9 +1263,10 @@ impl TableStorage for TableStore { snapshot: SnapshotHandle, chunk: ProvenInsertChunk, system_columns: SystemColumns, + write_budget: WriteBudget, ) -> Result { let ds = Arc::try_unwrap(snapshot.into_arc()).unwrap_or_else(|arc| (*arc).clone()); - TableStore::stage_proven_strict_insert(self, ds, chunk, system_columns) + TableStore::stage_proven_strict_insert(self, ds, chunk, system_columns, write_budget) .await .map(StagedHandle::new) } @@ -1223,6 +1288,7 @@ impl TableStorage for TableStore { interval: &crate::table_store::ProvenInsertInterval, external_preflight: &ExternalBlobPreflight, system_columns: SystemColumns, + write_budget: WriteBudget, ) -> Result { TableStore::scan_proven_insert_delta_bounded( self, @@ -1231,6 +1297,7 @@ impl TableStorage for TableStore { interval, external_preflight, system_columns, + write_budget, ) .await } diff --git a/crates/omnigraph/src/table_store.rs b/crates/omnigraph/src/table_store.rs index ff5c05dc5..c74f9c2f0 100644 --- a/crates/omnigraph/src/table_store.rs +++ b/crates/omnigraph/src/table_store.rs @@ -1,3 +1,4 @@ +use crate::storage_layer::WriteBudget; use arrow_array::{ Array, ArrayRef, LargeBinaryArray, RecordBatch, StringArray, StructArray, UInt64Array, builder::StringBuilder, @@ -1898,7 +1899,7 @@ impl TableStore { && total > limit { return Err(OmniError::resource_limit( - "materialized blob payload bytes", + MATERIALIZED_BLOB_PAYLOAD_BYTES, limit, total, )); @@ -1998,11 +1999,11 @@ impl TableStore { batch: &RecordBatch, external: &ExternalBlobPreflight, max_blob_bytes: u64, - ) -> Result { + ) -> Result { let payload = self.persisted_blob_payload_bytes(ds.schema(), batch, external)?; if payload > max_blob_bytes { return Err(OmniError::resource_limit( - "materialized blob payload bytes", + MATERIALIZED_BLOB_PAYLOAD_BYTES, max_blob_bytes, payload, )); @@ -2010,8 +2011,9 @@ impl TableStore { let retained = u64::try_from(batch.get_array_memory_size()).map_err(|_| { OmniError::manifest_internal("persisted Blob batch memory size exceeds u64") })?; - retained.checked_add(payload).ok_or_else(|| { - OmniError::manifest_internal("materialized Blob batch byte count overflow") + Ok(WriteBatchBytes { + rows: retained, + payload, }) } @@ -2858,14 +2860,16 @@ impl TableStore { batch: RecordBatch, semantics: KeyedWriteSemantics, system_columns: SystemColumns, + write_budget: WriteBudget, ) -> Result { if batch.num_rows() == 0 { return Err(OmniError::manifest_internal( "stage_keyed_write called with empty batch", )); } - let batch_bytes = u64::try_from(batch.get_array_memory_size()) - .map_err(|_| OmniError::manifest_internal("keyed write batch bytes exceed u64"))?; + let usage = write_batch_bytes(&batch)?; + let batch_bytes = usage.rows; + write_budget.check(MATERIALIZED_BLOB_PAYLOAD_BYTES, usage.payload)?; if batch.num_rows() > KEYED_WRITE_MAX_ROWS { return Err(OmniError::resource_limit( format!("keyed write entities for {type_key}"), @@ -2873,10 +2877,10 @@ impl TableStore { batch.num_rows() as u64, )); } - if batch_bytes > KEYED_WRITE_MAX_BYTES { + if batch_bytes > write_budget.bytes() { return Err(OmniError::resource_limit( format!("keyed write bytes for {type_key}"), - KEYED_WRITE_MAX_BYTES, + write_budget.bytes(), batch_bytes, )); } @@ -2897,6 +2901,7 @@ impl TableStore { &expected_schema_preorder_ids, "stage_keyed_write", system_columns, + write_budget, ) .await; } @@ -2908,7 +2913,7 @@ impl TableStore { // retains keyed fencing without an Append side door; Overwrite keeps // Lance's external-reference behavior because it accepts WriteParams. let batch = self - .prepare_keyed_write_batch(type_key, batch, system_columns) + .prepare_keyed_write_batch(type_key, batch, system_columns, write_budget) .await?; let merged_rows = batch.num_rows() as u64; @@ -2968,6 +2973,7 @@ impl TableStore { ds: Dataset, chunk: ProvenInsertChunk, system_columns: SystemColumns, + write_budget: WriteBudget, ) -> Result { let ( table_key, @@ -3007,6 +3013,7 @@ impl TableStore { &expected_schema_preorder_ids, "stage_proven_strict_insert", system_columns, + write_budget, ) .await } @@ -3028,15 +3035,16 @@ impl TableStore { expected_schema_preorder_ids: &[u32], context: &'static str, system_columns: SystemColumns, + write_budget: WriteBudget, ) -> Result { if batch.num_rows() == 0 { return Err(OmniError::manifest_internal(format!( "{context} called with empty batch" ))); } - let batch_bytes = u64::try_from(batch.get_array_memory_size()).map_err(|_| { - OmniError::manifest_internal(format!("{context} batch bytes exceed u64")) - })?; + let usage = write_batch_bytes(&batch)?; + let batch_bytes = usage.rows; + write_budget.check(MATERIALIZED_BLOB_PAYLOAD_BYTES, usage.payload)?; if batch.num_rows() > KEYED_WRITE_MAX_ROWS { return Err(OmniError::resource_limit( format!("keyed write entities for {table_key}"), @@ -3044,16 +3052,16 @@ impl TableStore { batch.num_rows() as u64, )); } - if batch_bytes > KEYED_WRITE_MAX_BYTES { + if batch_bytes > write_budget.bytes() { return Err(OmniError::resource_limit( format!("keyed write bytes for {table_key}"), - KEYED_WRITE_MAX_BYTES, + write_budget.bytes(), batch_bytes, )); } let batch = self - .prepare_keyed_write_batch(table_key, batch, system_columns) + .prepare_keyed_write_batch(table_key, batch, system_columns, write_budget) .await?; ensure_proven_insert_blobs_are_materialized(&batch, table_key)?; @@ -3165,11 +3173,18 @@ impl TableStore { type_key: &str, batch: RecordBatch, system_columns: SystemColumns, + write_budget: WriteBudget, ) -> Result { let external_uris = collect_external_blob_uris(&batch)?; let preflight = self.preflight_external_blob_uris(&external_uris).await?; - self.prepare_keyed_write_batch_with_preflight(type_key, batch, &preflight, system_columns) - .await + self.prepare_keyed_write_batch_with_preflight( + type_key, + batch, + &preflight, + system_columns, + write_budget, + ) + .await } /// Operation-wide sibling of [`Self::prepare_keyed_write_batch`]. The @@ -3181,42 +3196,23 @@ impl TableStore { batch: RecordBatch, preflight: &ExternalBlobPreflight, system_columns: SystemColumns, + write_budget: WriteBudget, ) -> Result { self.validate_keyed_write_batch(table_key, &batch, system_columns)?; let external_uris = collect_external_blob_uris(&batch)?; - let payload_bytes = preflight.materialized_payload_bytes(&external_uris)?; - if payload_bytes > KEYED_WRITE_MAX_BYTES { - return Err(OmniError::resource_limit( - "materialized external blob payload bytes", - KEYED_WRITE_MAX_BYTES, - payload_bytes, - )); - } - let retained_bytes = u64::try_from(batch.get_array_memory_size()).map_err(|_| { - OmniError::manifest_internal("keyed write input batch bytes exceed u64") - })?; - let predicted_bytes = retained_bytes.checked_add(payload_bytes).ok_or_else(|| { + let external_bytes = preflight.materialized_payload_bytes(&external_uris)?; + write_budget.check("materialized external blob payload bytes", external_bytes)?; + let usage = write_batch_bytes(&batch)?; + let payload_bytes = usage.payload.checked_add(external_bytes).ok_or_else(|| { OmniError::manifest_internal("materialized keyed Blob byte count overflow") })?; - if predicted_bytes > KEYED_WRITE_MAX_BYTES { - return Err(OmniError::resource_limit( - format!("keyed write bytes for {table_key}"), - KEYED_WRITE_MAX_BYTES, - predicted_bytes, - )); - } + write_budget.check(MATERIALIZED_BLOB_PAYLOAD_BYTES, payload_bytes)?; + write_budget.check(format!("keyed write bytes for {table_key}"), usage.rows)?; let batch = - materialize_external_blob_inputs(batch, KEYED_WRITE_MAX_BYTES, preflight).await?; - let materialized_bytes = u64::try_from(batch.get_array_memory_size()).map_err(|_| { - OmniError::manifest_internal("materialized keyed write batch bytes exceed u64") - })?; - if materialized_bytes > KEYED_WRITE_MAX_BYTES { - return Err(OmniError::resource_limit( - format!("keyed write bytes for {table_key}"), - KEYED_WRITE_MAX_BYTES, - materialized_bytes, - )); - } + materialize_external_blob_inputs(batch, write_budget.bytes(), preflight).await?; + let usage = write_batch_bytes(&batch)?; + write_budget.check(format!("keyed write bytes for {table_key}"), usage.rows)?; + write_budget.check(MATERIALIZED_BLOB_PAYLOAD_BYTES, usage.payload)?; Ok(batch) } @@ -3400,6 +3396,7 @@ impl TableStore { interval: &ProvenInsertInterval, external_preflight: &ExternalBlobPreflight, system_columns: SystemColumns, + write_budget: WriteBudget, ) -> Result { interval .validate( @@ -3421,7 +3418,7 @@ impl TableStore { Self::scan_stream_with(source, None, None, None, false, move |scanner| { selected.select(scanner); scanner.batch_size(KEYED_WRITE_MAX_ROWS); - scanner.batch_size_bytes(KEYED_WRITE_MAX_BYTES); + scanner.batch_size_bytes(write_budget.bytes()); Ok(()) }) .await? @@ -3431,6 +3428,7 @@ impl TableStore { output_schema, raw, type_key.to_string(), + write_budget, )); } @@ -3453,7 +3451,7 @@ impl TableStore { self.clone(), external_preflight.clone(), ), - |(mut raw, mut current, mut offset, source, store, external_preflight)| async move { + move |(mut raw, mut current, mut offset, source, store, external_preflight)| async move { loop { if let Some(batch) = current.as_ref() && offset < batch.num_rows() @@ -3485,7 +3483,7 @@ impl TableStore { source.schema(), descriptors, &[row_id], - Some(KEYED_WRITE_MAX_BYTES), + Some(write_budget.bytes()), Some(&external_preflight), None, ) @@ -3515,6 +3513,7 @@ impl TableStore { output_schema, materialized, type_key.to_string(), + write_budget, )) } @@ -4990,7 +4989,8 @@ impl TableStore { // columns now so an already-overwide match fails before // `take_rows` performs the second full-row read. let non_blob_bytes = non_blob_column_bytes(committed_ds, &batch)?; - let payload_budget = account.remaining_bytes_after(non_blob_bytes)?; + account.remaining_bytes_after(non_blob_bytes)?; + let payload_budget = account.write_budget.bytes() - account.payload_bytes; if let Some(schema) = &carried_without_blobs { let carried_rows = predicate_rows_as(schema, &batch)?; @@ -5036,16 +5036,17 @@ impl TableStore { ) .unwrap_or(error)); } - Err(OmniError::ResourceLimitExceeded { actual, .. }) => { - let actual = account - .bytes_with(non_blob_bytes)? - .checked_add(actual) - .ok_or_else(|| { - OmniError::manifest_internal("pending scan byte count overflow") - })?; + Err(OmniError::ResourceLimitExceeded { + ref resource, + actual, + .. + }) if resource == MATERIALIZED_BLOB_PAYLOAD_BYTES => { + let actual = account.payload_bytes.checked_add(actual).ok_or_else(|| { + OmniError::manifest_internal("pending scan byte count overflow") + })?; return Err(OmniError::resource_limit( - "retained keyed batch bytes per operation", - KEYED_WRITE_MAX_BYTES, + MATERIALIZED_BLOB_PAYLOAD_BYTES, + account.write_budget.bytes(), actual, )); } @@ -5322,6 +5323,8 @@ struct PendingScanAccount { table_key: String, rows: u64, bytes: u64, + payload_bytes: u64, + write_budget: WriteBudget, } impl PendingScanAccount { @@ -5330,8 +5333,13 @@ impl PendingScanAccount { table_key: budget.table_key, rows: 0, bytes: 0, + payload_bytes: budget.initial.payload_bytes, + write_budget: budget.write_budget, }; - account.add_usage(budget.initial_rows, budget.initial_bytes)?; + account + .write_budget + .check(MATERIALIZED_BLOB_PAYLOAD_BYTES, account.payload_bytes)?; + account.add_usage(budget.initial.rows, budget.initial.bytes)?; Ok(account) } @@ -5349,9 +5357,16 @@ impl PendingScanAccount { fn add_batch(&mut self, batch: &RecordBatch) -> Result<()> { let rows = u64::try_from(batch.num_rows()) .map_err(|_| OmniError::manifest_internal("pending scan row count exceeds u64"))?; - let bytes = u64::try_from(batch.get_array_memory_size()) - .map_err(|_| OmniError::manifest_internal("pending scan bytes exceed u64"))?; - self.add_usage(rows, bytes) + let usage = write_batch_bytes(batch)?; + let payload_bytes = self + .payload_bytes + .checked_add(usage.payload) + .ok_or_else(|| OmniError::manifest_internal("pending scan Blob payload overflow"))?; + self.write_budget + .check(MATERIALIZED_BLOB_PAYLOAD_BYTES, payload_bytes)?; + self.add_usage(rows, usage.rows)?; + self.payload_bytes = payload_bytes; + Ok(()) } fn add_usage(&mut self, rows: u64, bytes: u64) -> Result<()> { @@ -5370,10 +5385,10 @@ impl PendingScanAccount { .bytes .checked_add(bytes) .ok_or_else(|| OmniError::manifest_internal("pending scan byte count overflow"))?; - if next_bytes > KEYED_WRITE_MAX_BYTES { + if next_bytes > self.write_budget.bytes() { return Err(OmniError::resource_limit( "retained keyed batch bytes per operation", - KEYED_WRITE_MAX_BYTES, + self.write_budget.bytes(), next_bytes, )); } @@ -5407,14 +5422,14 @@ impl PendingScanAccount { fn remaining_bytes_after(&self, bytes: u64) -> Result { let actual = self.bytes_with(bytes)?; - if actual > KEYED_WRITE_MAX_BYTES { + if actual > self.write_budget.bytes() { return Err(OmniError::resource_limit( "retained keyed batch bytes per operation", - KEYED_WRITE_MAX_BYTES, + self.write_budget.bytes(), actual, )); } - Ok(KEYED_WRITE_MAX_BYTES - actual) + Ok(self.write_budget.bytes() - actual) } fn next_scan_rows(&self) -> usize { @@ -5425,7 +5440,7 @@ impl PendingScanAccount { } fn next_scan_bytes(&self) -> u64 { - KEYED_WRITE_MAX_BYTES.saturating_sub(self.bytes).max(1) + self.write_budget.bytes().saturating_sub(self.bytes).max(1) } } @@ -5599,6 +5614,56 @@ fn combine_committed_with_staged(ds: &Dataset, staged: &[StagedWrite]) -> Vec Result { + let mut bytes = WriteBatchBytes { + rows: u64::try_from(batch.get_array_memory_size()) + .map_err(|_| OmniError::manifest_internal("write batch bytes exceed u64"))?, + payload: 0, + }; + for (field, column) in batch.schema().fields().iter().zip(batch.columns()) { + let lance_field = + lance::datatypes::Field::try_from(field.as_ref()).map_err(OmniError::lance_internal)?; + if !lance_field.is_blob() { + continue; + } + let Some(descriptions) = column.as_any().downcast_ref::() else { + return Err(OmniError::manifest_internal( + "Blob write column is not a struct", + )); + }; + if descriptions.column_by_name("data").is_none() { + continue; + } + let input = logical_blob_input(descriptions, field.name())?; + let capacity = u64::try_from(input.data.values().capacity()) + .map_err(|_| OmniError::manifest_internal("Blob buffer capacity exceeds u64"))?; + bytes.rows = bytes.rows.checked_sub(capacity).ok_or_else(|| { + OmniError::manifest_internal("Blob buffer exceeds its Arrow memory charge") + })?; + for row in 0..descriptions.len() { + if descriptions.is_valid(row) && input.data.is_valid(row) { + let length = u64::try_from(input.data.value_length(row)) + .map_err(|_| OmniError::manifest_internal("Blob value length exceeds u64"))?; + bytes.payload = bytes.payload.checked_add(length).ok_or_else(|| { + OmniError::manifest_internal("Blob payload byte count overflow") + })?; + } + } + } + Ok(bytes) +} + struct LogicalBlobInput<'a> { data: &'a LargeBinaryArray, uris: &'a StringArray, @@ -6310,6 +6375,7 @@ fn bounded_proven_insert_stream( schema: SchemaRef, raw: SendableRecordBatchStream, table_key: String, + write_budget: WriteBudget, ) -> SendableRecordBatchStream { let output_schema = schema.clone(); let bounded = futures::stream::try_unfold( @@ -6321,10 +6387,11 @@ fn bounded_proven_insert_stream( Vec::::new(), 0_usize, 0_u64, + 0_u64, schema, table_key, ), - |( + move |( mut raw, mut current, mut current_offset, @@ -6332,28 +6399,29 @@ fn bounded_proven_insert_stream( mut accumulated, mut accumulated_rows, mut accumulated_bytes, + mut accumulated_payload_bytes, schema, table_key, )| async move { loop { if let Some((batch, consumed_rows)) = pending.take() { let batch_rows = batch.num_rows(); - let batch_bytes = - u64::try_from(batch.get_array_memory_size()).map_err(|_| { - OmniError::manifest_internal( - "proven insert delta batch bytes exceed u64", - ) - .into_datafusion_external() - })?; + let usage = + write_batch_bytes(&batch).map_err(OmniError::into_datafusion_external)?; + let batch_bytes = usage.rows; let fits = accumulated_rows .checked_add(batch_rows) .is_some_and(|rows| rows <= KEYED_WRITE_MAX_ROWS) && accumulated_bytes .checked_add(batch_bytes) - .is_some_and(|bytes| bytes <= KEYED_WRITE_MAX_BYTES); + .is_some_and(|bytes| bytes <= write_budget.bytes()) + && accumulated_payload_bytes + .checked_add(usage.payload) + .is_some_and(|bytes| bytes <= write_budget.bytes()); if accumulated.is_empty() || fits { accumulated_rows += batch_rows; accumulated_bytes += batch_bytes; + accumulated_payload_bytes += usage.payload; accumulated.push(batch); current_offset += consumed_rows; if current @@ -6364,12 +6432,14 @@ fn bounded_proven_insert_stream( current_offset = 0; } if accumulated_rows == KEYED_WRITE_MAX_ROWS - || accumulated_bytes == KEYED_WRITE_MAX_BYTES + || accumulated_bytes == write_budget.bytes() + || accumulated_payload_bytes == write_budget.bytes() { let output = finish_proven_insert_batch( &schema, std::mem::take(&mut accumulated), &table_key, + write_budget, ) .map_err(OmniError::into_datafusion_external)?; return Ok(Some(( @@ -6382,6 +6452,7 @@ fn bounded_proven_insert_stream( Vec::new(), 0, 0, + 0, schema, table_key, ), @@ -6395,6 +6466,7 @@ fn bounded_proven_insert_stream( &schema, std::mem::take(&mut accumulated), &table_key, + write_budget, ) .map_err(OmniError::into_datafusion_external)?; return Ok(Some(( @@ -6407,6 +6479,7 @@ fn bounded_proven_insert_stream( Vec::new(), 0, 0, + 0, schema, table_key, ), @@ -6416,8 +6489,13 @@ fn bounded_proven_insert_stream( if let Some(batch) = current.as_ref() { if current_offset < batch.num_rows() { pending = Some( - next_proven_insert_batch(batch, current_offset, &table_key) - .map_err(OmniError::into_datafusion_external)?, + next_proven_insert_batch( + batch, + current_offset, + &table_key, + write_budget, + ) + .map_err(OmniError::into_datafusion_external)?, ); continue; } @@ -6438,6 +6516,7 @@ fn bounded_proven_insert_stream( &schema, std::mem::take(&mut accumulated), &table_key, + write_budget, ) .map_err(OmniError::into_datafusion_external)?; return Ok(Some(( @@ -6450,6 +6529,7 @@ fn bounded_proven_insert_stream( Vec::new(), 0, 0, + 0, schema, table_key, ), @@ -6484,6 +6564,7 @@ fn next_proven_insert_batch( batch: &RecordBatch, offset: usize, table_key: &str, + write_budget: WriteBudget, ) -> Result<(RecordBatch, usize)> { if offset >= batch.num_rows() { return Err(OmniError::manifest_internal(format!( @@ -6493,7 +6574,8 @@ fn next_proven_insert_batch( } let max_rows = (batch.num_rows() - offset).min(KEYED_WRITE_MAX_ROWS); - let (mut rows, mut logical_bytes) = largest_proven_insert_prefix(batch, offset, max_rows)?; + let (mut rows, mut logical_bytes) = + largest_proven_insert_prefix(batch, offset, max_rows, write_budget)?; loop { let is_whole_batch = offset == 0 && rows == batch.num_rows(); let retained_bytes = u64::try_from(batch.get_array_memory_size()).unwrap_or(u64::MAX); @@ -6503,35 +6585,37 @@ fn next_proven_insert_batch( let retained_parent_slop = 64_u64 * 1024; let keeps_only_logical_slice = retained_bytes <= logical_bytes.saturating_add(retained_parent_slop); + let retained_usage = write_batch_bytes(batch)?; let candidate = if is_whole_batch - && retained_bytes <= KEYED_WRITE_MAX_BYTES + && retained_usage.rows <= write_budget.bytes() + && retained_usage.payload <= write_budget.bytes() + && retained_bytes <= write_budget.bytes().saturating_mul(2) && keeps_only_logical_slice { batch.clone() } else { copy_proven_insert_batch_range(batch, offset, rows)? }; - let bytes = u64::try_from(candidate.get_array_memory_size()).map_err(|_| { - OmniError::manifest_internal("proven insert delta batch bytes exceed u64") - })?; - if bytes <= KEYED_WRITE_MAX_BYTES { - validate_proven_insert_source_batch(&candidate, table_key)?; + let usage = write_batch_bytes(&candidate)?; + let bytes = usage.rows.max(usage.payload); + if bytes <= write_budget.bytes() { + validate_proven_insert_source_batch(&candidate, table_key, write_budget)?; return Ok((candidate, rows)); } if rows == 1 { return Err(OmniError::resource_limit( format!("proven insert delta bytes for {table_key}"), - KEYED_WRITE_MAX_BYTES, + write_budget.bytes(), bytes, )); } drop(candidate); - rows = ((u128::try_from(rows).unwrap() * u128::from(KEYED_WRITE_MAX_BYTES)) + rows = ((u128::try_from(rows).unwrap() * u128::from(write_budget.bytes())) / u128::from(bytes)) .try_into() .unwrap_or(1_usize) .clamp(1, rows - 1); - logical_bytes = proven_insert_slice_memory_size(batch, offset, rows)?; + logical_bytes = batch_slice_memory_size(batch, offset, rows)?; } } @@ -6543,31 +6627,56 @@ fn largest_proven_insert_prefix( batch: &RecordBatch, offset: usize, max_rows: usize, + write_budget: WriteBudget, ) -> Result<(usize, u64)> { - let one_row_bytes = proven_insert_slice_memory_size(batch, offset, 1)?; - if max_rows == 1 || one_row_bytes > KEYED_WRITE_MAX_BYTES { - return Ok((1, one_row_bytes)); + let fits = |rows| -> Result { + let slice = batch.slice(offset, rows); + let usage = write_batch_bytes(&slice)?; + let mut row_bytes = batch_slice_memory_size(batch, offset, rows)?; + for (field, column) in slice.schema().fields().iter().zip(slice.columns()) { + let lance_field = lance::datatypes::Field::try_from(field.as_ref()) + .map_err(OmniError::lance_internal)?; + if lance_field.is_blob() + && let Some(descriptions) = column.as_any().downcast_ref::() + && descriptions.column_by_name("data").is_some() + { + let input = logical_blob_input(descriptions, field.name())?; + let offsets = input.data.value_offsets(); + let payload_buffer = u64::try_from(offsets[offsets.len() - 1] - offsets[0]) + .map_err(|_| { + OmniError::manifest_internal("logical Blob slice bytes exceed u64") + })?; + row_bytes = row_bytes.checked_sub(payload_buffer).ok_or_else(|| { + OmniError::manifest_internal("logical Blob slice exceeds row size") + })?; + } + } + Ok(row_bytes <= write_budget.bytes() && usage.payload <= write_budget.bytes()) + }; + if max_rows == 1 || !fits(1)? { + return Ok((1, batch_slice_memory_size(batch, offset, 1)?)); } - - let max_bytes = proven_insert_slice_memory_size(batch, offset, max_rows)?; - if max_bytes <= KEYED_WRITE_MAX_BYTES { - return Ok((max_rows, max_bytes)); + if fits(max_rows)? { + return Ok((max_rows, batch_slice_memory_size(batch, offset, max_rows)?)); } - let mut low = 1_usize; let mut high = max_rows - 1; while low < high { let middle = low + (high - low).div_ceil(2); - if proven_insert_slice_memory_size(batch, offset, middle)? <= KEYED_WRITE_MAX_BYTES { + if fits(middle)? { low = middle; } else { high = middle - 1; } } - Ok((low, proven_insert_slice_memory_size(batch, offset, low)?)) + Ok((low, batch_slice_memory_size(batch, offset, low)?)) } -fn proven_insert_slice_memory_size(batch: &RecordBatch, offset: usize, rows: usize) -> Result { +pub(crate) fn batch_slice_memory_size( + batch: &RecordBatch, + offset: usize, + rows: usize, +) -> Result { batch.columns().iter().try_fold(0_u64, |total, column| { let bytes = column .slice(offset, rows) @@ -6576,11 +6685,9 @@ fn proven_insert_slice_memory_size(batch: &RecordBatch, offset: usize, rows: usi .map_err(OmniError::arrow_internal)?; total .checked_add(u64::try_from(bytes).map_err(|_| { - OmniError::manifest_internal("proven insert logical slice bytes exceed u64") + OmniError::manifest_internal("logical batch slice bytes exceed u64") })?) - .ok_or_else(|| { - OmniError::manifest_internal("proven insert logical slice bytes overflow") - }) + .ok_or_else(|| OmniError::manifest_internal("logical batch slice bytes overflow")) }) } @@ -6588,13 +6695,14 @@ fn finish_proven_insert_batch( schema: &SchemaRef, mut batches: Vec, table_key: &str, + write_budget: WriteBudget, ) -> Result { let batch = if batches.len() == 1 { batches.pop().expect("length checked") } else { arrow_select::concat::concat_batches(schema, &batches).map_err(OmniError::arrow_internal)? }; - validate_proven_insert_source_batch(&batch, table_key)?; + validate_proven_insert_source_batch(&batch, table_key, write_budget)?; Ok(batch) } @@ -6614,7 +6722,11 @@ fn copy_proven_insert_batch_range( arrow_select::take::take_record_batch(batch, &indices).map_err(OmniError::arrow_internal) } -fn validate_proven_insert_source_batch(batch: &RecordBatch, table_key: &str) -> Result<()> { +fn validate_proven_insert_source_batch( + batch: &RecordBatch, + table_key: &str, + write_budget: WriteBudget, +) -> Result<()> { if batch.num_rows() > KEYED_WRITE_MAX_ROWS { return Err(OmniError::resource_limit( format!("proven insert delta entities for {table_key}"), @@ -6622,15 +6734,12 @@ fn validate_proven_insert_source_batch(batch: &RecordBatch, table_key: &str) -> batch.num_rows() as u64, )); } - let bytes = u64::try_from(batch.get_array_memory_size()) - .map_err(|_| OmniError::manifest_internal("proven insert delta batch bytes exceed u64"))?; - if bytes > KEYED_WRITE_MAX_BYTES { - return Err(OmniError::resource_limit( - format!("proven insert delta bytes for {table_key}"), - KEYED_WRITE_MAX_BYTES, - bytes, - )); - } + let usage = write_batch_bytes(batch)?; + write_budget.check( + format!("proven insert delta bytes for {table_key}"), + usage.rows, + )?; + write_budget.check(MATERIALIZED_BLOB_PAYLOAD_BYTES, usage.payload)?; Ok(()) } diff --git a/crates/omnigraph/src/table_store/staged_tests.rs b/crates/omnigraph/src/table_store/staged_tests.rs index 3c6715c50..33ecfe4b1 100644 --- a/crates/omnigraph/src/table_store/staged_tests.rs +++ b/crates/omnigraph/src/table_store/staged_tests.rs @@ -24,7 +24,7 @@ use crate::error::{OmniError, StorageFailureKind}; use crate::instrumentation::{MergeWriteProbes, with_merge_write_probes}; use crate::storage_layer::{ IndexBuildSpec, KEYED_WRITE_MAX_BYTES, KEYED_WRITE_MAX_ROWS, KeyedWriteSemantics, - PendingScanBudget, ProvenInsertChunk, SnapshotHandle, + PendingScanBudget, PendingUsage, ProvenInsertChunk, SnapshotHandle, WriteBudget, }; use crate::table_store::{StagedWrite, TableStore}; use arrow_array::{Array, Int32Array, RecordBatch, StringArray, StructArray, UInt64Array}; @@ -188,6 +188,74 @@ fn blob_person_pk_batch(id: &str, payload: &[u8]) -> RecordBatch { .unwrap() } +#[test] +fn write_max_bytes_separates_only_typed_valid_blob_payloads() { + let batch = blob_person_pk_batch("one", &vec![0; 4093]); + let usage = super::write_batch_bytes(&batch).unwrap(); + assert_eq!(usage.payload, 4093); + assert!(usage.rows < 4093); + assert!(batch.get_array_memory_size() as u64 > 4093); + let content = batch + .column(1) + .as_any() + .downcast_ref::() + .unwrap(); + let null_content = StructArray::new( + content.fields().clone(), + content.columns().to_vec(), + Some(datafusion::arrow::buffer::NullBuffer::from(vec![false])), + ); + let null_batch = RecordBatch::try_new( + batch.schema(), + vec![batch.column(0).clone(), Arc::new(null_content)], + ) + .unwrap(); + assert_eq!(super::write_batch_bytes(&null_batch).unwrap().payload, 0); + let ordinary = RecordBatch::try_new( + Arc::new(Schema::new(vec![Field::new( + "bytes", + DataType::LargeBinary, + false, + )])), + vec![Arc::new(arrow_array::LargeBinaryArray::from(vec![ + vec![0; 4094].as_slice(), + ]))], + ) + .unwrap(); + let usage = super::write_batch_bytes(&ordinary).unwrap(); + assert_eq!(usage.payload, 0); + assert!( + usage.rows > 4093, + "ordinary binary bytes stay in the row account" + ); + let mut builder = lance::blob::BlobArrayBuilder::new(3); + for _ in 0..3 { + builder.push_bytes(vec![0; 2730]).unwrap(); + } + let content = builder.finish().unwrap(); + let data = content + .as_any() + .downcast_ref::() + .unwrap() + .column_by_name("data") + .unwrap() + .as_any() + .downcast_ref::() + .unwrap(); + assert!( + data.values().capacity() > 8190, + "fixture must retain payload capacity slack" + ); + let batch = RecordBatch::try_new( + Arc::new(Schema::new(vec![lance::blob::blob_field("content", true)])), + vec![content], + ) + .unwrap(); + let usage = super::write_batch_bytes(&batch).unwrap(); + assert_eq!(usage.payload, 8190); + assert!(usage.rows < 8190); +} + fn staged_key_filter( staged: &StagedWrite, ) -> &lance::dataset::write::merge_insert::inserted_rows::KeyExistenceFilter { @@ -257,15 +325,22 @@ fn collect_age_for_id(batches: &[RecordBatch], needle: &str) -> Option { fn pending_scan_budget_caps_are_inclusive_and_one_over_is_typed() { PendingScanAccount::new(PendingScanBudget::new( "test:people", - KEYED_WRITE_MAX_ROWS as u64, - KEYED_WRITE_MAX_BYTES, + PendingUsage { + rows: KEYED_WRITE_MAX_ROWS as u64, + bytes: KEYED_WRITE_MAX_BYTES, + payload_bytes: 0, + }, + WriteBudget::default(), )) .expect("the exact keyed row/byte limits are inclusive"); let row_error = PendingScanAccount::new(PendingScanBudget::new( "test:people", - KEYED_WRITE_MAX_ROWS as u64 + 1, - 0, + PendingUsage { + rows: KEYED_WRITE_MAX_ROWS as u64 + 1, + ..PendingUsage::default() + }, + WriteBudget::default(), )) .err() .expect("one row over must be rejected"); @@ -280,8 +355,11 @@ fn pending_scan_budget_caps_are_inclusive_and_one_over_is_typed() { let byte_error = PendingScanAccount::new(PendingScanBudget::new( "test:people", - 0, - KEYED_WRITE_MAX_BYTES + 1, + PendingUsage { + bytes: KEYED_WRITE_MAX_BYTES + 1, + ..PendingUsage::default() + }, + WriteBudget::default(), )) .err() .expect("one byte over must be rejected"); @@ -423,6 +501,7 @@ async fn keyed_upsert_forces_filter_route_and_preserves_conflict_metadata() { person_pk_batch(&[("alice", Some(31)), ("bob", Some(25))]), KeyedWriteSemantics::Upsert, SYSTEM_COLUMNS_LEGACY, + WriteBudget::default(), ) .await .unwrap(); @@ -475,6 +554,7 @@ async fn known_present_update_is_update_only_and_fails_closed_on_missing_ids() { person_pk_batch(&[("alice", Some(99))]), KeyedWriteSemantics::KnownPresentUpdate, SYSTEM_COLUMNS_LEGACY, + WriteBudget::default(), ), ) .await @@ -505,6 +585,7 @@ async fn known_present_update_is_update_only_and_fails_closed_on_missing_ids() { person_pk_batch(&[("bob", Some(25))]), KeyedWriteSemantics::KnownPresentUpdate, SYSTEM_COLUMNS_LEGACY, + WriteBudget::default(), ) .await .unwrap_err(); @@ -528,6 +609,7 @@ async fn known_present_update_is_update_only_and_fails_closed_on_missing_ids() { person_pk_batch(&[("alice", Some(31))]), KeyedWriteSemantics::KnownPresentUpdate, SYSTEM_COLUMNS_LEGACY, + WriteBudget::default(), ) .await .unwrap(); @@ -561,6 +643,7 @@ async fn all_new_upsert_certifies_insert_absence_and_persists_it_in_history() { person_pk_batch(&[("bob", Some(25))]), KeyedWriteSemantics::Upsert, SYSTEM_COLUMNS_LEGACY, + WriteBudget::default(), ) .await .unwrap(); @@ -660,6 +743,7 @@ async fn keyed_upsert_stamps_no_by_source_delete_marker_and_persists_it() { person_pk_batch(&[("alice", Some(31))]), KeyedWriteSemantics::Upsert, SYSTEM_COLUMNS_LEGACY, + WriteBudget::default(), ) .await .unwrap(); @@ -718,6 +802,7 @@ async fn keyed_strict_insert_preflights_typed_conflict_without_changing_mode() { person_pk_batch(&[("alice", Some(99))]), KeyedWriteSemantics::StrictInsert, SYSTEM_COLUMNS_LEGACY, + WriteBudget::default(), ) .await .unwrap_err(); @@ -745,6 +830,7 @@ async fn keyed_strict_insert_preflights_typed_conflict_without_changing_mode() { person_pk_batch(&[("bob", Some(25))]), KeyedWriteSemantics::StrictInsert, SYSTEM_COLUMNS_LEGACY, + WriteBudget::default(), ), ) .await @@ -836,6 +922,7 @@ async fn proven_strict_insert_pins_update_shape_and_leaves_new_fragments_unindex ) .unwrap(), SYSTEM_COLUMNS_LEGACY, + WriteBudget::default(), ) .await .unwrap(); @@ -952,6 +1039,7 @@ async fn concurrent_proven_strict_inserts_of_same_key_land_exactly_one_effect() ) .unwrap(), SYSTEM_COLUMNS_LEGACY, + WriteBudget::default(), ) .await .unwrap(); @@ -966,6 +1054,7 @@ async fn concurrent_proven_strict_inserts_of_same_key_land_exactly_one_effect() ) .unwrap(), SYSTEM_COLUMNS_LEGACY, + WriteBudget::default(), ) .await .unwrap(); @@ -1012,6 +1101,7 @@ async fn proven_insert_chunk_rejects_target_version_reuse_before_staging() { person_pk_batch(&[("carol", Some(40))]), KeyedWriteSemantics::StrictInsert, SYSTEM_COLUMNS_LEGACY, + WriteBudget::default(), ) .await .unwrap(); @@ -1022,7 +1112,12 @@ async fn proven_insert_chunk_rejects_target_version_reuse_before_staging() { let version_before_rejection = advanced.version().version; let error = store - .stage_proven_strict_insert(advanced, chunk, SYSTEM_COLUMNS_LEGACY) + .stage_proven_strict_insert( + advanced, + chunk, + SYSTEM_COLUMNS_LEGACY, + WriteBudget::default(), + ) .await .unwrap_err(); assert!( @@ -1075,7 +1170,7 @@ async fn proven_insert_rejects_prepared_blob_descriptors_before_staging() { let version_before_rejection = target.version().version; let error = store - .stage_proven_strict_insert(target, chunk, SYSTEM_COLUMNS_LEGACY) + .stage_proven_strict_insert(target, chunk, SYSTEM_COLUMNS_LEGACY, WriteBudget::default()) .await .unwrap_err(); assert!( @@ -1118,6 +1213,7 @@ async fn proven_and_general_strict_same_key_conflict_in_both_commit_orders() { ) .unwrap(), SYSTEM_COLUMNS_LEGACY, + WriteBudget::default(), ) .await .unwrap(); @@ -1128,6 +1224,7 @@ async fn proven_and_general_strict_same_key_conflict_in_both_commit_orders() { person_pk_batch(&[("bob", Some(26))]), KeyedWriteSemantics::StrictInsert, SYSTEM_COLUMNS_LEGACY, + WriteBudget::default(), ) .await .unwrap(); @@ -1233,6 +1330,7 @@ async fn keyed_write_rejects_missing_or_non_id_primary_key() { person_batch(&[("bob", Some(25))]), KeyedWriteSemantics::Upsert, SYSTEM_COLUMNS_LEGACY, + WriteBudget::default(), ) .await .unwrap_err(); @@ -1265,6 +1363,7 @@ async fn keyed_write_rejects_missing_or_non_id_primary_key() { wrong_batch, KeyedWriteSemantics::Upsert, SYSTEM_COLUMNS_LEGACY, + WriteBudget::default(), ) .await .unwrap_err(); @@ -1287,6 +1386,39 @@ fn proven_insert_delta_scan_never_enables_strict_batch_size() { ); } +#[tokio::test] +async fn write_max_bytes_proven_insert_compacts_a_single_retained_row() { + let schema = person_pk_schema(); + let parent = RecordBatch::try_new( + schema.clone(), + vec![ + Arc::new(StringArray::from(vec![ + "small".to_string(), + "x".repeat(5000), + ])), + Arc::new(Int32Array::from(vec![Some(1), Some(2)])), + ], + ) + .unwrap(); + let slice = parent.slice(0, 1); + let settings = omnigraph_compiler::settings::SessionSettings::default() + .with("write_max_bytes", "4093") + .unwrap(); + let budget = WriteBudget::from_settings(&settings); + let retained = slice.get_array_memory_size() as u64; + assert!(retained > budget.bytes() && retained <= 2 * budget.bytes()); + let reader = arrow_array::RecordBatchIterator::new([Ok(slice)], schema.clone()); + let raw = lance_datafusion::utils::reader_to_stream(Box::new(reader)); + let output: Vec<_> = + super::bounded_proven_insert_stream(schema, raw, "Person".to_string(), budget) + .try_collect() + .await + .unwrap(); + assert_eq!(output.len(), 1); + assert_eq!(output[0].num_rows(), 1); + assert!(super::write_batch_bytes(&output[0]).unwrap().rows <= budget.bytes()); +} + #[tokio::test] async fn proven_insert_boundary_normalizer_coalesces_safe_small_batches() { let schema = person_pk_schema(); @@ -1305,11 +1437,15 @@ async fn proven_insert_boundary_normalizer_coalesces_safe_small_batches() { .collect::>(); let reader = arrow_array::RecordBatchIterator::new(batches, schema.clone()); let raw = lance_datafusion::utils::reader_to_stream(Box::new(reader)); - let output: Vec = - super::bounded_proven_insert_stream(schema, raw, "Person".to_string()) - .try_collect() - .await - .unwrap(); + let output: Vec = super::bounded_proven_insert_stream( + schema, + raw, + "Person".to_string(), + WriteBudget::default(), + ) + .try_collect() + .await + .unwrap(); assert_eq!(output.len(), 1); assert_eq!(output[0].num_rows(), 10); assert!(u64::try_from(output[0].get_array_memory_size()).unwrap() <= KEYED_WRITE_MAX_BYTES); @@ -1346,7 +1482,12 @@ async fn proven_insert_boundary_normalizer_splits_retained_parent_lazily() { let reader = arrow_array::RecordBatchIterator::new([Ok(parent)], schema.clone()); let raw = lance_datafusion::utils::reader_to_stream(Box::new(reader)); - let mut output = super::bounded_proven_insert_stream(schema, raw, "Person".to_string()); + let mut output = super::bounded_proven_insert_stream( + schema, + raw, + "Person".to_string(), + WriteBudget::default(), + ); for _ in 0..3 { let batch = output @@ -1452,7 +1593,11 @@ async fn scan_with_pending_rejects_key_column_missing_from_projection() { Some(&["note"]), None, Some("id"), - PendingScanBudget::new("test:people", 0, 0), + PendingScanBudget::new( + "test:people", + PendingUsage::default(), + WriteBudget::default(), + ), ) .await .expect_err("scan_with_pending must reject merge-shadow with missing key in projection"); @@ -1474,7 +1619,14 @@ async fn scan_with_pending_rejects_key_column_missing_from_projection() { Some(&["id", "note"]), None, Some("id"), - PendingScanBudget::new("test:people", 8190, 0), + PendingScanBudget::new( + "test:people", + PendingUsage { + rows: 8190, + ..PendingUsage::default() + }, + WriteBudget::default(), + ), ) .await .expect("projection containing key_column must succeed"); @@ -1495,7 +1647,14 @@ async fn scan_with_pending_rejects_key_column_missing_from_projection() { Some(&["id", "note"]), None, Some("id"), - PendingScanBudget::new("test:people", 8191, 0), + PendingScanBudget::new( + "test:people", + PendingUsage { + rows: 8191, + ..PendingUsage::default() + }, + WriteBudget::default(), + ), ) .await .expect_err("pending + unshadowed committed output must share the row budget"); diff --git a/crates/omnigraph/tests/consistency.rs b/crates/omnigraph/tests/consistency.rs index 132cec25a..667c4de52 100644 --- a/crates/omnigraph/tests/consistency.rs +++ b/crates/omnigraph/tests/consistency.rs @@ -242,6 +242,68 @@ async fn load_keyed_write_row_cap_excludes_strict_overwrite() { assert_eq!(count_rows(&over, "node:Thing").await, LIMIT + 1); } +#[tokio::test] +async fn write_max_bytes_load_boundaries_are_independent_and_atomic() { + use base64::Engine as _; + use omnigraph::settings::{SettingId, SettingValue, Source}; + for strict in [false, true] { + for mode in [LoadMode::Append, LoadMode::Merge] { + let dir = tempfile::tempdir().unwrap(); + let mut db = helpers::session( + Omnigraph::init( + dir.path().to_str().unwrap(), + "node Doc { slug: String @key content: Blob? note: String? }", + ) + .await + .unwrap(), + ); + db.set( + SettingId::WriteMaxBytes, + &SettingValue::Integer(4093), + Source::File, + ) + .unwrap(); + let row = |slug: &str, bytes: usize| { + serde_json::json!({ + "type":"Doc", "data":{"slug":slug,"note":"scalar","content":format!("base64:{}", + base64::engine::general_purpose::STANDARD.encode(vec![0; bytes]))} + }) + .to_string() + }; + let exact = row("exact", 4093); + if strict { + db.load_graph_batch("main", &exact, mode).await.unwrap(); + } else { + db.load_jsonl(&exact, mode).await.unwrap(); + } + let before = snapshot_main(&db).await.unwrap().graph_manifest_version(); + let wide = + serde_json::json!({"type":"Doc","data":{"slug":"wide","note":"x".repeat(4094)}}) + .to_string(); + for input in [ + row("over", 4094), + format!("{}\n{}", row("a", 2047), row("b", 2047)), + wide, + ] { + let error = if strict { + db.load_graph_batch("main", &input, mode).await.unwrap_err() + } else { + db.load_jsonl(&input, mode).await.unwrap_err() + }; + assert!( + matches!(error, OmniError::ResourceLimitExceeded { limit: 4093, .. }), + "strict={strict}, mode={mode:?}: {error:?}" + ); + assert_eq!( + snapshot_main(&db).await.unwrap().graph_manifest_version(), + before + ); + assert_eq!(count_rows(&db, "node:Doc").await, 1); + } + } + } +} + /// The sibling 32 MiB cap is measured from the staged Arrow batch, not JSON /// syntax. A single wide keyed row must be rejected with a typed limit before /// either Lance HEAD or graph visibility can move. diff --git a/crates/omnigraph/tests/merge_fast_forward.rs b/crates/omnigraph/tests/merge_fast_forward.rs index f18ba9d99..580f4de2b 100644 --- a/crates/omnigraph/tests/merge_fast_forward.rs +++ b/crates/omnigraph/tests/merge_fast_forward.rs @@ -1423,6 +1423,115 @@ query insert_doc($title: String, $content: Blob, $note: String) { } "#; +/// A diverged table requires payload materialization and planned write chunks. +/// Both lineage implementations must use the same setting through publication. +#[tokio::test] +async fn write_max_bytes_merge_keeps_independent_payload_and_chunk_allowances() { + use base64::Engine as _; + use omnigraph::settings::{SettingId, SettingValue, Source}; + for lineage in ["off", "on", "verify"] { + let dir = tempfile::tempdir().unwrap(); + let uri = dir.path().to_str().unwrap(); + let mut main = helpers::session(Omnigraph::init(uri, BLOB_SCHEMA).await.unwrap()); + main.branch_create("feature").await.unwrap(); + let payload = format!( + "base64:{}", + base64::engine::general_purpose::STANDARD.encode(vec![0; 8191]) + ); + let mut source_rows = vec![ + serde_json::json!({"type":"Document","data":{ + "title":"payload", "content":payload, "note":"scalar" + }}) + .to_string(), + ]; + for index in 0..3 { + source_rows.push( + serde_json::json!({"type":"Document","data":{ + "title":format!("source-{index}"), "note":"x".repeat(6000) + }}) + .to_string(), + ); + } + main.load("feature", &source_rows.join("\n"), LoadMode::Append) + .await + .unwrap(); + main.load( + "main", + r#"{"type":"Document","data":{"title":"target","note":"target"}}"#, + LoadMode::Append, + ) + .await + .unwrap(); + main.set( + SettingId::MergeLineage, + &SettingValue::Ident(lineage.to_string()), + Source::File, + ) + .unwrap(); + main.set( + SettingId::WriteMaxBytes, + &SettingValue::Integer(8190), + Source::File, + ) + .unwrap(); + let before = snapshot_main(&main).await.unwrap().graph_manifest_version(); + let probes = MergeWriteProbes::default(); + let error = with_merge_write_probes(probes.clone(), main.branch_merge("feature", "main")) + .await + .unwrap_err(); + assert!( + matches!( + error, + OmniError::ResourceLimitExceeded { + limit: 8190, + actual: 8191, + .. + } + ), + "{lineage}: {error:?}" + ); + assert_eq!( + snapshot_main(&main).await.unwrap().graph_manifest_version(), + before + ); + assert_eq!( + probes.stage_fenced_insert_calls() + + probes.stage_merge_insert_calls() + + probes.stage_known_present_update_calls(), + 0 + ); + main.set( + SettingId::WriteMaxBytes, + &SettingValue::Integer(8191), + Source::File, + ) + .unwrap(); + let probes = MergeWriteProbes::default(); + let result = with_merge_write_probes(probes.clone(), main.branch_merge("feature", "main")) + .await + .expect("exact payload and independently bounded rows must publish"); + assert_eq!(result.outcome, MergeOutcome::Merged); + assert_eq!(count_rows(&main, "node:Document").await, 5); + assert!( + probes.stage_fenced_insert_calls() + + probes.stage_merge_insert_calls() + + probes.stage_known_present_update_calls() + > 1, + "fixture must publish multiple bounded chunks" + ); + assert_eq!( + read_managed_blob_bytes( + &main, + ReadTarget::branch("main"), + node_blob_cell("Document", "payload", "content") + ) + .await + .len(), + 8191 + ); + } +} + /// A proven pure-insert fast-forward on a Blob table is a pointer switch onto /// main: no general ordered diff, no Blob payload read, and the managed bytes /// of both rows read back through main. diff --git a/crates/omnigraph/tests/writes.rs b/crates/omnigraph/tests/writes.rs index 515d1c95e..f077ccedd 100644 --- a/crates/omnigraph/tests/writes.rs +++ b/crates/omnigraph/tests/writes.rs @@ -891,24 +891,17 @@ edge LinkZ: Zed -> Zed { n: I32? } ); } -/// 24 MiB of external payload plus 10 MiB of retained rows fit per type, not together. -/// Rust, not GQT: MiB-scale inputs and the payload-read probe are outside the case format. +/// The read probe distinguishes payload admission from a late staging refusal. #[tokio::test] -async fn external_blob_bytes_join_the_operation_allowance_before_any_payload_read() { - const SCHEMA: &str = "\ -node Document { title: String @key content: Blob? note: String? } -node Image { title: String @key content: Blob? note: String? } -"; - +async fn write_max_bytes_checks_inline_and_repeated_external_payloads_before_reading() { + const SCHEMA: &str = "node Document { title: String @key content: Blob? note: String? }\nnode Image { title: String @key content: Blob? }"; let dir = tempfile::tempdir().unwrap(); let external_path = dir.path().join("payload.blob"); - let file = std::fs::File::create(&external_path).unwrap(); - file.set_len(12 * 1024 * 1024).unwrap(); - drop(file); + std::fs::write(&external_path, vec![0; 2048]).unwrap(); let external_uri = format!("file://{}", external_path.display()); let policy = ExternalBlobPolicy::allow(vec![ ExternalBlobBase::new( - url::Url::from_directory_path(dir.path()).expect("external blob base is absolute"), + url::Url::from_directory_path(dir.path()).unwrap(), ExternalBlobExecutionScope::EmbeddedOnly, ) .unwrap(), @@ -923,131 +916,78 @@ node Image { title: String @key content: Blob? note: String? } .with_external_blob_policy(policy) .unwrap(), ); - let before_manifest = snapshot_main(&db).await.unwrap().graph_manifest_version(); + let before = snapshot_main(&db).await.unwrap().graph_manifest_version(); let files = files_under(&graph_path); - - let note = "x".repeat(5 * 1024 * 1024); let stage_probes = StageWriteProbes::rendezvous(1); let read_probes = omnigraph::instrumentation::MergeWriteProbes::default(); + let source = r#"set write_max_bytes = 8191; + query mixed($uri: String, $inline: Blob) { + insert Document { title: "one", content: $uri } + insert Document { title: "two", content: $uri } + insert Image { title: "three", content: $inline } + }"#; + use base64::Engine as _; + let inline = format!( + "base64:{}", + base64::engine::general_purpose::STANDARD.encode(vec![0; 4096]) + ); let error = with_stage_write_probes( stage_probes.clone(), omnigraph::instrumentation::with_merge_write_probes( read_probes.clone(), db.mutate( "main", - r#"query wide($uri: String, $note: String) { - insert Document { title: "one", content: $uri, note: $note } - insert Image { title: "two", content: $uri, note: $note } - }"#, - "wide", - ¶ms(&[("$uri", &external_uri), ("$note", ¬e)]), + source, + "mixed", + ¶ms(&[("$uri", &external_uri), ("$inline", &inline)]), ), ), ) .await - .expect_err("external payloads and retained rows must share one operation allowance"); + .unwrap_err(); assert!( - operation_refusal(&error, KEYED_BATCH_BYTES), - "unexpected refusal: {error:?}" + matches!( + error, + OmniError::ResourceLimitExceeded { + limit: 8191, + actual: 8192, + .. + } + ), + "{error:?}" ); assert_eq!( read_probes.blob_payload_read_calls(), 0, - "aggregate admission must precede every external payload read" + "inline and external admission precedes GET" ); - assert_eq!(read_probes.blob_managed_batch_read_calls(), 0); assert_eq!(stage_probes.entered(), 0); assert_eq!(files_under(&graph_path), files); assert_eq!( snapshot_main(&db).await.unwrap().graph_manifest_version(), - before_manifest + before ); - let accepted = db - .mutate( - "main", - r#"query small($uri: String) { - insert Document { title: "one", content: $uri } - }"#, - "small", - ¶ms(&[("$uri", &external_uri)]), - ) - .await - .expect("one 12 MiB external payload fits after the refusal"); - assert_eq!(accepted.affected_nodes, 1); -} -/// Three equal payloads leave a LargeBinary buffer at 4/3 of its content: predicted -/// 24.4 MiB, materialized 32.5 MiB. Rust, not GQT: MiB-scale inputs, builder capacity. -#[tokio::test] -async fn materialized_blob_batches_are_rechecked_against_the_operation_allowance_before_staging() { - const SCHEMA: &str = "\ -node Document { title: String @key content: Blob? } -node Image { title: String @key content: Blob? } -"; - - let dir = tempfile::tempdir().unwrap(); - let external_path = dir.path().join("payload.blob"); - let file = std::fs::File::create(&external_path).unwrap(); - file.set_len(4 * 1024 * 1024 + 64 * 1024).unwrap(); - drop(file); - let external_uri = format!("file://{}", external_path.display()); - let policy = ExternalBlobPolicy::allow(vec![ - ExternalBlobBase::new( - url::Url::from_directory_path(dir.path()).expect("external blob base is absolute"), - ExternalBlobExecutionScope::EmbeddedOnly, - ) - .unwrap(), - ]) - .unwrap(); - let graph_dir = tempfile::tempdir().unwrap(); - let graph_path = graph_dir.path().join("graph"); - let db = helpers::session( - Omnigraph::init(graph_path.to_str().unwrap(), SCHEMA) - .await - .unwrap() - .with_external_blob_policy(policy) - .unwrap(), + let inline = format!( + "base64:{}", + base64::engine::general_purpose::STANDARD.encode(vec![0; 4095]) ); - let before_manifest = snapshot_main(&db).await.unwrap().graph_manifest_version(); - let files = files_under(&graph_path); - - let stage_probes = StageWriteProbes::rendezvous(1); - let read_probes = omnigraph::instrumentation::MergeWriteProbes::default(); - let error = with_stage_write_probes( - stage_probes.clone(), - omnigraph::instrumentation::with_merge_write_probes( - read_probes.clone(), - db.mutate( - "main", - r#"query thrice($uri: String) { - insert Document { title: "a", content: $uri } - insert Document { title: "b", content: $uri } - insert Document { title: "c", content: $uri } - insert Image { title: "a", content: $uri } - insert Image { title: "b", content: $uri } - insert Image { title: "c", content: $uri } - }"#, - "thrice", - ¶ms(&[("$uri", &external_uri)]), - ), + let accepted = omnigraph::instrumentation::with_merge_write_probes( + read_probes.clone(), + db.mutate( + "main", + source, + "mixed", + ¶ms(&[("$uri", &external_uri), ("$inline", &inline)]), ), ) .await - .expect_err("materialized batches must be summed across tables before staging"); - assert!( - operation_refusal(&error, KEYED_BATCH_BYTES), - "unexpected refusal: {error:?}" - ); + .expect("8191 payload bytes plus independent scalar rows fit"); + assert_eq!(accepted.affected_nodes, 3); assert_eq!( read_probes.blob_payload_read_calls(), - 2, - "the pre-read estimate admits this operation; only the re-check after reading refuses" - ); - assert_eq!(stage_probes.entered(), 0); - assert_eq!(files_under(&graph_path), files); - assert_eq!( - snapshot_main(&db).await.unwrap().graph_manifest_version(), - before_manifest + 1, + "fetch caching preserves repeated-reference charging" ); } @@ -1223,7 +1163,7 @@ query update_note($note: String) { ref resource, limit: LIMIT, actual, - } if resource == "retained keyed batch bytes per operation" && actual > LIMIT + } if resource == "materialized blob payload bytes" && actual > LIMIT ), "oversized update blob must be rejected before payload read, got {error:?}" ); diff --git a/docs/dev/blob.md b/docs/dev/blob.md index a4dc4d4f1..1fd0a3442 100644 --- a/docs/dev/blob.md +++ b/docs/dev/blob.md @@ -52,7 +52,7 @@ New external references are denied by default. A configured policy permits only A base may not overlap OmniGraph storage. Ingress reads with the process's storage principal, so a base over a graph or cluster root would let a writer copy manifest, table, or ledger bytes into a managed cell that Cedar then serves as graph data. Three checks hold the rule, each against the roots it knows: cluster validation compares every base with the cluster storage root (`external_blob_base_overlaps_storage_root`), serve boot quarantines a graph whose applied server-safe base overlaps that root, and `Omnigraph::with_external_blob_policy` refuses a base overlapping the handle's own graph root. The comparison is by URI component in either direction, using the lexical and canonical forms of both sides (`ExternalBlobBase::ensure_disjoint_from_storage_root`). The root is classified by the storage layer's rule (`omnigraph_storage::storage_kind_for_uri`), so an absolute local path containing `://` is a local root, and a local root is made absolute by the storage adapter's own lexical fold (`omnigraph_storage::absolutize_lexically`), so `/link/../cluster` is compared at `/cluster`, where storage writes. A base whose scheme cannot name the root's storage kind (any base against an `az://` or in-memory root, an `s3://` base against a local root, a `file://` base against an S3 root) is disjoint without the root being rendered. A same-kind root that has no base-shaped form (an empty or percent-sign path component, an ancestor that fails to resolve for a reason other than absence) and a root in an unrecognized scheme fail closed as `StorageRootConflict::UncomparableRoot`, published as `external_blob_storage_root_uncomparable`; `StorageRootConflict::Overlap` is `external_blob_base_overlaps_storage_root`, and a base or policy that fails its own validation is `StorageRootConflict::InvalidPolicy`, never either code. Serve boot applies the quarantine only to a graph entry whose composite digest binds its policy; any other entry fails boot with `external_blob_policy_digest_mismatch`. -Admission is done after last-write-wins coalescing, so superseded input cannot trigger target I/O. Equivalent normalized references are probed and read once per bounded operation where materialization is required. URI metadata, selected reference count, and carried payload bytes all participate in pre-effect limits (`KEYED_WRITE_MAX_BYTES`, `KEYED_WRITE_MAX_ROWS`, `EXTERNAL_BLOB_URI_MAX_BYTES`); the user-facing values and reported resources are listed once in [Blob limits](../user/blobs.md#limits). +Admission is done after last-write-wins coalescing, so superseded external references cannot trigger target I/O. Equivalent normalized references are probed and read once per bounded operation where materialization is required, while every reference counts its payload length. An immutable `WriteBudget` carries the effective `write_max_bytes` for independent row-data and payload admission. URI metadata, selected reference count and per-URI length retain their fixed pre-effect limits; the user-facing values and reported resources are listed once in [Blob limits](../user/blobs.md#limits). Storage mode determines ownership: diff --git a/docs/dev/execution.md b/docs/dev/execution.md index 6d74cdfc1..15660c098 100644 --- a/docs/dev/execution.md +++ b/docs/dev/execution.md @@ -750,21 +750,23 @@ All load modes share the mutation publisher and recovery protocol: | `Append` | Strict insert by exact physical `id`; an existing ID is a typed conflict. The public mode name does not mean a bare Lance Append transaction. | | `Merge` | Upsert by exact physical `id`; the last input occurrence wins. | -Mutation insert/update and keyed Load retain the per-table limits of 8,192 rows -and 32 MiB, plus one 32 MiB sum of retained Arrow batches across touched tables. -The sum uses `get_array_memory_size`, preserving conservative shared-buffer -counting. Keyed parsing separately caps its decoded-payload estimate across -types at 32 MiB before retaining each row. External Blob copy admission includes -the retained keyed batches plus copied payload estimates before payload reads; -materialized batches are checked again before fragment staging. +Mutation insert/update and keyed Load retain the per-table limit of 8,192 rows. +An immutable `WriteBudget` captures the session's `write_max_bytes` (default +32 MiB) once, including retries. Retained row data has one allowance per table +and one across tables; logical Blob payloads have a separate allowance. +The row account uses Arrow memory size minus typed logical Blob payload buffer +capacity, retaining descriptor and shared-buffer accounting. Keyed parsing +checks row estimates and decoded payload bytes independently. Inline and +copied external payloads are admitted together before payload reads, then +materialized batches are checked again before staging. Mutation delete, cascading delete and Overwrite replacement removal scan IDs -incrementally under one 32 MiB allowance per operation, charging UTF-8 bytes -plus one `String` slot before copying an ID. These checks precede this -operation's data fragments and publication. They do not bound JSON containers, -simultaneous conversion copies, predicate/validation state or native scan -buffers. Overwrite's bulk input retains its existing separate checks and is not -subject to the keyed row limit. See [writes.md](writes.md#keyed-writes). +incrementally under their own `write_max_bytes` allowance per operation, +charging UTF-8 bytes plus one `String` slot before copying. These checks precede +the operation's data fragments and publication. They do not bound JSON +containers, simultaneous conversion copies, predicate/validation state or +native scan buffers. Overwrite's bulk input retains its separate checks and is +not subject to keyed aggregate row limits. See [writes.md](writes.md#keyed-writes). `load_graph_batch_as` is the strict graph-level NDJSON boundary. Each nonblank line is one logical node or edge envelope; duplicate members, physical fields, diff --git a/docs/dev/ingestion.md b/docs/dev/ingestion.md index 87eb815b3..e8dbd8497 100644 --- a/docs/dev/ingestion.md +++ b/docs/dev/ingestion.md @@ -25,7 +25,7 @@ The optional top-level `id` supplies entity identity. `data` holds user properti The strict parser rejects duplicate members, unknown fields, reserved physical columns, malformed envelopes, and noncanonical supplied entity IDs. It accepts logical type and property names only; callers never select physical datasets, lanes, fragments, or bindings. -The HTTP handler authorizes both change and any requested branch creation before polling the body. It verifies `Content-Type`, rejects an oversized `Content-Length` early, and otherwise collects at most 32 MiB. The parser also bounds individual lines and per-table retained rows/Arrow bytes before durable effects. Keyed modes add one 32 MiB sum of the parse estimate and one 32 MiB sum of retained Arrow batches across all touched tables. +The HTTP handler authorizes both change and any requested branch creation before polling the body. It verifies `Content-Type`, rejects an oversized `Content-Length` early, and otherwise collects at most 32 MiB. The parser also bounds individual lines and per-table retained rows/Arrow bytes before durable effects. Keyed modes bound parser estimates and retained row data across tables with the session's `write_max_bytes` (default 32 MiB). Logical Blob payloads have their own allowance of the same size. ### Compatibility loader @@ -55,7 +55,7 @@ Load modes are shared across the surfaces: One batch may touch several logical declarations. Cross-table visibility is still atomic: readers see all published table versions or none. An error after recovery is armed returns recovery-required rather than claiming rollback or success. -The write envelope is bounded before arm. Keyed work is limited to 8,192 retained rows and 32 MiB of exact retained Arrow data per table, with operation-wide limits for carried external Blob payloads and other retained plans. The retained Arrow batches of all touched tables together are limited to 32 MiB, and so is the keyed parse estimate. An Overwrite load collects the committed IDs absent from its replacement under a separate 32 MiB removed-ID allowance, each ID charged its UTF-8 length plus 24 bytes; see [writes.md](writes.md#keyed-writes). The HTTP strict-batch body has its own 32 MiB ceiling. These limits bound one commit; clients split larger imports into explicit batches and accept one graph commit per batch. +The write envelope is bounded before arm. Keyed work is limited to 8,192 retained rows per table. The session's `write_max_bytes` (default 32 MiB) independently bounds retained row data, logical Blob payloads and parsing estimates, preserving the existing table and operation scopes. An Overwrite load collects committed IDs absent from its replacement under a separate removed-ID allowance of the same size; each ID is charged its UTF-8 length plus 24 bytes. Overwrite keeps its bulk-input behavior and is exempt from keyed aggregate row/batch limits. See [writes.md](writes.md#keyed-writes). The HTTP strict-batch body has its own fixed 32 MiB ceiling. Clients split larger incremental imports into explicit batches and accept one graph commit per batch. ## Blob and index behavior diff --git a/docs/dev/merge.md b/docs/dev/merge.md index c4d05074d..8518c7417 100644 --- a/docs/dev/merge.md +++ b/docs/dev/merge.md @@ -87,6 +87,10 @@ filter-bearing Update shape. It performs no target MergeInsert join or target ID preflight. The marker is not a signature and raw Lance writers remain unsupported. RFC 0023 owns the detailed proof and performance evidence. +The effective `write_max_bytes` is captured once. It accompanies the exact +planned chunk rows through staging and publication, so scanner batch choices +cannot change the admitted transaction boundaries. + ## General route The `merge_lineage` session setting at `on` (the release default, `verify` in @@ -116,8 +120,9 @@ the same cursor (`crates/omnigraph/src/ordered_cursor.rs`): row-width shape — the width estimate that plans chunk sizes only reduces retries, it is never load-bearing for the bound. A single indivisible row wider than the ceiling still hydrates alone; -- Blob columns hydrate as descriptors; Blob-bearing rows are materialized - under the same operation budget; +- Blob columns hydrate as descriptors; materialized Blob payloads share one + operation-wide `write_max_bytes` allowance (default 32 MiB), independent of + the row-and-descriptor allowance used for each write chunk; - selected new IDs stage as strict inserts, existing IDs as known-present updates, and removals as deletes; - the transaction plan is pre-minted and bounded before recovery arm; diff --git a/docs/dev/writes.md b/docs/dev/writes.md index 91b45bc50..34c1a4241 100644 --- a/docs/dev/writes.md +++ b/docs/dev/writes.md @@ -364,28 +364,30 @@ sealed, exact-`id`, filter-bearing MergeInsert adapter: existing ID; - upsert updates or inserts without changing modes on retry; - a bare Lance Append is not a production graph-table write; -- one table's keyed input is bounded to 8,192 rows and 32 MiB before its - data is staged. +- one table's keyed input is bounded to 8,192 rows and the effective + `write_max_bytes` (default 32 MiB) before its data is staged. Insert/update mutations and keyed Append/Merge loads also cap the sum of -retained Arrow batches across tables at 32 MiB -(`retained keyed batch bytes per operation`). Admission uses -`get_array_memory_size` accounting; shared buffers may be conservatively counted -more than once. An update's pending-aware scan charges the same sum under the -same resource name. The keyed parse spool separately caps its decoded-payload -estimate across tables at 32 MiB (`keyed parsed entity bytes per operation`). -External Blob copy admission adds copied payload estimates, which are not yet -read, to the retained keyed batches under the same resource name before reading -payloads, then checks materialized batches before staging fragments. +retained row data across tables at `write_max_bytes` +(`retained keyed batch bytes per operation`). Admission starts with +`get_array_memory_size`, subtracting only typed logical Blob payload buffer +capacity. Offsets, validity, URI descriptors and ordinary columns remain in +the row account; shared buffers may be counted more than once. Logical Blob +payload lengths have an independent allowance of the same size. Payload +accounts count logical payload lengths; builder over-capacity (up to about one +third) is outside them. Pending-aware +update scans charge both accounts, including their initial pending input. +The keyed parse spool separately caps its row estimate and decoded Blob bytes. +Inline and copied external payload lengths are admitted together before GETs; +materialized row and payload accounts are checked again before staging. Delete mutations, cascades and Overwrite's removed-ID detection stream matches -instead of collecting the full scan. One 32 MiB -`retained removed-id bytes per operation` allowance covers all tables, charging -each ID's UTF-8 length plus one 24-byte `String` slot before copying it. -Overwrite's bulk input is not subject to the keyed row/batch limits. These are -fixed representation limits with no setting, not a -combined allocator/RSS budget; native scan buffers, conversion copies and -validation's derived state are outside them. Refusal precedes the current +under one `write_max_bytes` removed-ID allowance per operation, charging each +ID's UTF-8 length plus one 24-byte `String` slot before copying it. +Overwrite's bulk input is not subject to the keyed aggregate row/batch limits. +These are representation limits, not a combined allocator/RSS budget; native +scan buffers, conversion copies and validation's derived state are outside them. +Refusal precedes the current operation's fragment staging and publication, though writable open may have completed earlier schema work and a load may already have created its branch. @@ -446,7 +448,7 @@ payload ranges before reading bytes or staging any effect. Overwrite can preserve an allowed external descriptor through Lance `WriteParams`. Keyed writes and row-writing merge paths materialize selected -external bytes under the operation's 32 MiB budget because Lance's MergeInsert +external bytes under the operation's `write_max_bytes` Blob payload budget because Lance's MergeInsert surface has no equivalent reference-preservation hook. A pointer-only branch adoption does no source I/O. An update never reads the Blobs it assigns; a carried stored external reference the policy refuses fails as diff --git a/docs/user/blobs.md b/docs/user/blobs.md index e58f6ca0c..3b9bc4bac 100644 --- a/docs/user/blobs.md +++ b/docs/user/blobs.md @@ -150,16 +150,28 @@ and retry. | Limit | Applies to | Reported resource | |---|---|---| -| 32 MiB of decoded `base64:` bytes | Each node or edge type in one load, in every mode, including `overwrite` | `decoded blob input bytes for ` | -| 32 MiB of decoded `base64:` bytes | One `base64:` value, in a load or in an insert or update mutation | `decoded blob input bytes` | -| 32 MiB per touched type, and 32 MiB across all touched types, Blob bytes included | Incremental writes: `append` and `merge` loads, inserts and updates. External bytes copied in and Blob values carried unchanged by an update count | `keyed write bytes for
`, `keyed entity bytes for
`, `retained keyed batch bytes per operation` | -| 32 MiB of external payload copied into managed storage | One incremental write operation across all its types, and each type within it: two types copying 20 MiB each exceed it although each fits its per-type limit | `materialized external blob payload bytes` | -| 32 MiB of Blob payload | One branch merge that writes rows, across all types, managed and external bytes together | `materialized blob payload bytes` | +| `write_max_bytes` decoded bytes | Each node or edge type in one load, in every mode, including `overwrite` | `decoded blob input bytes for
` | +| `write_max_bytes` decoded bytes | One `base64:` value in an insert or update mutation; a statement's values add up across its Blob properties and an update's matched rows. A load reports its decoded bytes under the `for
` and `per operation` names | `decoded blob input bytes per operation` | +| `write_max_bytes` row bytes per type and across all touched types | Incremental writes: `append` and `merge` loads, inserts and updates. Counts ordinary columns, Blob descriptors and Arrow bookkeeping; excludes logical Blob payload buffers | `keyed write bytes for
`, `keyed entity bytes for
`, `retained keyed batch bytes per operation`, `keyed parsed entity bytes for
`, `keyed parsed entity bytes per operation` | +| `write_max_bytes` logical Blob payload bytes | One incremental write across all types: inline payloads, copied external payloads and Blob values carried by updates count together | `materialized blob payload bytes`, `decoded blob input bytes per operation` | +| `write_max_bytes` Blob payload bytes | One branch merge that writes rows, across all types, managed and external bytes together | `materialized blob payload bytes` | | 8,192 external references | One write operation or merge | `external Blob reference cells` | | 32 MiB of retained URI metadata | One write operation or merge. Every copy of a URI the operation keeps counts, plus 24 bytes per copy: admission keeps each reference's text twice and each distinct object's normalized URI twice, so distinct URIs reach the limit at about 8 MiB of text | `external Blob URI metadata bytes` | | 64 KiB | One external URI | `external Blob URI bytes` | | 4 MiB | One embedded managed range read | `Blob read range bytes` | +Superseded versions of a key written in one operation count as input +(`decoded blob input bytes per operation`) until the last-write-wins fold. +An update of a row inserted earlier in the same mutation is such a version: +it carries the pending row's Blob, so that payload counts twice, the carried +copy as `materialized blob payload bytes`. + +[`write_max_bytes`](queries/settings.md) defaults to 32 MiB and accepts +`1..=33554432`. Payload and row allowances are independent: a payload exactly +at the limit fits when its row and descriptors also fit. Repeated external +references each count their payload length even when one fetch serves them. +External-only admission can also report `materialized external blob payload bytes`. + `
` names the type as `node:` or `edge:`, for example `keyed entity bytes for node:Document`. diff --git a/docs/user/mutations/index.md b/docs/user/mutations/index.md index ead28817c..88b5a40d1 100644 --- a/docs/user/mutations/index.md +++ b/docs/user/mutations/index.md @@ -148,24 +148,26 @@ Loads preserve supplied embeddings and do not generate them. Results report ## Limits and conflicts Insert/update mutations and incremental keyed loads are bounded to 8,192 -entities and 32 MiB per touched type, plus 32 MiB of retained Arrow batches -across all touched types in one operation. Keyed loads also have a separate -32 MiB parsed-payload estimate across types. External Blob payloads that require -copying count toward the aggregate allowance. Every strict load retains its -projected in-memory size check. Blob values have further limits; see -[Blob limits](../blobs.md#limits). +entities per touched type. The [`write_max_bytes`](../queries/settings.md) +setting (default 32 MiB) bounds retained row data per type and across all +touched types. Blob payloads have a separate allowance of the same size, +including inline bytes, copied external bytes and carried update values. +Every version of a key written in one mutation counts, so an insert and an +update of the same row charge its Blob twice. +Keyed loads also bound their parser estimate independently. Every strict +load retains its projected input check. See [Blob limits](../blobs.md#limits). Deletes, including cascades, and overwrite loads collecting replaced IDs have -a separate 32 MiB allowance per operation for those IDs, summed over all +a separate `write_max_bytes` allowance per operation for those IDs, summed over all touched types. Each removed ID is charged its UTF-8 length plus 24 bytes, so -the allowance holds 671,088 IDs of 26 bytes, the length of a generated ID. A +the default allowance holds 671,088 IDs of 26 bytes, the length of a generated ID. A delete and the edges it cascades to draw on the same allowance. An overwrite of entities loaded without a `@key` and without an explicit `id` removes every committed ID of that type, because those IDs are generated again on each load. Oversized work returns a resource-limit error before its data is staged or -published. These checks do not bound total engine memory, and no setting -changes them. Split larger inserts, updates, keyed loads and deletes into +published. These checks do not bound total engine memory. The setting can +lower each allowance from its 32 MiB maximum. Split larger inserts, updates, keyed loads and deletes into explicit commits. An overwrite replaces each represented type as one image and cannot be split: it keeps its bulk-input behavior and remains subject to its separate input and removed-ID checks. diff --git a/docs/user/queries/settings.md b/docs/user/queries/settings.md index 717f587c9..d18e66503 100644 --- a/docs/user/queries/settings.md +++ b/docs/user/queries/settings.md @@ -47,7 +47,7 @@ field, `set` query parameter or `set` line naming one is refused. A setting reaches the engine through one of three doors: - CLI: `--set name=value`, repeatable, on `omnigraph query`, `mutate`, - `branch merge`, `commit changes` and `changes poll`; see the + `branch merge`, `commit changes` and `changes poll`, and direct `--store` loads; see the [CLI guide](../cli/index.md#session-settings). - HTTP body: the `settings` object on `POST /query`, `POST /mutate`, `POST /mutate/if-graph-commit` and `POST /branches/merge`, one key per @@ -75,6 +75,7 @@ refuses startup; no default is substituted. | `ann_nprobes` | integer, at least `0` | `20` | request | `OMNIGRAPH_ANN_NPROBES` | the partition cap per index delta of a `nearest` scan; `0` is no cap | | `stage_write_concurrency` | integer `1..=64` | `8` | process | `OMNIGRAPH_LOAD_CONCURRENCY` | the width of the staged-write fan-out for `load` and `mutate` | | `traversal_work_limit` | integer `1..=9223372036854775807` | `1000000` | request | `OMNIGRAPH_TRAVERSAL_WORK_LIMIT` | shared traversal row-work cap for statements containing alternatives or wildcard | +| `write_max_bytes` | integer `1..=33554432` | `33554432` (32 MiB) | request | `OMNIGRAPH_WRITE_MAX_BYTES` | independent row-data and Blob-payload allowances for bounded writes; lowers the existing mutate, load and materializing branch-merge limits | | `history_release_bytes` | integer `1024..=262144` | `262144` | request | `OMNIGRAPH_HISTORY_RELEASE_BYTES` | the byte budget of a branch's buffer of unreleased commits; a mutate, load or branch merge of the session whose buffer and head reach it closes a history block, and the publish after it writes the block under `__history` (schema apply, branch create and delete, repair and upgrade publish under the production budget); a load reaches it only through the CLI or an embedded `Session`, since the HTTP load routes take no settings; query results never change, only the number of requests and files; production keeps the default, a caller may only lower it | For these statements the cap covers all traversals, including named and nested @@ -99,6 +100,23 @@ Internal traversal pins used by the GQT harness can force indexed execution. A forced CSR pin is refused for statements with selections; there is no public `set traversal` setting. +`write_max_bytes` applies to each existing write account: row data and Blob +descriptors, logical Blob payloads, parser estimates and retained removed IDs. +The accounts keep their existing per-table, per-operation or per-chunk scope. +Row data and Blob payloads have separate allowances: a payload exactly at the +limit can accompany a row whose metadata fits its own allowance. The value is +captured once for an operation, including its retries and merge publication. +A one-row keyed write costs about 1 to 3 KiB of row data from Arrow buffer +rounding, so allowances below a few KiB refuse every write. + +The maximum is inclusive. A lower environment value is a process default, +so a request may still select any valid value up to `33554432`. Embedded +sessions and direct `--store` CLI loads can override it. HTTP load routes +inherit the process default and accept no request setting; remote CLI loads +refuse `--set`. HTTP bodies, Blob reads, compaction and external URI metadata +retain their independent limits. Lower values can produce smaller merge +chunks or reject a write that the default admits. + `history_release_bytes` is measured against the branch's buffered commit and table-change rows as stored, plus the head whole; the row layout is in [storage versioning](../../dev/versioning.md#current-storage-contract). The @@ -122,7 +140,7 @@ settings line's refusal as `ERROR line , column : `. set merge_lineage = fast; error: unknown value `fast` for setting `merge_lineage`; expected one of off, on, verify set traversal = csr; (likewise reset traversal; and show traversal;) -error: unknown setting `traversal`; expected one of engine, rrf_plan, merge_lineage, ann_nprobes, stage_write_concurrency, traversal_work_limit, history_release_bytes +error: unknown setting `traversal`; expected one of engine, rrf_plan, merge_lineage, ann_nprobes, stage_write_concurrency, traversal_work_limit, write_max_bytes, history_release_bytes set ann_nprobes = "many"; error: setting `ann_nprobes` takes an integer of at least 0, got a string set stage_write_concurrency = 0; diff --git a/openapi.json b/openapi.json index 254770dc4..2c25fc06e 100644 --- a/openapi.json +++ b/openapi.json @@ -9404,6 +9404,12 @@ "description": "the finite work budget shared by edge selections in one query; exhaustion terminates the query with an error", "maximum": 9223372036854775807, "minimum": 1 + }, + "write_max_bytes": { + "type": "integer", + "description": "the byte allowance for bounded writes by mutate, load and branch merge; entity data and materialized Blob payloads each have this independent allowance; lowering it preserves the existing per-type, per-operation and per-chunk scopes", + "maximum": 33554432, + "minimum": 1 } }, "additionalProperties": false