diff --git a/changelog.d/query-scans-stream-and-limits-hydrate.fixed.md b/changelog.d/query-scans-stream-and-limits-hydrate.fixed.md new file mode 100644 index 000000000..449fb4877 --- /dev/null +++ b/changelog.d/query-scans-stream-and-limits-hydrate.fixed.md @@ -0,0 +1,11 @@ +- A read that projects a wide column no longer fails with `resource limit + exceeded for query_memory_bytes` once the type's projected columns exceed the + 150 MiB query pool, even when it returns one row: every unranked node scan now + streams bounded batches instead of holding the whole type, so an unordered + `limit` stops reading after its first batches and a top-k holds only its kept + rows. Under a `limit` over a type with more than four rows per kept row, the + columns only the output reads are fetched by row address for the kept rows + alone; `explain` shows a `HydrateColumns` node and the + `late_materialization` pass. See [execution][hydrate-execution]. + +[hydrate-execution]: ../docs/dev/execution.md diff --git a/crates/omnigraph-gqt-core/src/plan.rs b/crates/omnigraph-gqt-core/src/plan.rs index aa74fd716..41cf21b4c 100644 --- a/crates/omnigraph-gqt-core/src/plan.rs +++ b/crates/omnigraph-gqt-core/src/plan.rs @@ -21,6 +21,7 @@ //! scan Passage as $p: runtime filter text //! scan Passage as $p: no runtime filter //! filter reads [a.state, b.state] +//! hydrate $d: columns [body, title] //! pass projection_pushdown //! not pass aggregate_pushdown //! ``` @@ -40,7 +41,7 @@ use serde_json::Value; use crate::report::Row; -const FORMS: &str = "forms: `scan [ as $var]: columns [a, b]`, `scan [ as $var]: not columns [a, b]`, `scan [ as $var]: filter reads [v.a]`, `scan [ as $var]: no filter`, `scan [ as $var]: access id_lookup`, `hash join $var[ ran ]`, `scan [ as $var]: ranked [ fetch ][ nprobes ]`, `scan [ as $var]: runtime filter `, `scan [ as $var]: no runtime filter`, `contains join $h.x contains $n.y`, `cross join $h.x contains $n.y`, `expand $src $dst: mode [ ran ]`, `filter reads [a.x, b.y]`, `sort tiebreak [$a.@id, $e.@type]`, `rank fuse row tiebreak [$e.@type, $e.@id]`, `expand $a $b: selection alternation [Knows out, Likes in]`, `sort no tiebreak`, `aggregate : () -> `, `block aggregate : () -> `, `result columns [: , ...]`, `type : `, `cast : -> `, `no cast `, `pass `, `not pass `"; +const FORMS: &str = "forms: `scan [ as $var]: columns [a, b]`, `scan [ as $var]: not columns [a, b]`, `scan [ as $var]: filter reads [v.a]`, `scan [ as $var]: no filter`, `scan [ as $var]: access id_lookup`, `hash join $var[ ran ]`, `scan [ as $var]: ranked [ fetch ][ nprobes ]`, `scan [ as $var]: runtime filter `, `scan [ as $var]: no runtime filter`, `contains join $h.x contains $n.y`, `cross join $h.x contains $n.y`, `expand $src $dst: mode [ ran ]`, `filter reads [a.x, b.y]`, `sort tiebreak [$a.@id, $e.@type]`, `rank fuse row tiebreak [$e.@type, $e.@id]`, `expand $a $b: selection alternation [Knows out, Likes in]`, `sort no tiebreak`, `aggregate : () -> `, `block aggregate : () -> `, `result columns [: , ...]`, `type : `, `cast : -> `, `no cast `, `hydrate $var: columns [a, b]`, `pass `, `not pass `"; const ID_LOOKUP: &str = "id_lookup"; const JOIN_SIDES: [&str; 2] = ["hash_join", "id_lookup"]; @@ -171,6 +172,12 @@ pub(crate) enum PlanLine { Sort { tiebreak: Vec, }, + /// A physical `HydrateColumns` fetches exactly `columns` of `$binding` by + /// row address for the rows that reached the output. + Hydrate { + binding: String, + columns: Vec, + }, /// The optimizer pass `name` fired, or did not when `negated`. Pass { name: String, @@ -372,6 +379,26 @@ pub(crate) fn parse_plan_body(body: &[(usize, &str)]) -> Result, S lines.push(PlanLine::Sort { tiebreak }); continue; } + if let Some(rest) = line.strip_prefix("hydrate ") { + let (binding, claim) = rest + .split_once(':') + .ok_or_else(|| refused("claims `hydrate $var: columns [a, b]`"))?; + let binding = binding + .trim() + .strip_prefix('$') + .filter(|binding| identifier(binding)) + .ok_or_else(|| refused("names the hydrated binding with `$`"))?; + let columns = claim + .trim() + .strip_prefix("columns") + .and_then(column_list) + .ok_or_else(|| refused("claims `hydrate $var: columns [a, b]`"))?; + lines.push(PlanLine::Hydrate { + binding: binding.to_string(), + columns, + }); + continue; + } if let Some(rest) = line.strip_prefix("hash join ") { let rest = rest.trim_start(); let (binding, claim) = rest.split_once(' ').unwrap_or((rest, "")); @@ -868,6 +895,8 @@ struct PlannedNodes { sorts: Vec, String>>, fusions: Vec, String>>, selections: Vec<(String, String, Value)>, + /// `(binding, sorted columns)` of every `HydrateColumns` binding. + hydrations: Vec<(String, Vec)>, } #[derive(Debug)] @@ -1089,6 +1118,31 @@ fn planned_physical(node: &Value, out: &mut PlannedNodes) { }; let id = node.get("id").and_then(Value::as_u64); match node.get("node").and_then(Value::as_str) { + Some("HydrateColumns") => { + for binding in node + .get("bindings") + .and_then(Value::as_array) + .into_iter() + .flatten() + { + let columns = binding + .get("columns") + .and_then(Value::as_array) + .into_iter() + .flatten() + .filter_map(Value::as_str) + .map(str::to_string) + .collect(); + out.hydrations.push(( + binding + .get("binding") + .and_then(Value::as_str) + .unwrap_or_default() + .to_string(), + sorted_set(columns), + )); + } + } Some("Aggregate") => match node.get("aggregates").and_then(Value::as_array) { Some(aggregates) => { out.aggregates @@ -1470,6 +1524,21 @@ pub(crate) fn plan_mismatch( )); } } + PlanLine::Hydrate { binding, columns } => { + let want = sorted_set(columns.clone()); + let found: Vec<&Vec> = nodes + .hydrations + .iter() + .filter(|(hydrated, _)| hydrated == binding) + .map(|(_, columns)| columns) + .collect(); + if !found.contains(&&want) { + return Some(format!( + "expect plan: no `HydrateColumns` fetches {want:?} of `${binding}`; the plan hydrates {:?}", + nodes.hydrations + )); + } + } PlanLine::Sort { tiebreak } => { if nodes.sorts.is_empty() { return Some("expect plan: the physical plan has no `Sort`".to_string()); @@ -2587,6 +2656,36 @@ mod tests { } } + /// A `hydrate` line reads the physical `HydrateColumns`: the exact set + /// of one binding's hydrated columns, in any order. + #[test] + fn hydrate_claims_read_the_physical_hydration() { + let explain = json!({ + "physical_plan": {"node": "HydrateColumns", "id": 4, "bindings": [ + {"binding": "d", "table": "node:Doc", "columns": ["title", "body"]} + ], "inputs": [{"node": "Page", "id": 3, "inputs": []}]}, + "passes": ["resolve", "late_materialization"], + }); + let lines = parse_plan_body(&[(0, "hydrate $d: columns [body, title]")]).unwrap(); + assert_eq!(check(&lines, &explain), None); + for (claim, want) in [ + ("hydrate $d: columns [body]", "no `HydrateColumns` fetches"), + ("hydrate $e: columns [body, title]", "of `$e`"), + ] { + let lines = parse_plan_body(&[(0, claim)]).unwrap(); + let mismatch = check(&lines, &explain).unwrap(); + assert!(mismatch.contains(want), "{claim}: {mismatch}"); + } + for refused in [ + "hydrate d: columns [body]", + "hydrate $d columns [body]", + "hydrate $d: [body]", + "hydrate $d: columns []", + ] { + assert!(parse_plan_body(&[(0, refused)]).is_err(), "{refused}"); + } + } + /// A `contains join` line reads the physical `ContainsJoin`'s columns /// and a `runtime filter` claim the marked scan's column; the collected /// side is unmarked, and a `cross join` line the plain product's filters. diff --git a/crates/omnigraph-gqt/README.md b/crates/omnigraph-gqt/README.md index e3fd8c0ff..a0efc89ec 100644 --- a/crates/omnigraph-gqt/README.md +++ b/crates/omnigraph-gqt/README.md @@ -366,6 +366,7 @@ hash join $e expand $d Knows $e: mode indexed_scan filter reads [d.rank, e.rank] sort tiebreak [$d, $e] +hydrate $d: columns [body] pass projection_pushdown not pass aggregate_pushdown aggregate total: sum(I64?) exact_integer round_to_nearest -> F64? @@ -383,8 +384,11 @@ keys a physical `Sort` appends after user order keys. `$a` abbreviates `$a.@id`; `sort no tiebreak` requires an empty list. `rank fuse row tiebreak [...]` checks the exact downstream keys of `RankFuse`, and `rank fuse no row tiebreak` requires none. Dropping a type key or swapping key order fails these assertions. -`pass ` states that a named optimizer pass fired, `not pass ` that -it did not. Projection/read lists are sets; identity keys and selection members +`hydrate $d: columns [..]` states that a physical `HydrateColumns` fetches +exactly those columns of `$d` by row address above the root limit (pass +`late_materialization`); the `scan` lines keep reading the logical plan, whose +projection still lists them. `pass ` states that a named optimizer pass +fired, `not pass ` that it did not. Projection/read lists are sets; identity keys and selection members are ordered lists. A mismatch prints the whole explain document. `aggregate : () -> ` checks the named output's aggregate function, input type, accumulator, diff --git a/crates/omnigraph-gqt/cases/v2/planner/hydrate_return_columns_above_the_limit.gqt b/crates/omnigraph-gqt/cases/v2/planner/hydrate_return_columns_above_the_limit.gqt new file mode 100644 index 000000000..caf0bfaf5 --- /dev/null +++ b/crates/omnigraph-gqt/cases/v2/planner/hydrate_return_columns_above_the_limit.gqt @@ -0,0 +1,180 @@ +# issue: none +# notes: Twelve docs; d07 and d10 have no body. d00 and d01 both cite d10, d02 cites +# notes: d11, d03 cites d05. Pass 6 fetches a return column only the output reads by +# notes: row address above the root limit when its binding's scan estimates more than +# notes: four rows per kept row. Steps 1-3 hydrate (12 > 4 x 2; 12 > 4 x 1 for $b, +# notes: whose scan step 3's key lookup on $a does not bound). Steps 4-6 do not: a +# notes: sorted alias, 12 is not above 4 x 3, a key lookup reads one row. Step 2 +# notes: reaches $b through a hash join, step 3 by id lookup: both destination paths +# notes: carry the row address. Every rows step is compared with engine v1. + +--- runner +timeout_ms: 10000 +environments: + - target: omnigraph-engine + storage: local-filesystem + +--- schema +node Doc { + slug: String @key + rank: I32 + title: String + body: String? + embedding: Vector(2) +} + +edge Cites: Doc -> Doc + +--- seed +{"type":"Doc","data":{"slug":"d00","rank":0,"title":"t-00","embedding":[0.0,0.0],"body":"body 00"}} +{"type":"Doc","data":{"slug":"d01","rank":1,"title":"t-01","embedding":[1.0,0.0],"body":"body 01"}} +{"type":"Doc","data":{"slug":"d02","rank":2,"title":"t-02","embedding":[2.0,0.0],"body":"body 02"}} +{"type":"Doc","data":{"slug":"d03","rank":3,"title":"t-03","embedding":[3.0,0.0],"body":"body 03"}} +{"type":"Doc","data":{"slug":"d04","rank":4,"title":"t-04","embedding":[4.0,0.0],"body":"body 04"}} +{"type":"Doc","data":{"slug":"d05","rank":5,"title":"t-05","embedding":[5.0,0.0],"body":"body 05"}} +{"type":"Doc","data":{"slug":"d06","rank":6,"title":"t-06","embedding":[6.0,0.0],"body":"body 06"}} +{"type":"Doc","data":{"slug":"d07","rank":7,"title":"t-07","embedding":[7.0,0.0]}} +{"type":"Doc","data":{"slug":"d08","rank":8,"title":"t-08","embedding":[8.0,0.0],"body":"body 08"}} +{"type":"Doc","data":{"slug":"d09","rank":9,"title":"t-09","embedding":[9.0,0.0],"body":"body 09"}} +{"type":"Doc","data":{"slug":"d10","rank":10,"title":"t-10","embedding":[10.0,0.0]}} +{"type":"Doc","data":{"slug":"d11","rank":11,"title":"t-11","embedding":[11.0,0.0],"body":"body 11"}} +{"edge":"Cites","from":"d00","to":"d10","data":{}} +{"edge":"Cites","from":"d01","to":"d10","data":{}} +{"edge":"Cites","from":"d02","to":"d11","data":{}} +{"edge":"Cites","from":"d03","to":"d05","data":{}} + +--- query +query top_two() { + match { $d: Doc } + return { $d.slug, $d.body, $d.title } + order { $d.rank desc } + limit 2 +} + +--- expect ordered +{"d.body":"body 11","d.slug":"d11","d.title":"t-11"} +{"d.slug":"d10","d.title":"t-10"} + +--- expect shape +d.slug: String +d.body: String? +d.title: String + +--- expect plan +pass late_materialization +hydrate $d: columns [body, title] +scan Doc as $d: columns [__id, slug, rank, body, title] + +--- expect same as v1 + +--- query +query cited_twice() { + match { + $a: Doc + $a cites $b + } + return { $a.slug, $b.slug, $b.body, $b.embedding } + order { $a.slug } + limit 2 +} + +--- expect ordered +{"a.slug":"d00","b.embedding":[10,0],"b.slug":"d10"} +{"a.slug":"d01","b.embedding":[10,0],"b.slug":"d10"} + +--- expect shape +a.slug: String +b.slug: String +b.body: String? +b.embedding: Vector(2) + +--- expect plan +pass late_materialization +hydrate $b: columns [body, embedding] +hash join $b + +--- expect same as v1 + +--- query +query first_citation() { + match { + $a: Doc { slug: "d02" } + $a cites $b + } + return { $b.body } + limit 1 +} + +--- expect unordered +{"b.body":"body 11"} + +--- expect shape +b.body: String? + +--- expect plan +pass late_materialization +hydrate $b: columns [body] +scan Doc as $b: access id_lookup + +--- expect same as v1 + +--- query +query by_title() { + match { $d: Doc } + return { $d.slug, $d.title as t } + order { t desc } + limit 2 +} + +--- expect ordered +{"d.slug":"d11","t":"t-11"} +{"d.slug":"d10","t":"t-10"} + +--- expect shape +d.slug: String +t: String + +--- expect plan +not pass late_materialization + +--- expect same as v1 + +--- query +query lowest_three() { + match { $d: Doc } + return { $d.slug, $d.body } + order { $d.rank } + limit 3 +} + +--- expect ordered +{"d.body":"body 00","d.slug":"d00"} +{"d.body":"body 01","d.slug":"d01"} +{"d.body":"body 02","d.slug":"d02"} + +--- expect shape +d.slug: String +d.body: String? + +--- expect plan +not pass late_materialization + +--- expect same as v1 + +--- query +query one_body() { + match { $d: Doc { slug: "d03" } } + return { $d.body } + limit 1 +} + +--- expect unordered +{"d.body":"body 03"} + +--- expect shape +d.body: String? + +--- expect plan +not pass late_materialization + +--- expect same as v1 diff --git a/crates/omnigraph-gqt/cases_slow/v2/wide_column_scan_limit_answers.gqt b/crates/omnigraph-gqt/cases_slow/v2/wide_column_scan_limit_answers.gqt new file mode 100644 index 000000000..cc8d13380 --- /dev/null +++ b/crates/omnigraph-gqt/cases_slow/v2/wide_column_scan_limit_answers.gqt @@ -0,0 +1,138 @@ +# issue: none +# red_on: a9aa2850 (v0.12.0); step 3 (query) fails with `resource limit exceeded for query_memory_bytes: actual 158237376, limit 157286400`, and step 4 run alone fails with `actual 158354916`, while engine v1 (`--- expect same as v1`) returns the expected rows for both. +# notes: 5,000 Doc rows whose `body` is one 40,000-byte string (the 16 characters +# notes: 0123456789abcdef repeated 2,500 times): 200 MB of `body` decoded, more than +# notes: the default 150 MiB query budget, while each answer is one 40 KB row. +# notes: Step 1 writes them: 100 loop iterations of 50 inserts, slugs d_ with +# notes: i in 0..100 and kk in 00..49. Step 2 counts them. Step 3 pins that an +# notes: unordered `limit 1` projecting the wide column answers under the budget; +# notes: every row carries the same body, so any one row is right. Step 4 pins the +# notes: same for a top-k (`order ... desc limit 1`): its winner is d9_49, the +# notes: greatest slug in byte order (`_` sorts after the digits), written in the +# notes: tenth of the 100 commits, so a sort that stopped early would miss it. +# notes: In both, the scan streams the narrow columns and the kept row's body is fetched +# notes: by row address above the limit (pass late_materialization). +# notes: Cheap tier: `a_wide_column_read_under_a_limit_follows_its_result` in +# notes: crates/omnigraph/tests/engine_v2_memory.rs (the same shapes, 16 MiB pool). +# notes: Rename to issue__wide_column_scan_limit_answers.gqt once the issue exists. + +--- runner +timeout_ms: 60000 +environments: + - target: omnigraph-engine + storage: local-filesystem + +--- schema +node Doc { + slug: String @key + body: String? +} + +--- seed + +--- loop $i 0 100 +--- mutate +query add_docs($s00: String, $s01: String, $s02: String, $s03: String, $s04: String, $s05: String, $s06: String, $s07: String, $s08: String, $s09: String, $s10: String, $s11: String, $s12: String, $s13: String, $s14: String, $s15: String, $s16: String, $s17: String, $s18: String, $s19: String, $s20: String, $s21: String, $s22: String, $s23: String, $s24: String, $s25: String, $s26: String, $s27: String, $s28: String, $s29: String, $s30: String, $s31: String, $s32: String, $s33: String, $s34: String, $s35: String, $s36: String, $s37: String, $s38: String, $s39: String, $s40: String, $s41: String, $s42: String, $s43: String, $s44: String, $s45: String, $s46: String, $s47: String, $s48: String, $s49: String, $body: String) { + insert Doc { slug: $s00, body: $body } + insert Doc { slug: $s01, body: $body } + insert Doc { slug: $s02, body: $body } + insert Doc { slug: $s03, body: $body } + insert Doc { slug: $s04, body: $body } + insert Doc { slug: $s05, body: $body } + insert Doc { slug: $s06, body: $body } + insert Doc { slug: $s07, body: $body } + insert Doc { slug: $s08, body: $body } + insert Doc { slug: $s09, body: $body } + insert Doc { slug: $s10, body: $body } + insert Doc { slug: $s11, body: $body } + insert Doc { slug: $s12, body: $body } + insert Doc { slug: $s13, body: $body } + insert Doc { slug: $s14, body: $body } + insert Doc { slug: $s15, body: $body } + insert Doc { slug: $s16, body: $body } + insert Doc { slug: $s17, body: $body } + insert Doc { slug: $s18, body: $body } + insert Doc { slug: $s19, body: $body } + insert Doc { slug: $s20, body: $body } + insert Doc { slug: $s21, body: $body } + insert Doc { slug: $s22, body: $body } + insert Doc { slug: $s23, body: $body } + insert Doc { slug: $s24, body: $body } + insert Doc { slug: $s25, body: $body } + insert Doc { slug: $s26, body: $body } + insert Doc { slug: $s27, body: $body } + insert Doc { slug: $s28, body: $body } + insert Doc { slug: $s29, body: $body } + insert Doc { slug: $s30, body: $body } + insert Doc { slug: $s31, body: $body } + insert Doc { slug: $s32, body: $body } + insert Doc { slug: $s33, body: $body } + insert Doc { slug: $s34, body: $body } + insert Doc { slug: $s35, body: $body } + insert Doc { slug: $s36, body: $body } + insert Doc { slug: $s37, body: $body } + insert Doc { slug: $s38, body: $body } + insert Doc { slug: $s39, body: $body } + insert Doc { slug: $s40, body: $body } + insert Doc { slug: $s41, body: $body } + insert Doc { slug: $s42, body: $body } + insert Doc { slug: $s43, body: $body } + insert Doc { slug: $s44, body: $body } + insert Doc { slug: $s45, body: $body } + insert Doc { slug: $s46, body: $body } + insert Doc { slug: $s47, body: $body } + insert Doc { slug: $s48, body: $body } + insert Doc { slug: $s49, body: $body } +} +--- params +{"s00": "d${i}_00", "s01": "d${i}_01", "s02": "d${i}_02", "s03": "d${i}_03", "s04": "d${i}_04", "s05": "d${i}_05", "s06": "d${i}_06", "s07": "d${i}_07", "s08": "d${i}_08", "s09": "d${i}_09", "s10": "d${i}_10", "s11": "d${i}_11", "s12": "d${i}_12", "s13": "d${i}_13", "s14": "d${i}_14", "s15": "d${i}_15", "s16": "d${i}_16", "s17": "d${i}_17", "s18": "d${i}_18", "s19": "d${i}_19", "s20": "d${i}_20", "s21": "d${i}_21", "s22": "d${i}_22", "s23": "d${i}_23", "s24": "d${i}_24", "s25": "d${i}_25", "s26": "d${i}_26", "s27": "d${i}_27", "s28": "d${i}_28", "s29": "d${i}_29", "s30": "d${i}_30", "s31": "d${i}_31", "s32": "d${i}_32", "s33": "d${i}_33", "s34": "d${i}_34", "s35": "d${i}_35", "s36": "d${i}_36", "s37": "d${i}_37", "s38": "d${i}_38", "s39": "d${i}_39", "s40": "d${i}_40", "s41": "d${i}_41", "s42": "d${i}_42", "s43": "d${i}_43", "s44": "d${i}_44", "s45": "d${i}_45", "s46": "d${i}_46", "s47": "d${i}_47", "s48": "d${i}_48", "s49": "d${i}_49", "body": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"} +--- expect affected: nodes=50 edges=0 +--- endloop + +--- query +query doc_count() { + match { $d: Doc } + return { count($d) as docs } +} + +--- expect unordered +{"docs": 5000} + +--- expect shape +docs: I64 + +--- query +query any_body() { + match { $d: Doc } + return { $d.body } + limit 1 +} + +--- expect unordered +{"d.body": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"} + +--- expect shape +d.body: String? + +--- expect plan +pass late_materialization +hydrate $d: columns [body] + +--- query +query greatest_slug() { + match { $d: Doc } + return { $d.slug, $d.body } + order { $d.slug desc } + limit 1 +} + +--- expect ordered +{"d.slug": "d9_49", "d.body": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"} + +--- expect shape +d.slug: String +d.body: String? + +--- expect plan +pass late_materialization +hydrate $d: columns [body] diff --git a/crates/omnigraph-planner/src/cost.rs b/crates/omnigraph-planner/src/cost.rs index bfabe5d09..d3382e5aa 100644 --- a/crates/omnigraph-planner/src/cost.rs +++ b/crates/omnigraph-planner/src/cost.rs @@ -96,6 +96,20 @@ pub const HASH_JOIN_RATIO: u64 = 8; /// side may take at most one part in this many of the memory pool. pub const HASH_JOIN_POOL_DIVISOR: u64 = 4; +/// A query's return-only columns are fetched by row address for the rows a +/// root `limit` keeps once the binding's table holds more than this many +/// rows per kept row: a take reads each kept row's pages apart, where the +/// scan would read the column for every row it passes. +pub const HYDRATE_ROW_RATIO: u64 = 4; + +/// The bytes one `HydrateColumns` chunk may retain under a query pool of +/// `pool` bytes: an eighth, so the chunk, the rows it is re-attached to and +/// the batches queued beside them fit the pool together. The planner +/// declares it as the node's `retained_limit`; the operator enforces it. +pub fn hydrate_chunk_bytes(pool: u64) -> u64 { + (pool / 8).max(1) +} + /// The access path of a dependent scan. `HashJoin` needs both: the table /// holds at most `HASH_JOIN_RATIO` rows per frontier row, and the build /// side's estimated bytes are known and at most `build_bytes_budget`. diff --git a/crates/omnigraph-planner/src/lib.rs b/crates/omnigraph-planner/src/lib.rs index 46582facf..1b34d37f1 100644 --- a/crates/omnigraph-planner/src/lib.rs +++ b/crates/omnigraph-planner/src/lib.rs @@ -47,8 +47,9 @@ pub use aggregate::{ pub use bound::{BOUND_PLAN_VERSION, BoundPlan, ValueTable}; pub use cost::{ AccessPath, CSR_BUILD_FACTOR, ExpandCostInputs, ExpandMode, ExpandPolicy, - HASH_JOIN_POOL_DIVISOR, HASH_JOIN_RATIO, IndexCoverage, choose_access_path, choose_expand_mode, - direction_probe_factor, estimate_rows, executed_hops, scan_row_estimate, should_switch_to_csr, + HASH_JOIN_POOL_DIVISOR, HASH_JOIN_RATIO, HYDRATE_ROW_RATIO, IndexCoverage, choose_access_path, + choose_expand_mode, direction_probe_factor, estimate_rows, executed_hops, hydrate_chunk_bytes, + scan_row_estimate, should_switch_to_csr, }; pub use error::PlanError; pub use explain::Explain; @@ -65,9 +66,9 @@ pub use operation::{Operation, PageBudgetSpec, ScopeSpec, Side, TableRef}; pub use optimizer::{Bounds, physical_plan, rewrite}; pub use output::{NodeObjectType, validate_output_schemas}; pub use physical::{ - Assumptions, DatasetPin, Estimate, GatePolicy, Hop, NodeId, OverfetchRung, PhysicalNode, - PhysicalPlan, Prefilter, PrefilterMode, Properties, RankArm, RankKind, RankScope, RankedAccess, - ScanInput, StatisticSource, + Assumptions, DatasetPin, Estimate, GatePolicy, Hop, HydratedBinding, HydratedColumn, NodeId, + OverfetchRung, PhysicalNode, PhysicalPlan, Prefilter, PrefilterMode, Properties, + ROW_ADDRESS_PREFIX, RankArm, RankKind, RankScope, RankedAccess, ScanInput, StatisticSource, }; pub use registry::{Coverage, Entry, Route, Shape}; pub use route::RouteOverride; diff --git a/crates/omnigraph-planner/src/lower.rs b/crates/omnigraph-planner/src/lower.rs index 5a43e5161..ce292381e 100644 --- a/crates/omnigraph-planner/src/lower.rs +++ b/crates/omnigraph-planner/src/lower.rs @@ -13,7 +13,9 @@ use crate::aggregate::AggregateSpec; use crate::cost::{AccessPath, ExpandMode, ExpandPolicy}; use crate::error::PlanError; use crate::logical::{ColumnRef, KeyJoinKind, ScanSpec}; -use crate::physical::{NodeId, PhysicalNode, PhysicalPlan, RankArm, RankedAccess, ScanInput}; +use crate::physical::{ + HydratedBinding, NodeId, PhysicalNode, PhysicalPlan, RankArm, RankedAccess, ScanInput, +}; #[cfg(doc)] use crate::physical::RankKind; @@ -136,6 +138,14 @@ pub trait Lower { input: Self::Op, ) -> Result; + /// Called with the rows that reached the output lowered. + fn hydrate_columns( + &mut self, + id: NodeId, + bindings: &[HydratedBinding], + input: Self::Op, + ) -> Result; + fn row_compare(&mut self, id: NodeId, input: Self::Op) -> Result; fn classify_three_way(&mut self, id: NodeId, input: Self::Op) -> Result; @@ -371,6 +381,10 @@ impl PhysicalPlan { let input = self.lower_node(*input, l)?; l.hydrate_by_address(id, *side, input) } + PhysicalNode::HydrateColumns { input, bindings } => { + let input = self.lower_node(*input, l)?; + l.hydrate_columns(id, bindings, input) + } PhysicalNode::RowCompare { input } => { let input = self.lower_node(*input, l)?; l.row_compare(id, input) diff --git a/crates/omnigraph-planner/src/mirror.rs b/crates/omnigraph-planner/src/mirror.rs index a39b3dca6..f3da7976d 100644 --- a/crates/omnigraph-planner/src/mirror.rs +++ b/crates/omnigraph-planner/src/mirror.rs @@ -26,8 +26,8 @@ use crate::error::PlanError; use crate::logical::{ColumnRef, GqFilter, KeyJoinKind, Predicate, RuntimeFilterSpec, ScanSpec}; use crate::operation::TableRef; use crate::physical::{ - Assumptions, Estimate, Hop, NodeId, OverfetchRung, PhysicalNode, PhysicalPlan, Prefilter, - Properties, RankArm, RankKind, RankScope, RankedAccess, ScanInput, StatisticSource, + Assumptions, Estimate, Hop, HydratedBinding, NodeId, OverfetchRung, PhysicalNode, PhysicalPlan, + Prefilter, Properties, RankArm, RankKind, RankScope, RankedAccess, ScanInput, StatisticSource, }; use crate::source::SideId; use omnigraph_compiler::types::{AggSignature, ExprType}; @@ -248,6 +248,10 @@ pub enum NodeMirror { input: NodeId, side: SideId, }, + HydrateColumns { + input: NodeId, + bindings: Vec, + }, RowCompare { input: NodeId, }, @@ -399,6 +403,10 @@ impl From<&PhysicalNode> for NodeMirror { input: *input, side: *side, }, + PhysicalNode::HydrateColumns { input, bindings } => Self::HydrateColumns { + input: *input, + bindings: bindings.clone(), + }, PhysicalNode::RowCompare { input } => Self::RowCompare { input: *input }, PhysicalNode::ClassifyThreeWay { input } => Self::ClassifyThreeWay { input: *input }, PhysicalNode::Limit { input, rows } => Self::Limit { @@ -600,6 +608,9 @@ impl TryFrom for PhysicalNode { fallback, }, NodeMirror::HydrateByAddress { input, side } => Self::HydrateByAddress { input, side }, + NodeMirror::HydrateColumns { input, bindings } => { + Self::HydrateColumns { input, bindings } + } NodeMirror::RowCompare { input } => Self::RowCompare { input }, NodeMirror::ClassifyThreeWay { input } => Self::ClassifyThreeWay { input }, NodeMirror::Limit { input, rows } => Self::Limit { input, rows }, diff --git a/crates/omnigraph-planner/src/optimizer.rs b/crates/omnigraph-planner/src/optimizer.rs index 89dbf243e..460080a83 100644 --- a/crates/omnigraph-planner/src/optimizer.rs +++ b/crates/omnigraph-planner/src/optimizer.rs @@ -12,9 +12,9 @@ use omnigraph_compiler::settings::Traversal; use omnigraph_compiler::traversal::{EDGE_TYPE_COLUMN, EdgeSelection}; use crate::cost::{ - AccessPath, ExpandCostInputs, ExpandMode, ExpandPolicy, HASH_JOIN_POOL_DIVISOR, IndexCoverage, - choose_access_path, choose_expand_mode, direction_probe_factor, estimate_rows, executed_hops, - scan_row_estimate, + AccessPath, ExpandCostInputs, ExpandMode, ExpandPolicy, HASH_JOIN_POOL_DIVISOR, + HYDRATE_ROW_RATIO, IndexCoverage, choose_access_path, choose_expand_mode, + direction_probe_factor, estimate_rows, executed_hops, hydrate_chunk_bytes, scan_row_estimate, }; use crate::error::PlanError; use crate::logical::{ @@ -26,9 +26,9 @@ use crate::lower::ContainsJoinFields; use crate::operation::{Operation, Side}; use crate::output::{node_object_types, return_schema}; use crate::physical::{ - Assumptions, Estimate, Hop, NodeId, OverfetchRung, PhysicalNode, PhysicalPlan, Prefilter, - Properties, RankArm, RankKind, RankScope, RankedAccess, ScanInput, StatisticSource, - TextContains, + Assumptions, Estimate, Hop, HydratedBinding, HydratedColumn, NodeId, OverfetchRung, + PhysicalNode, PhysicalPlan, Prefilter, Properties, RankArm, RankKind, RankScope, RankedAccess, + ScanInput, StatisticSource, TextContains, }; use crate::source::{NodeTypeSpec, PlanSource, SideId}; @@ -1074,6 +1074,7 @@ pub fn physical_plan( bounds: &Bounds, mut fired: Vec<&'static str>, ) -> Result { + let query = is_query_plan(plan); let before = plan .schema(plan.root()) .cloned() @@ -1105,11 +1106,13 @@ pub fn physical_plan( join_algorithm, expand_mode, access_path, - decisions, + mut decisions, .. } = lowering; physical.set_root(root); - if late_materialization { + let late_query = + query && materialize_returns_late(&mut physical, plan, source, &mut decisions)?; + if late_materialization || late_query { fired.push(PASS_LATE_MATERIALIZATION); } let contains_join = physical @@ -1876,6 +1879,209 @@ fn predicate_reads(predicate: &Predicate, out: &mut Vec) { } } +/// Pass 6 on a query plan. A return column that nothing but the output +/// reads (a bare `$b.property`, not a key, a `Blob` or a sort alias) leaves +/// its binding's scan, which reads the binding's row address instead, and a +/// `HydrateColumns` above the root `Limit` fetches it for the rows the limit +/// kept. A scan reads (and Lance reads ahead) more rows than the limit keeps +/// whenever a sort, a filter or a traversal sits below it, so the pass +/// applies to every binding whose scan's row estimate is above +/// `HYDRATE_ROW_RATIO` rows per kept row or unknown; a key lookup reads one +/// row and keeps its columns. Plans that rank by fusion or aggregate keep +/// theirs. +fn materialize_returns_late( + physical: &mut PhysicalPlan, + logical: &LogicalPlan, + source: &dyn PlanSource, + decisions: &mut Vec, +) -> Result { + let root = physical.root(); + let Some(&PhysicalNode::Limit { input, rows: limit }) = physical.node(root) else { + return Ok(false); + }; + if limit == 0 { + return Ok(false); + } + let mut id = input; + let mut alias_keys: HashSet = HashSet::new(); + while let Some(PhysicalNode::Sort { + input, order_by, .. + }) = physical.node(id) + { + alias_keys.extend(order_by.iter().filter_map(|key| match &key.expr { + IRExpr::AliasRef(alias, _) => Some(alias.clone()), + _ => None, + })); + id = *input; + } + let Some(PhysicalNode::Projection { return_exprs, .. }) = physical.node(id) else { + return Ok(false); + }; + let return_exprs = return_exprs.clone(); + if physical.live().any(|(_, node)| { + matches!( + node, + PhysicalNode::RankFuse { .. } | PhysicalNode::Aggregate { .. } + ) + }) { + return Ok(false); + } + let Some(returned) = logical_return_projection(logical) else { + return Ok(false); + }; + + // What decides rows: every read outside the return, then the return's + // own reads that are not a bare, unsorted property. + let mut kept: HashSet<(String, String)> = HashSet::new(); + let mut whole: HashSet = HashSet::new(); + let mut keep = |read: ColumnRef| match read.property { + Some(property) => { + kept.insert((read.binding, property)); + } + None => { + whole.insert(read.binding); + } + }; + for (logical_id, node) in logical.live() { + if logical_id != returned { + node_reads(node).into_iter().for_each(&mut keep); + } + } + let mut candidates: BTreeMap> = BTreeMap::new(); + for (position, item) in return_exprs.iter().enumerate() { + let output = result_column(item); + match (&item.expr, output) { + ( + IRExpr::PropAccess { + variable, property, .. + }, + output, + ) if !alias_keys.contains(&output) => { + candidates + .entry(variable.clone()) + .or_default() + .push(HydratedColumn { + position, + output, + property: property.clone(), + }); + } + (expr, _) => { + let mut reads = Vec::new(); + reads_of_expr(expr, &mut reads); + reads.into_iter().for_each(&mut keep); + } + } + } + + let threshold = u64::try_from(limit) + .unwrap_or(u64::MAX) + .saturating_mul(HYDRATE_ROW_RATIO); + let mut bindings = Vec::new(); + let mut rewrites: Vec<(NodeId, Vec)> = Vec::new(); + for (binding, columns) in candidates { + if whole.contains(&binding) { + continue; + } + let scans: Vec = physical + .live() + .filter(|(_, node)| { + matches!(node, PhysicalNode::Scan { spec, .. } + if spec.binding.as_deref() == Some(binding.as_str())) + }) + .map(|(scan, _)| scan) + .collect(); + let [scan] = scans[..] else { + continue; + }; + let Some(PhysicalNode::Scan { + spec, + ranked: None, + keys_only: false, + .. + }) = physical.node(scan) + else { + continue; + }; + let Some(projection) = spec.projection.as_ref() else { + continue; + }; + let node_type = node_type_of(spec, source)?; + let deferrable = |property: &str| { + !kept.contains(&(binding.clone(), property.to_string())) + && !node_type.key.iter().any(|key| key == property) + && property != node_type.columns.id + && projection.iter().any(|column| column == property) + && node_type + .schema + .field_with_name(property) + .is_ok_and(|field| { + node_type.object_columns.iter().any(|name| name == property) + || matches!(field.data_type(), DataType::FixedSizeList(..)) + }) + }; + let columns: Vec = columns + .into_iter() + .filter(|column| deferrable(&column.property)) + .collect(); + if columns.is_empty() { + continue; + } + let rows = scan_row_estimate(spec, source); + decisions.push(StatisticSource { + statistic: format!("hydrate_rows({binding})"), + value: format!( + "{} rows, limit {limit}, ratio {HYDRATE_ROW_RATIO}", + rows.map_or_else(|| "unknown".to_string(), |rows| rows.to_string()) + ), + origin: "manifest row count, at most one row under a key equality", + }); + if rows.is_some_and(|rows| rows <= threshold) { + continue; + } + let mut read: Vec = projection + .iter() + .filter(|column| !columns.iter().any(|deferred| &deferred.property == *column)) + .cloned() + .collect(); + if !read.iter().any(|column| column == ROW_ADDR) { + read.push(ROW_ADDR.to_string()); + } + rewrites.push((scan, read)); + bindings.push(HydratedBinding { + binding, + table: spec.table.clone(), + columns, + }); + } + if bindings.is_empty() { + return Ok(false); + } + for (scan, read) in rewrites { + if let Some(PhysicalNode::Scan { spec, .. }) = physical.node_mut(scan) { + spec.projection = Some(read); + } + } + let hydrate = physical.add(PhysicalNode::HydrateColumns { + input: root, + bindings, + }); + physical.set_root(hydrate); + Ok(true) +} + +/// The query's return `Projection`: under the root `Limit` and `Sort`s. +fn logical_return_projection(plan: &LogicalPlan) -> Option { + let mut id = plan.root(); + loop { + match plan.node(id)? { + LogicalNode::Limit { input, .. } | LogicalNode::Sort { input, .. } => id = *input, + LogicalNode::Projection { .. } => return Some(id), + _ => return None, + } + } +} + /// Stage 2, pass 4. With an adjacency proof, the `LeftOuter` diff's child scan /// reads only the fragments the transaction wrote, filtered to the version /// window, and its parent scan only the fragments the transaction touched. @@ -3117,6 +3323,7 @@ pub fn declared_ordering(plan: &PhysicalPlan, id: NodeId) -> Option> PhysicalNode::SortMergeJoin { on, .. } => Some(vec![on.clone()]), PhysicalNode::HashJoin { probe, .. } => declared_ordering(plan, *probe), PhysicalNode::HydrateByAddress { input, .. } + | PhysicalNode::HydrateColumns { input, .. } | PhysicalNode::RowCompare { input, .. } | PhysicalNode::ClassifyThreeWay { input } | PhysicalNode::Page { input, .. } @@ -3306,6 +3513,22 @@ fn derive_properties( }], } } + PhysicalNode::HydrateColumns { input, .. } => { + let input = props(plan, *input)?; + let retained = hydrate_chunk_bytes(bounds.query_memory_pool_bytes); + Properties { + schema: input.schema.clone(), + ordering: input.ordering.clone(), + rows: input.rows, + work_bytes: Estimate::Unknown, + retained_limit: Some(retained), + sources: vec![StatisticSource { + statistic: "retained_limit".to_string(), + value: retained.to_string(), + origin: "query memory pool / 8", + }], + } + } PhysicalNode::RowCompare { input, .. } => { let input = props(plan, *input)?; Properties { diff --git a/crates/omnigraph-planner/src/physical.rs b/crates/omnigraph-planner/src/physical.rs index 8b508acbe..8abcec7b5 100644 --- a/crates/omnigraph-planner/src/physical.rs +++ b/crates/omnigraph-planner/src/physical.rs @@ -17,6 +17,7 @@ use crate::logical::{ tiebreak_text, }; use crate::mirror::EdgeSelectionMirror; +use crate::operation::TableRef; use crate::source::SideId; /// The index of a node in a [`PhysicalPlan`]. @@ -382,6 +383,29 @@ impl Properties { } } +/// The prefix of the column a query's return projection carries for a +/// `HydrateColumns` above it: `^n` holds binding `n`'s row address. No GQ +/// alias or `binding.property` name starts with it. +pub const ROW_ADDRESS_PREFIX: &str = "^"; + +/// One binding whose return columns a `HydrateColumns` fetches: its pinned +/// table and the columns, each with the return position it fills. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct HydratedBinding { + pub binding: String, + pub table: TableRef, + pub columns: Vec, +} + +/// One deferred return item: `binding.property` under `output`, the return +/// column at `position`. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct HydratedColumn { + pub position: usize, + pub output: String, + pub property: String, +} + /// The operator catalog. The change-feed and merge nodes (`Scan`, /// `SortMergeJoin`, `HydrateByAddress`, `RowCompare`, `ClassifyThreeWay`, /// `Page`) each have one push operator in the engine's `engine/push/`; the @@ -434,6 +458,15 @@ pub enum PhysicalNode { input: NodeId, side: SideId, }, + /// Pass 6 on a query: the return columns only the output reads, fetched + /// for the rows that reached it by each binding's row address (the + /// `ROW_ADDRESS_PREFIX` column the projection below carries) from the + /// binding's pinned table, in bounded chunks, and placed at their return + /// positions between the input's other columns. + HydrateColumns { + input: NodeId, + bindings: Vec, + }, RowCompare { input: NodeId, }, @@ -543,6 +576,7 @@ impl PhysicalNode { Self::MetadataCount { .. } => "MetadataCount", Self::HashJoin { .. } => "HashJoin", Self::HydrateByAddress { .. } => "HydrateByAddress", + Self::HydrateColumns { .. } => "HydrateColumns", Self::SortMergeJoin { .. } => "SortMergeJoin", Self::RowCompare { .. } => "RowCompare", Self::ClassifyThreeWay { .. } => "ClassifyThreeWay", @@ -563,6 +597,7 @@ impl PhysicalNode { pub fn inputs(&self) -> Vec { match self { Self::HydrateByAddress { input, .. } + | Self::HydrateColumns { input, .. } | Self::RowCompare { input, .. } | Self::ClassifyThreeWay { input } | Self::Page { input, .. } @@ -588,6 +623,7 @@ impl PhysicalNode { fn inputs_mut(&mut self) -> Vec<&mut NodeId> { match self { Self::HydrateByAddress { input, .. } + | Self::HydrateColumns { input, .. } | Self::RowCompare { input, .. } | Self::ClassifyThreeWay { input } | Self::Page { input, .. } @@ -881,6 +917,21 @@ impl PhysicalPlan { "node": "HydrateByAddress", "side": side, }), + PhysicalNode::HydrateColumns { bindings, .. } => json!({ + "node": "HydrateColumns", + "bindings": bindings + .iter() + .map(|binding| json!({ + "binding": binding.binding, + "table": binding.table.type_key, + "columns": binding + .columns + .iter() + .map(|column| column.property.as_str()) + .collect::>(), + })) + .collect::>(), + }), PhysicalNode::HashJoin { binding, fallback, .. } => json!({ @@ -1191,6 +1242,7 @@ impl PhysicalPlan { } PhysicalNode::RowCompare { input } | PhysicalNode::ClassifyThreeWay { input } + | PhysicalNode::HydrateColumns { input, .. } | PhysicalNode::Page { input, .. } | PhysicalNode::Limit { input, .. } | PhysicalNode::Filter { input, .. } diff --git a/crates/omnigraph-planner/tests/bound_plan.rs b/crates/omnigraph-planner/tests/bound_plan.rs index 016388b1a..b30914bc4 100644 --- a/crates/omnigraph-planner/tests/bound_plan.rs +++ b/crates/omnigraph-planner/tests/bound_plan.rs @@ -507,6 +507,31 @@ fn a_filtered_cross_join_has_its_own_tag_and_a_plain_one_the_old_shape() { assert_eq!(round_trip(&plain), plain); } +/// A top-k whose return-only column is fetched by row address reads back +/// with its `HydrateColumns` node and the scan's address column. +#[test] +fn a_hydrating_plan_reads_back_with_its_bindings() { + let mut query = query(prop("d", "slug")); + query.return_exprs.push(IRProjection { + expr: prop("d", "text"), + alias: None, + column: "d.text".into(), + ty: prop("d", "text").ty().clone(), + }); + query.limit = Some(2); + let plan = plan_query(&query, &source(), &fixture_bounds::BOUNDS).expect("the query plans"); + assert!(matches!( + plan.node(plan.root()), + Some(PhysicalNode::HydrateColumns { bindings, .. }) + if bindings.len() == 1 && bindings[0].columns[0].property == "text" + )); + let bound = BoundPlan { + plan, + values: Default::default(), + }; + assert_eq!(round_trip(&bound), bound); +} + #[test] fn expression_mirror_preserves_every_typed_interior_and_nested_cast() { use omnigraph_planner::mirror::ExprMirror; diff --git a/crates/omnigraph-planner/tests/lower_walk.rs b/crates/omnigraph-planner/tests/lower_walk.rs index b342634ac..c9e454e46 100644 --- a/crates/omnigraph-planner/tests/lower_walk.rs +++ b/crates/omnigraph-planner/tests/lower_walk.rs @@ -3,9 +3,9 @@ use omnigraph_compiler::ir::{IRExpr, IROrdering, IRProjection}; use omnigraph_planner::{ - AggregateFields, ColumnRef, ContainsJoinFields, ExpandFields, HashJoinFields, Lower, NodeId, - PhysicalNode, PhysicalPlan, PlanError, Prefilter, RankArm, RankFuseFields, RankKind, - RankedAccess, ScanInput, ScanSpec, SideId, SortMergeJoinFields, + AggregateFields, ColumnRef, ContainsJoinFields, ExpandFields, HashJoinFields, HydratedBinding, + Lower, NodeId, PhysicalNode, PhysicalPlan, PlanError, Prefilter, RankArm, RankFuseFields, + RankKind, RankedAccess, ScanInput, ScanSpec, SideId, SortMergeJoinFields, }; fn no_prefilter() -> Prefilter { @@ -85,6 +85,15 @@ impl Lower for Trace { self.call("hydrate_by_address", id, &[&input]) } + fn hydrate_columns( + &mut self, + id: NodeId, + _: &[HydratedBinding], + input: String, + ) -> Result { + self.call("hydrate_columns", id, &[&input]) + } + fn row_compare(&mut self, id: NodeId, input: String) -> Result { self.call("row_compare", id, &[&input]) } @@ -227,7 +236,11 @@ fn every_input_is_lowered_before_its_consumer_and_finish_runs_last() { input: filter, rows: 3, }); - plan.set_root(limit); + let hydrate = plan.add(PhysicalNode::HydrateColumns { + input: limit, + bindings: Vec::new(), + }); + plan.set_root(hydrate); let mut trace = Trace::default(); plan.lower(&mut trace).expect("the plan lowers"); @@ -240,7 +253,8 @@ fn every_input_is_lowered_before_its_consumer_and_finish_runs_last() { "cross_join#2(outer_reference#0(),outer_reference#1())", "filter#3(cross_join#2(outer_reference#0(),outer_reference#1()))", "limit#4(filter#3(cross_join#2(outer_reference#0(),outer_reference#1())))", - "finish#4(limit#4(filter#3(cross_join#2(outer_reference#0(),outer_reference#1()))))", + "hydrate_columns#5(limit#4(filter#3(cross_join#2(outer_reference#0(),outer_reference#1()))))", + "finish#5(hydrate_columns#5(limit#4(filter#3(cross_join#2(outer_reference#0(),outer_reference#1())))))", ] ); } diff --git a/crates/omnigraph-planner/tests/query_plan/cost.rs b/crates/omnigraph-planner/tests/query_plan/cost.rs index c221dc62f..c16a471ed 100644 --- a/crates/omnigraph-planner/tests/query_plan/cost.rs +++ b/crates/omnigraph-planner/tests/query_plan/cost.rs @@ -680,3 +680,232 @@ fn find_node<'j>(node: &'j serde_json::Value, name: &str) -> Option<&'j serde_js .flatten() .find_map(|input| find_node(input, name)) } + +/// The physical scan of `binding`'s projection after every pass. +fn physical_projection(plan: &PhysicalPlan, binding: &str) -> BTreeSet { + plan.live() + .find_map(|(_, node)| match node { + PhysicalNode::Scan { spec, .. } if spec.binding.as_deref() == Some(binding) => { + spec.projection.clone() + } + _ => None, + }) + .unwrap_or_else(|| panic!("no physical scan bound to `{binding}`")) + .into_iter() + .collect() +} + +/// One hydrated binding: its name and `(return position, property)` per +/// fetched column. +type Hydrated = (String, Vec<(usize, String)>); + +/// The root `HydrateColumns`: per binding, the properties it fetches with +/// the return positions they fill. +fn root_hydration(plan: &PhysicalPlan) -> Option> { + match plan.node(plan.root()) { + Some(PhysicalNode::HydrateColumns { bindings, .. }) => Some( + bindings + .iter() + .map(|binding| { + ( + binding.binding.clone(), + binding + .columns + .iter() + .map(|column| (column.position, column.property.clone())) + .collect(), + ) + }) + .collect(), + ), + _ => None, + } +} + +/// A top-k over a table four times its limit or larger, or of unknown size, +/// fetches the column only its output reads by row address above the limit; +/// the scan reads the row address in its place. The logical plan keeps the +/// column, and a table within four times the limit keeps it on the scan. +#[test] +fn a_top_k_fetches_its_return_only_column_by_row_address() { + let op = ir( + vec![scan("c")], + vec![prop("c", "slug"), prop("c", "body"), prop("c", "rank")], + vec![prop("c", "rank")], + ); + for rows in [Some(41), None] { + let source = source_with_rows(rows); + let mut logical = resolve(&op, &source).expect("resolve"); + let fired = rewrite(&mut logical, &source).expect("rewrite"); + let optimized = omnigraph_planner::physical_plan(&mut logical, &source, &bounds(), fired) + .expect("lower"); + let plan = optimized.physical; + assert!( + optimized.fired.contains(&"late_materialization"), + "{rows:?}: {:?}", + optimized.fired + ); + assert_eq!( + root_hydration(&plan), + Some(vec![("c".to_string(), vec![(1, "body".to_string())])]), + "{rows:?}" + ); + assert_eq!( + physical_projection(&plan, "c"), + set(&["__id", "slug", "rank", "_rowaddr"]) + ); + assert_eq!( + projection_of(&logical, "c"), + set(&["__id", "slug", "rank", "body"]) + ); + let json = plan.to_json(); + assert_eq!(json["node"], "HydrateColumns"); + assert_eq!(json["bindings"][0]["columns"], serde_json::json!(["body"])); + assert!(json["properties"]["retained_limit"].as_u64().is_some()); + } + let (plan, fired) = physical(&op, &source_with_rows(Some(40))); + assert!(!fired.contains(&"late_materialization"), "{fired:?}"); + assert_eq!(root_hydration(&plan), None); + assert_eq!( + physical_projection(&plan, "c"), + set(&["__id", "slug", "rank", "body"]) + ); +} + +/// A return item typed as its expression, named `column` (the alias when it +/// has one). +fn returned(expr: IRExpr, alias: Option<&str>, column: &str) -> IRProjection { + IRProjection { + ty: expr.ty().clone(), + expr, + alias: alias.map(str::to_string), + column: column.to_string(), + } +} + +/// Under a limit, a scan whose row estimate is above four times the limit +/// hydrates its return-only column, a bare table scan as much as a traversal +/// destination: Lance reads ahead of a consumer that stops early. A scan a key +/// equality bounds to one row keeps its column. +#[test] +fn a_limit_hydrates_every_large_scan_but_not_a_key_lookup() { + let bare = ir(vec![scan("c")], vec![prop("c", "body")], vec![]); + let (plan, fired) = physical(&bare, &source_with_rows(Some(1_000))); + assert!(fired.contains(&"late_materialization"), "{fired:?}"); + assert_eq!( + root_hydration(&plan), + Some(vec![("c".to_string(), vec![(0, "body".to_string())])]) + ); + + let lookup = ir( + vec![IROp::NodeScan { + variable: "c".to_string(), + type_name: "T".to_string(), + filters: vec![IRExpr::comparison( + prop("c", "slug"), + CompOp::Eq, + IRExpr::Literal( + Literal::String("one".to_string()), + value_type(ScalarType::String, false), + ), + )], + }], + vec![prop("c", "body")], + vec![], + ); + let (plan, fired) = physical(&lookup, &source_with_rows(Some(1_000))); + assert!(!fired.contains(&"late_materialization"), "{fired:?}"); + assert_eq!(root_hydration(&plan), None); + + let traversal = ir( + vec![scan("a"), expand("a", "b", vec![])], + vec![prop("a", "slug"), prop("b", "body")], + vec![], + ); + let (plan, fired) = physical(&traversal, &source_with_rows(Some(1_000))); + assert!(fired.contains(&"late_materialization"), "{fired:?}"); + assert_eq!( + root_hydration(&plan), + Some(vec![("b".to_string(), vec![(1, "body".to_string())])]) + ); + assert!(physical_projection(&plan, "b").contains("_rowaddr")); + assert!(!physical_projection(&plan, "b").contains("body")); +} + +/// Only a bare return-only property is hydrated: the key, a sort key, a +/// column a filter reads, a computed return (named or not) and a return the +/// sort orders by alias stay on the scan, and an unnamed computed return does +/// not keep the others from hydrating. +#[test] +fn hydration_keeps_every_column_something_besides_the_output_reads() { + let op = Operation::Query(Box::new(QueryIR { + name: "q".to_string(), + params: vec![], + pipeline: vec![IROp::NodeScan { + variable: "c".to_string(), + type_name: "T".to_string(), + filters: vec![IRExpr::comparison( + prop("c", "state"), + CompOp::Eq, + IRExpr::Literal( + Literal::String("open".to_string()), + value_type(ScalarType::String, false), + ), + )], + }], + return_exprs: vec![ + returned(prop("c", "slug"), None, "c.slug"), + returned(prop("c", "rank"), None, "c.rank"), + returned(prop("c", "state"), None, "c.state"), + returned(prop("c", "title"), Some("t"), "t"), + returned( + IRExpr::comparison( + prop("c", "kind"), + CompOp::Eq, + IRExpr::Literal( + Literal::String("x".to_string()), + value_type(ScalarType::String, false), + ), + ), + Some("is_x"), + "is_x", + ), + returned( + IRExpr::comparison( + prop("c", "edits"), + CompOp::Eq, + IRExpr::Literal( + Literal::String("0".to_string()), + value_type(ScalarType::String, false), + ), + ), + None, + "edits_eq", + ), + returned(prop("c", "body"), Some("text"), "text"), + ], + order_by: vec![ + IROrdering { + expr: prop("c", "rank"), + descending: false, + }, + IROrdering { + expr: IRExpr::AliasRef("t".to_string(), property_type("title")), + descending: true, + }, + ], + limit: Some(5), + })); + let (plan, fired) = physical(&op, &source_with_rows(Some(1_000))); + assert!(fired.contains(&"late_materialization"), "{fired:?}"); + assert_eq!( + root_hydration(&plan), + Some(vec![("c".to_string(), vec![(6, "body".to_string())])]) + ); + assert_eq!( + physical_projection(&plan, "c"), + set(&[ + "__id", "slug", "rank", "state", "title", "kind", "edits", "_rowaddr" + ]) + ); +} diff --git a/crates/omnigraph/src/engine/expr.rs b/crates/omnigraph/src/engine/expr.rs index e6e78447d..4d97d19e3 100644 --- a/crates/omnigraph/src/engine/expr.rs +++ b/crates/omnigraph/src/engine/expr.rs @@ -34,6 +34,7 @@ impl ProjectionContext { PhysicalNode::SortMergeJoin { .. } | PhysicalNode::HashJoin { .. } | PhysicalNode::HydrateByAddress { .. } + | PhysicalNode::HydrateColumns { .. } | PhysicalNode::RowCompare { .. } | PhysicalNode::ClassifyThreeWay { .. } | PhysicalNode::Limit { .. } diff --git a/crates/omnigraph/src/engine/lower.rs b/crates/omnigraph/src/engine/lower.rs index d95e1d01b..973293a4f 100644 --- a/crates/omnigraph/src/engine/lower.rs +++ b/crates/omnigraph/src/engine/lower.rs @@ -25,18 +25,19 @@ use omnigraph_compiler::traversal::EDGE_TYPE_COLUMN; use omnigraph_planner::logical::{EDGE_TYPE_MEMBER, IDENTITY_MEMBER}; use omnigraph_planner::{ Accumulator, AggregateFields, BoundPlan, ColumnRef, ContainsJoinFields, ExpandFields, - HashJoinFields, Lower, NodeId, PhysicalNode, PhysicalPlan, PlanError, Predicate, RankArm, - RankFuseFields, RankKind, RankedAccess, RuntimeFilterKind, RuntimeFilterSpec, ScanInput, - ScanSpec, SideId, SortMergeJoinFields, ValueTable, + HashJoinFields, HydratedBinding, Lower, NodeId, PhysicalNode, PhysicalPlan, PlanError, + Predicate, ROW_ADDRESS_PREFIX, RankArm, RankFuseFields, RankKind, RankedAccess, + RuntimeFilterKind, RuntimeFilterSpec, ScanInput, ScanSpec, SideId, SortMergeJoinFields, + ValueTable, }; use super::adapters::{GqProjectionExpr, LoweringId, Projected}; use super::exact_aggregate::{EXACT_CARRIER, ExactIntegerUdaf}; use super::operators::{ AntiJoinMaskExec, ArmOrder, ContainsJoinExec, CrossJoinExec, ExpandExec, ExpandExecution, - ExpandStep, FilterExec, GraphEnv, HashJoinExec, LimitExec, LookupSpec, MetadataCountExec, - OuterReferenceExec, OuterSlot, ProjectionExec, RankFuseExec, RuntimeFilterSlot, ScanExec, - ScanSource, SortExec, SortKey, fresh_tag_column, tagged_schema, + ExpandStep, FilterExec, GraphEnv, HashJoinExec, HydrateExec, LimitExec, LookupSpec, + MetadataCountExec, OuterReferenceExec, OuterSlot, ProjectionExec, RankFuseExec, + RuntimeFilterSlot, ScanExec, ScanSource, SortExec, SortKey, fresh_tag_column, tagged_schema, }; use super::*; @@ -295,6 +296,35 @@ impl<'a> Lowering<'a> { _ => None, } } + + /// The rows of the `Limit` directly over the projection over scan `id`: + /// the scan sends its first batch once it has that many rows instead of + /// gathering a full batch. Batch size never changes which rows reach the + /// limit, only how many are read before it stops. + fn limit_over_scan(&self, id: NodeId) -> Option { + let projection = self.plan.parent_of(id)?; + let Some(PhysicalNode::Projection { .. }) = self.plan.node(projection) else { + return None; + }; + match self.plan.node(self.plan.parent_of(projection)?)? { + PhysicalNode::Limit { rows, .. } => Some(*rows), + _ => None, + } + } + + /// The bindings a `HydrateColumns` over node `id` (through the `Sort`s + /// and the `Limit` above it) fetches: the projection skips their columns + /// and carries each binding's row address instead. + fn hydration_above(&self, mut id: NodeId) -> &'a [HydratedBinding] { + while let Some(parent) = self.plan.parent_of(id) { + match self.plan.node(parent) { + Some(PhysicalNode::Sort { .. } | PhysicalNode::Limit { .. }) => id = parent, + Some(PhysicalNode::HydrateColumns { bindings, .. }) => return bindings, + _ => break, + } + } + &[] + } } /// The engine's error with the planner walk's own refusals folded in. @@ -387,20 +417,43 @@ impl<'l, 'a> Walk<'l, 'a> { /// The one operator of node `id`. fn built(&mut self, id: NodeId, operator: impl ExecutionPlan) -> Lowers { let plan: Plan = Arc::new(operator); + let node = self.lowering.node(id)?; if matches!( - self.lowering.node(id)?, + node, PhysicalNode::Projection { .. } | PhysicalNode::Aggregate { .. } | PhysicalNode::MetadataCount { .. } + | PhysicalNode::HydrateColumns { .. } ) { let declared = self.lowering.plan.properties(id).ok_or_else(|| { OmniError::manifest_internal("return operator has no declared schema") })?; - super::typed_value::check_output_schema( - plan.schema().as_ref(), - declared.schema.as_ref(), - matches!(self.lowering.node(id)?, PhysicalNode::Projection { .. }), - )?; + let hidden = matches!( + node, + PhysicalNode::Projection { .. } | PhysicalNode::HydrateColumns { .. } + ); + // A projection under `HydrateColumns` carries every declared + // column but the deferred ones, which the hydration above adds. + let deferred: Vec = if matches!(node, PhysicalNode::Projection { .. }) { + self.lowering + .hydration_above(id) + .iter() + .flat_map(|binding| binding.columns.iter().map(|column| column.position)) + .collect() + } else { + Vec::new() + }; + let carried = Schema::new( + declared + .schema + .fields() + .iter() + .enumerate() + .filter(|(position, _)| !deferred.contains(position)) + .map(|(_, field)| Arc::clone(field)) + .collect::>(), + ); + super::typed_value::check_output_schema(plan.schema().as_ref(), &carried, hidden)?; } self.operators.insert(id, Arc::clone(&plan)); Ok(plan) @@ -471,6 +524,9 @@ impl Lower for Walk<'_, '_> { } }; let mut scan = self.lowering.scan(source, spec)?; + if let Some(rows) = self.lowering.limit_over_scan(id) { + scan = scan.with_gather_rows(rows); + } if let Some(filter) = runtime { scan = scan.with_runtime_filter(Some(Arc::clone(&filter))); self.runtime_filters.insert(id, filter); @@ -511,6 +567,28 @@ impl Lower for Walk<'_, '_> { Err(Self::not_a_pipeline_node("HydrateByAddress")) } + fn hydrate_columns( + &mut self, + id: NodeId, + bindings: &[HydratedBinding], + input: Plan, + ) -> Lowers { + if !self.outers.is_empty() { + return Err(Self::not_a_pipeline_node("HydrateColumns")); + } + let declared = self.lowering.plan.properties(id).ok_or_else(|| { + OmniError::manifest_internal("`HydrateColumns` has no declared schema") + })?; + let hydrate = HydrateExec::try_new( + input, + bindings.to_vec(), + self.lowering.snapshot.clone(), + self.lowering.catalog, + declared.schema.as_ref(), + )?; + self.built(id, hydrate) + } + fn row_compare(&mut self, _id: NodeId, _input: Plan) -> Lowers { Err(Self::not_a_pipeline_node("RowCompare")) } @@ -739,13 +817,34 @@ impl Lower for Walk<'_, '_> { return Err(OmniError::manifest("query has no return projections".to_string()).into()); } let input_schema = input.schema(); + let hydrated = self.lowering.hydration_above(id); + let deferred: Vec = hydrated + .iter() + .flat_map(|binding| binding.columns.iter().map(|column| column.position)) + .collect(); let mut exprs: Vec<(Arc, String)> = - Vec::with_capacity(return_exprs.len()); - for proj in return_exprs { + Vec::with_capacity(return_exprs.len() + hydrated.len()); + for (position, proj) in return_exprs.iter().enumerate() { + if deferred.contains(&position) { + continue; + } let projected = Projected::Expression(proj.expr.clone(), proj.ty.clone()); let name = return_name(proj); exprs.push((self.lowering.projection(projected, &self.scope), name)); } + for binding in hydrated { + let name = format!("{}.{}", binding.binding, lance_core::ROW_ADDR); + let (index, _) = input_schema.column_with_name(&name).ok_or_else(|| { + OmniError::manifest_internal(format!( + "the scan of `${}` carries no row address for `HydrateColumns`", + binding.binding + )) + })?; + exprs.push(( + Arc::new(Column::new(&name, index)), + format!("{ROW_ADDRESS_PREFIX}{}", binding.binding), + )); + } if let Some((keys, tiebreak)) = self.lowering.sort_above(id) { let hidden = hidden_columns( keys, diff --git a/crates/omnigraph/src/engine/operators/hydrate.rs b/crates/omnigraph/src/engine/operators/hydrate.rs new file mode 100644 index 000000000..195e101e8 --- /dev/null +++ b/crates/omnigraph/src/engine/operators/hydrate.rs @@ -0,0 +1,527 @@ +//! `HydrateExec`: a `HydrateColumns` node. The rows that reached the output +//! carry each deferred binding's row address (the `^binding` column the +//! return projection below carries); this operator takes the deferred +//! columns of those rows from the binding's pinned table by address, one +//! chunk at a time under `hydrate_chunk_bytes`, and emits the return columns +//! in return order without the addresses. A chunk's bound covers the fetched +//! rows and their copies, one per output row, which the pool admits before +//! Arrow builds them. + +use std::collections::HashMap; +use std::fmt; +use std::sync::Arc; + +use arrow_array::{Array, ArrayRef, RecordBatch, UInt32Array, UInt64Array, new_null_array}; +use arrow_schema::{Field, Schema, SchemaRef}; +use datafusion::common::{DataFusionError, Result as DfResult}; +use datafusion::execution::TaskContext; +use datafusion::physical_plan::metrics::{ + ExecutionPlanMetricsSet, Gauge, MetricBuilder, MetricsSet, +}; +use datafusion::physical_plan::{ + DisplayAs, DisplayFormatType, ExecutionPlan, PlanProperties, SendableRecordBatchStream, +}; +use futures::StreamExt; +use lance::Dataset; +use lance::dataset::TakeBuilder; +use lance_core::ROW_ADDR; +use lance_datafusion::projection::ProjectionPlan; +use omnigraph_compiler::catalog::Catalog; +use omnigraph_planner::{HydratedBinding, ROW_ADDRESS_PREFIX, hydrate_chunk_bytes}; + +use super::memory::WorkMemory; +use super::producer::{BatchSender, producer_stream}; +use super::{external, polled, streaming_properties}; +use crate::db::Snapshot; +use crate::error::{OmniError, Result}; + +/// The pool owner of one hydrated chunk: the fetched rows, then the output. +const CHUNK: &str = "hydrate chunk"; + +/// The admission of a chunk's copied values while Arrow builds them. +const OUTPUT: &str = "hydrate output"; + +/// The gauge of the most bytes one chunk held at once: its fetched rows and +/// their copies. It stays under `hydrate_chunk_bytes` unless one row alone +/// exceeds it. +const PEAK_CHUNK_BYTES: &str = "peak_chunk_bytes"; + +/// Rows of the first chunk, before a hydrated row's width is measured. +const SEED_ROWS: usize = 4; + +pub(crate) struct HydrateExec { + input: Arc, + bindings: Vec, + layout: Vec, + snapshot: Snapshot, + properties: Arc, + metrics: ExecutionPlanMetricsSet, +} + +/// Where one output column comes from: an input column (by index), or the +/// `column`th deferred column of the `binding`th hydrated binding. +#[derive(Debug, Clone, Copy)] +enum Slot { + Input(usize), + Hydrated { binding: usize, column: usize }, +} + +impl HydrateExec { + /// The output is the return order: a deferred column sits at its return + /// position under its return name with the field `declared` gives it, + /// as the projection would have carried it; the input's other columns + /// fill the remaining positions in their order, and the row addresses + /// leave. + pub(crate) fn try_new( + input: Arc, + bindings: Vec, + snapshot: Snapshot, + catalog: &Catalog, + declared: &Schema, + ) -> Result { + let input_schema = input.schema(); + let mut deferred: HashMap = HashMap::new(); + for (index, binding) in bindings.iter().enumerate() { + let address = address_column(&binding.binding); + if input_schema.column_with_name(&address).is_none() { + return Err(OmniError::manifest_internal(format!( + "the rows reaching `HydrateColumns` carry no row address for `${}`", + binding.binding + ))); + } + let node_type = binding + .table + .node_type_name() + .and_then(|type_name| catalog.node_types.get(type_name)) + .ok_or_else(|| { + OmniError::manifest_internal(format!( + "`HydrateColumns` reads `{}`, which is no node type", + binding.table.type_key + )) + })?; + for (column_index, column) in binding.columns.iter().enumerate() { + if node_type + .arrow_schema + .field_with_name(&column.property) + .is_err() + { + return Err(OmniError::manifest_internal(format!( + "`{}` has no property `{}` to hydrate", + binding.table.type_key, column.property + ))); + } + let field = declared.field_with_name(&column.output).map_err(|_| { + OmniError::manifest_internal(format!( + "`HydrateColumns` declares no return column `{}`", + column.output + )) + })?; + let slot = Slot::Hydrated { + binding: index, + column: column_index, + }; + let field = field.clone(); + if deferred.insert(column.position, (slot, field)).is_some() { + return Err(OmniError::manifest_internal(format!( + "two hydrated columns fill return position {}", + column.position + ))); + } + } + } + let mut carried = input_schema + .fields() + .iter() + .enumerate() + .filter(|(_, field)| !field.name().starts_with(ROW_ADDRESS_PREFIX)); + let width = carried.clone().count() + deferred.len(); + let mut layout = Vec::with_capacity(width); + let mut fields = Vec::with_capacity(width); + for position in 0..width { + let (slot, field) = match deferred.remove(&position) { + Some(hydrated) => hydrated, + None => { + let (index, field) = carried.next().ok_or_else(|| { + OmniError::manifest_internal(format!( + "`HydrateColumns` has no input column for return position {position}" + )) + })?; + (Slot::Input(index), field.as_ref().clone()) + } + }; + layout.push(slot); + fields.push(field); + } + if let Some(position) = deferred.keys().min() { + return Err(OmniError::manifest_internal(format!( + "a hydrated column fills return position {position}, past the return's {width}" + ))); + } + let properties = streaming_properties(Arc::new(Schema::new(fields))); + Ok(Self { + input, + bindings, + layout, + snapshot, + properties, + metrics: ExecutionPlanMetricsSet::new(), + }) + } +} + +/// The input column holding `binding`'s row address. +fn address_column(binding: &str) -> String { + format!("{ROW_ADDRESS_PREFIX}{binding}") +} + +impl fmt::Debug for HydrateExec { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("HydrateExec") + .field("bindings", &self.bindings) + .finish_non_exhaustive() + } +} + +impl DisplayAs for HydrateExec { + fn fmt_as(&self, _t: DisplayFormatType, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let bindings: Vec = self + .bindings + .iter() + .map(|binding| { + let columns: Vec<&str> = binding + .columns + .iter() + .map(|column| column.property.as_str()) + .collect(); + format!("${}: [{}]", binding.binding, columns.join(", ")) + }) + .collect(); + write!(f, "HydrateExec: {}", bindings.join(", ")) + } +} + +impl ExecutionPlan for HydrateExec { + fn name(&self) -> &str { + "HydrateExec" + } + + fn metrics(&self) -> Option { + Some(self.metrics.clone_inner()) + } + + fn properties(&self) -> &Arc { + &self.properties + } + + fn children(&self) -> Vec<&Arc> { + vec![&self.input] + } + + fn with_new_children( + self: Arc, + mut children: Vec>, + ) -> DfResult> { + assert_eq!(children.len(), 1, "HydrateExec has one child"); + Ok(Arc::new(Self { + input: children.pop().expect("one child"), + bindings: self.bindings.clone(), + layout: self.layout.clone(), + snapshot: self.snapshot.clone(), + properties: Arc::clone(&self.properties), + metrics: ExecutionPlanMetricsSet::new(), + })) + } + + fn execute( + &self, + partition: usize, + ctx: Arc, + ) -> DfResult { + assert_eq!(partition, 0, "HydrateExec has one partition"); + let schema: SchemaRef = self.schema(); + let input = self.input.execute(0, Arc::clone(&ctx))?; + let mut work = WorkMemory::new(ctx, "HydrateExec")?; + work.set_metrics(self.metrics.clone()); + work.metric("input_rows", 0); + work.metric("hydrated_rows", 0); + let plan = Hydration { + layout: self.layout.clone(), + bindings: self.bindings.clone(), + snapshot: self.snapshot.clone(), + schema: Arc::clone(&schema), + peak: MetricBuilder::new(&self.metrics).gauge(PEAK_CHUNK_BYTES, 0), + }; + let stream = producer_stream( + schema, + Arc::new(work), + Some(&self.metrics), + move |memory, sender| async move { hydrate(input, plan, &memory, &sender).await }, + ); + Ok(polled(&self.metrics, stream)) + } +} + +/// What one execution hydrates, moved into its producer. +struct Hydration { + layout: Vec, + bindings: Vec, + snapshot: Snapshot, + schema: SchemaRef, + peak: Gauge, +} + +/// One deferred binding's pinned table and the columns taken from it. +struct Source { + binding: String, + address: String, + dataset: Arc, + projection: Arc, + /// `(return name, property)` for each deferred return item. + columns: Vec<(String, String)>, +} + +async fn hydrate( + mut input: SendableRecordBatchStream, + plan: Hydration, + memory: &Arc, + sender: &BatchSender, +) -> DfResult<()> { + let hard = usize::try_from(hydrate_chunk_bytes(memory.pool_bytes())).unwrap_or(usize::MAX); + let target = (hard / 2).max(1); + let mut sources = Vec::with_capacity(plan.bindings.len()); + for binding in &plan.bindings { + let dataset = Arc::new( + plan.snapshot + .open_lance_dataset(&binding.table.type_key) + .await + .map_err(external)?, + ); + let mut properties: Vec<&str> = Vec::new(); + for column in &binding.columns { + if !properties.contains(&column.property.as_str()) { + properties.push(&column.property); + } + } + let projection = dataset + .schema() + .project(&properties) + .and_then(|schema| ProjectionPlan::from_schema(dataset.clone(), &schema)) + .map_err(|error| external(OmniError::storage_context("hydrate projection", error)))?; + sources.push(Source { + binding: binding.binding.clone(), + address: address_column(&binding.binding), + dataset, + projection: Arc::new(projection), + columns: binding + .columns + .iter() + .map(|column| (column.output.clone(), column.property.clone())) + .collect(), + }); + } + // The costliest output row seen so far, its share of the fetched rows + // and its own copy, plans the next window's rows. + let mut row_bytes = 0usize; + while let Some(batch) = input.next().await { + let batch = batch?; + memory.metric("input_rows", batch.num_rows()); + let mut start = 0; + while start < batch.num_rows() { + let planned = target + .checked_div(row_bytes) + .map_or(SEED_ROWS, |rows| rows.max(1)); + let mut rows = planned.min(batch.num_rows() - start); + loop { + memory.check()?; + let window = batch.slice(start, rows); + let chunk = Arc::new(memory.child(CHUNK)?); + let (fetched, fetched_bytes) = + fetch_window(&window, &sources, &plan.schema, &chunk).await?; + if fetched_bytes > hard && rows > 1 { + rows = rows.div_ceil(2); + continue; + } + // A row a join repeats is copied once per output row, so the + // window keeps a prefix, halved until the pool's estimate of + // its copies fits beside the fetched rows; the rest is the + // next window. + let mut keep = rows; + let mut copied = copy_bytes(&fetched, keep, &chunk)?; + while keep > 1 && fetched_bytes.saturating_add(copied) > hard { + keep = keep.div_ceil(2); + copied = copy_bytes(&fetched, keep, &chunk)?; + } + row_bytes = row_bytes + .max(fetched_bytes.saturating_add(copied) / keep) + .max(1); + let taken = fetched + .iter() + .map(|source| { + chunk + .take_once(&source.values, &source.indices.slice(0, keep), OUTPUT) + .map(|values| values.columns().to_vec()) + }) + .collect::>>()?; + let copies = taken + .iter() + .flatten() + .map(|column| column.get_array_memory_size()) + .sum::(); + plan.peak.set_max(fetched_bytes.saturating_add(copies)); + drop(fetched); + let output = assemble(&window.slice(0, keep), &taken, &plan)?; + chunk.output(&output)?; + memory.metric("hydrated_rows", keep); + sender.send(output, chunk).await?; + start += keep; + break; + } + } + } + Ok(()) +} + +/// One source's deferred columns for a window: `values` holds them for the +/// distinct rows Lance returned (one null row when every address is null), +/// and `indices` places each window row among those rows, null for a null +/// address. +struct Fetched { + values: RecordBatch, + indices: UInt32Array, +} + +/// What `take_once` admits to copy the first `rows` rows of the window from +/// every source. +fn copy_bytes(fetched: &[Fetched], rows: usize, chunk: &WorkMemory) -> DfResult { + fetched.iter().try_fold(0usize, |sum, source| { + Ok(sum.saturating_add(chunk.take_bytes(&source.values, &source.indices.slice(0, rows))?)) + }) +} + +/// Each source's deferred columns for the distinct rows of `window`, held +/// by `chunk`, and the bytes Lance returned. A row address the pinned table +/// does not hold is an integrity failure. +async fn fetch_window( + window: &RecordBatch, + sources: &[Source], + schema: &SchemaRef, + chunk: &WorkMemory, +) -> DfResult<(Vec, usize)> { + let mut fetched = Vec::with_capacity(sources.len()); + let mut bytes = 0usize; + for source in sources { + let addresses = window + .column_by_name(&source.address) + .and_then(|column| column.as_any().downcast_ref::()) + .ok_or_else(|| { + DataFusionError::Internal(format!( + "`HydrateColumns` input has no UInt64 '{}'", + source.address + )) + })?; + let mut unique: Vec = addresses.iter().flatten().collect(); + unique.sort_unstable(); + unique.dedup(); + chunk.entries::<(u64, u32)>(unique.len())?; + let fields = source + .columns + .iter() + .map(|(output, _)| { + let field = schema.field_with_name(output)?; + Ok(Field::new(output, field.data_type().clone(), true)) + }) + .collect::>>()?; + let values_schema = Arc::new(Schema::new(fields)); + if unique.is_empty() { + let columns = values_schema + .fields() + .iter() + .map(|field| new_null_array(field.data_type(), 1)) + .collect(); + fetched.push(Fetched { + values: RecordBatch::try_new(values_schema, columns)?, + indices: UInt32Array::new_null(window.num_rows()), + }); + continue; + } + let batch = TakeBuilder::try_new_from_addresses( + Arc::clone(&source.dataset), + unique, + Arc::clone(&source.projection), + ) + .map(|builder| builder.with_row_address(true)) + .map_err(|error| external(OmniError::storage_context("hydrate take", error)))? + .execute() + .await + .map_err(|error| { + external(OmniError::storage_context( + format!("hydrating the return columns of `${}`", source.binding), + error, + )) + })?; + chunk.hold(&batch)?; + bytes = bytes.saturating_add(batch.get_array_memory_size()); + let positions: HashMap = batch + .column_by_name(ROW_ADDR) + .and_then(|column| column.as_any().downcast_ref::()) + .ok_or_else(|| { + DataFusionError::Internal("a row-address take returned no _rowaddr".into()) + })? + .values() + .iter() + .enumerate() + .map(|(row, address)| Ok((*address, u32::try_from(row)?))) + .collect::>() + .map_err(|_| DataFusionError::Internal("a hydrated chunk exceeds u32 rows".into()))?; + let indices = addresses + .iter() + .map(|address| { + address + .map(|address| { + positions.get(&address).copied().ok_or_else(|| { + external(OmniError::manifest_internal(format!( + "row address {address} of `${}` is not in its pinned table", + source.binding + ))) + }) + }) + .transpose() + }) + .collect::>()?; + let columns = source + .columns + .iter() + .map(|(_, property)| { + batch.column_by_name(property).cloned().ok_or_else(|| { + DataFusionError::Internal(format!( + "a row-address take returned no '{property}'" + )) + }) + }) + .collect::>>()?; + fetched.push(Fetched { + values: RecordBatch::try_new(values_schema, columns)?, + indices, + }); + } + Ok((fetched, bytes)) +} + +/// The return columns of `window` in return order, as `plan.layout` places +/// them: a deferred one from its source's taken arrays, any other from the +/// window. +fn assemble( + window: &RecordBatch, + taken: &[Vec], + plan: &Hydration, +) -> DfResult { + let columns = plan + .layout + .iter() + .map(|slot| match *slot { + Slot::Input(index) => Arc::clone(window.column(index)), + Slot::Hydrated { binding, column } => Arc::clone(&taken[binding][column]), + }) + .collect::>(); + Ok(RecordBatch::try_new(Arc::clone(&plan.schema), columns)?) +} diff --git a/crates/omnigraph/src/engine/operators/memory.rs b/crates/omnigraph/src/engine/operators/memory.rs index d288e9621..b31ccd623 100644 --- a/crates/omnigraph/src/engine/operators/memory.rs +++ b/crates/omnigraph/src/engine/operators/memory.rs @@ -457,6 +457,11 @@ impl WorkMemory { }) } + /// The query pool's size, which the plan's assumed memory limit set. + pub(in crate::engine) fn pool_bytes(&self) -> u64 { + self.resources.limit + } + /// Target bytes per producer batch, leaving room for queued batches and consumers. pub(in crate::engine) fn batch_bytes(&self) -> usize { (self.resources.limit / 32).clamp(1, 1024 * 1024) as usize @@ -687,13 +692,26 @@ impl WorkMemory { self.take_admitted(batch, indices, name, 1) } - fn take_admitted( + /// The bytes `take_once` admits for the rows of `batch` at `indices` + /// before Arrow allocates them: one copy per index, a row repeated by + /// the indices once per repetition, and Arrow's scratch. A caller sizes + /// a window by it before it takes. + pub(in crate::engine) fn take_bytes( &self, batch: &RecordBatch, indices: &UInt32Array, - name: &str, - factor: usize, - ) -> DfResult { + ) -> DfResult { + let (picked, scratch) = self.take_estimate(batch, indices)?; + Ok(picked.saturating_add(scratch)) + } + + /// The picked bytes of a take and its scratch bytes. A null index copies + /// no row: it costs what a null row of the column costs. + fn take_estimate( + &self, + batch: &RecordBatch, + indices: &UInt32Array, + ) -> DfResult<(usize, usize)> { let mut bytes = 0usize; let mut scratch_bytes = 0usize; for column in batch.columns() { @@ -703,18 +721,38 @@ impl WorkMemory { let picked = match flat_take_size(&data, indices) { Some(flat) => flat, None => { + let mut null_row = None; let mut picked = 0usize; - for row in indices.values() { + for row in indices.iter() { self.check()?; - picked = picked - .saturating_add(slice_memory_size(&data, *row as usize, 1)?) - .saturating_add(16); + let row_bytes = match row { + Some(row) => slice_memory_size(&data, row as usize, 1)?, + None => match null_row { + Some(bytes) => bytes, + None => { + let null = ArrayData::new_null(data.data_type(), 1); + *null_row.insert(slice_memory_size(&null, 0, 1)?) + } + }, + }; + picked = picked.saturating_add(row_bytes).saturating_add(16); } picked } }; bytes = bytes.saturating_add(picked).saturating_add(128); } + Ok((bytes, scratch_bytes)) + } + + fn take_admitted( + &self, + batch: &RecordBatch, + indices: &UInt32Array, + name: &str, + factor: usize, + ) -> DfResult { + let (bytes, scratch_bytes) = self.take_estimate(batch, indices)?; let admitted = self.resources.reservation( name, bytes.saturating_mul(factor).saturating_add(scratch_bytes), @@ -733,7 +771,9 @@ impl WorkMemory { /// An upper bound on the picked bytes of a flat or fixed-size-list column /// without a slice per row: 16 per row, 2 more when nullable, the row's own -/// bytes and offset. `None` for the variable nested and dictionary kinds. +/// bytes and offset. A null index copies no bytes of a variable-width value +/// (Arrow's byte take skips it); fixed-width values are allocated for every +/// index. `None` for the variable nested and dictionary kinds. fn flat_take_size(data: &ArrayData, indices: &UInt32Array) -> Option { let rows = indices.len(); let per_row = if data.nulls().is_some() { 18 } else { 16 }; @@ -744,18 +784,22 @@ fn flat_take_size(data: &ArrayData, indices: &UInt32Array) -> Option { .saturating_mul(field.data_type().primitive_width()?), DataType::Utf8 | DataType::Binary => { let offsets = data.buffers()[0].typed_data::(); - indices.values().iter().fold(0usize, |sum, &row| { - let row = base + row as usize; - sum.saturating_add((offsets[row + 1] - offsets[row]) as usize) - .saturating_add(4) + indices.iter().fold(0usize, |sum, row| { + let value = row.map_or(0, |row| { + let row = base + row as usize; + (offsets[row + 1] - offsets[row]) as usize + }); + sum.saturating_add(value).saturating_add(4) }) } DataType::LargeUtf8 | DataType::LargeBinary => { let offsets = data.buffers()[0].typed_data::(); - indices.values().iter().fold(0usize, |sum, &row| { - let row = base + row as usize; - sum.saturating_add((offsets[row + 1] - offsets[row]) as usize) - .saturating_add(8) + indices.iter().fold(0usize, |sum, row| { + let value = row.map_or(0, |row| { + let row = base + row as usize; + (offsets[row + 1] - offsets[row]) as usize + }); + sum.saturating_add(value).saturating_add(8) }) } DataType::Boolean => rows, @@ -1151,6 +1195,56 @@ mod tests { } } + /// A take admits one copy per index and a null row per null index, in the + /// flat path (a text column) and the per-row path (a struct over it): 64 + /// null slots whose raw value names a 512 KiB row admit a few KiB, and + /// four repetitions of that row admit four copies per column, which a + /// 4 MiB pool holding the batch refuses before Arrow builds them. A case + /// cannot write null indices. + #[test] + fn a_take_admits_a_copy_per_index_and_a_null_row_per_null_index() { + use arrow_array::{StringArray, StructArray}; + use datafusion::arrow::buffer::NullBuffer; + let wide = "x".repeat(512 * 1024); + let text: Arc = + Arc::new(StringArray::from(vec![Some(wide.as_str()), Some("y")])); + let field = Arc::new(Field::new("text", DataType::Utf8, true)); + let nested: Arc = Arc::new(StructArray::from(vec![( + Arc::clone(&field), + Arc::clone(&text), + )])); + let schema = Arc::new(Schema::new(vec![ + field.as_ref().clone(), + Field::new("nested", nested.data_type().clone(), true), + ])); + let batch = RecordBatch::try_new(schema, vec![text, nested]).unwrap(); + let (pool, memory) = test_memory(4 * 1_048_576); + memory.hold(&batch).unwrap(); + let nulls = UInt32Array::new(vec![0u32; 64].into(), Some(NullBuffer::new_null(64))); + let admitted = memory.take_bytes(&batch, &nulls).unwrap(); + assert!( + admitted < 16 * 1024, + "null indices admitted {admitted} bytes" + ); + let taken = memory.take_once(&batch, &nulls, "test take").unwrap(); + assert_eq!(taken.num_rows(), 64); + assert!( + taken + .columns() + .iter() + .all(|column| column.null_count() == 64) + ); + let repeated = UInt32Array::from(vec![0u32; 4]); + assert!(memory.take_bytes(&batch, &repeated).unwrap() >= 8 * 512 * 1024); + assert!( + memory.take_once(&batch, &repeated, "test take").is_err(), + "four copies of the row in two columns exceed the pool" + ); + drop(taken); + drop(memory); + assert_eq!(pool.reserved(), 0); + } + /// Pins the runtime to one blocking thread; a case cannot size the runtime. #[test] fn one_blocking_thread_can_poll_nested_work_and_async_io() { diff --git a/crates/omnigraph/src/engine/operators/mod.rs b/crates/omnigraph/src/engine/operators/mod.rs index b4ede7dee..36805b1bf 100644 --- a/crates/omnigraph/src/engine/operators/mod.rs +++ b/crates/omnigraph/src/engine/operators/mod.rs @@ -36,6 +36,7 @@ mod filter; #[cfg(test)] mod fixtures; mod hash_join; +mod hydrate; mod limit; pub(in crate::engine) mod memory; mod metadata_count; @@ -59,6 +60,7 @@ pub(super) use expand::{ }; pub(super) use filter::FilterExec; pub(super) use hash_join::{HashJoinExec, LookupSpec}; +pub(super) use hydrate::HydrateExec; pub(super) use limit::LimitExec; pub(super) use metadata_count::MetadataCountExec; pub(super) use projection::ProjectionExec; diff --git a/crates/omnigraph/src/engine/operators/scan/mod.rs b/crates/omnigraph/src/engine/operators/scan/mod.rs index 7faa3ff08..7d70183a5 100644 --- a/crates/omnigraph/src/engine/operators/scan/mod.rs +++ b/crates/omnigraph/src/engine/operators/scan/mod.rs @@ -1,6 +1,7 @@ //! `ScanExec`: a binding's scan under its `SearchMode`, columns prefixed, its -//! `ScanReport` recorded for the search retry ladders; a breaker, except the -//! marked plain table read (`pipelined`). +//! `ScanReport` recorded for the search retry ladders. Every unranked table +//! read streams (`pipelined`); a ranked scan is a breaker, since its ladder +//! reruns it whole. use datafusion::physical_plan::metrics::{ExecutionPlanMetricsSet, MetricsSet}; use std::fmt; @@ -49,23 +50,24 @@ pub(crate) struct ScanExec { snapshot: Snapshot, catalog: Arc, runtime_filter: Option>, + /// The rows after which a streamed read sends what it gathered, below the + /// session's batch rows when a `Limit` sits over the scan's projection. + gather_rows: Option, properties: Arc, metrics: ExecutionPlanMetricsSet, } impl ScanExec { /// This scan under the runtime filter its parent fills, when the plan - /// marked one; a marked table read that ranks nothing then streams. + /// marked one. pub(crate) fn with_runtime_filter(mut self, filter: Option>) -> Self { self.runtime_filter = filter; - if let ScanSource::Table { .. } = &self.source { - let schema = self.schema(); - self.properties = if self.pipelines() { - streaming_properties(schema) - } else { - breaker_properties(schema) - }; - } + self + } + + /// This scan sending its gathered rows once it holds `rows` of them. + pub(crate) fn with_gather_rows(mut self, rows: usize) -> Self { + self.gather_rows = Some(rows.max(1)); self } @@ -98,7 +100,11 @@ impl ScanExec { mode, projection.as_ref(), )?; - breaker_properties(schema) + if pipelined::streams(mode) { + streaming_properties(schema) + } else { + breaker_properties(schema) + } } }; Ok(Self { @@ -111,6 +117,7 @@ impl ScanExec { snapshot, catalog, runtime_filter: None, + gather_rows: None, properties, metrics: ExecutionPlanMetricsSet::new(), }) @@ -215,8 +222,9 @@ impl ExecutionPlan for ScanExec { } ScanSource::Table { mode, report } => (mode.as_ref().clone(), Arc::clone(report)), }; - if let Some(slot) = self.runtime_filter.as_ref().filter(|_| self.pipelines()) { - return self.execute_pipelined(mode, slot.take(), ctx); + if self.pipelines() { + let filter = self.runtime_filter.as_ref().and_then(|slot| slot.take()); + return self.execute_pipelined(mode, filter, ctx); } let schema: SchemaRef = self.schema(); let type_name = self.type_name.clone(); diff --git a/crates/omnigraph/src/engine/operators/scan/pipelined.rs b/crates/omnigraph/src/engine/operators/scan/pipelined.rs index 080d983d1..b63425e4d 100644 --- a/crates/omnigraph/src/engine/operators/scan/pipelined.rs +++ b/crates/omnigraph/src/engine/operators/scan/pipelined.rs @@ -1,9 +1,13 @@ -//! The pipelined path of a marked plain table read: bounded Lance batches, filter -//! or none, each sieved by the `RuntimeFilter` the parent left, prefixed, -//! conformed and sent as it is read, so the scan never holds the table. +//! The pipelined path of every unranked table read: bounded Lance batches, +//! each sieved by the `RuntimeFilter` a marking parent left (a contains join), +//! prefixed, conformed, gathered up to the session's batch rows or the byte +//! target and sent, so the scan never holds the table, a consumer that stops +//! early (a `limit`) stops the read, and a table of many small fragments still +//! reaches its consumer in full-size batches. use std::sync::Arc; +use arrow_array::RecordBatch; use arrow_schema::SchemaRef; use datafusion::common::Result as DfResult; use datafusion::execution::TaskContext; @@ -20,20 +24,25 @@ use crate::engine::search::SearchMode; /// The pool owner of one streamed batch's hold, from its sieve to its send. const SCAN_BATCH: &str = "v2 scan batch"; +/// The pool owner of the Lance batch being sieved. +const SCAN_INPUT: &str = "v2 scan input"; + +/// Whether a table read under `mode` streams: it ranks nothing. +pub(super) fn streams(mode: &SearchMode) -> bool { + mode.bm25.is_none() && mode.nearest.is_none() +} impl ScanExec { - /// Whether the scan sends each sieved Lance batch as it is read: a marked - /// table read under no search mode (a ranked scan is never marked). + /// Whether the scan sends each Lance batch as it is read: every table read + /// under no search mode. A ranked scan stays a breaker: its overfetch + /// ladder reruns it whole. pub(super) fn pipelines(&self) -> bool { - self.runtime_filter.is_some() - && matches!( - &self.source, - ScanSource::Table { mode, .. } if mode.bm25.is_none() && mode.nearest.is_none() - ) + matches!(&self.source, ScanSource::Table { mode, .. } if streams(mode)) } - /// The marked table read as a pipeline over `stream_batches`, under the - /// filter the parent left in the slot (`None` when it left none). + /// The table read as a pipeline over `stream_batches`, under the filter a + /// marking parent left in the slot (`None` when it left none or the scan + /// is unmarked); only a marked scan counts what its filter read. pub(super) fn execute_pipelined( &self, mode: SearchMode, @@ -52,7 +61,11 @@ impl ScanExec { let mut work = WorkMemory::new(ctx, "ScanExec")?; work.set_metrics(self.metrics.clone()); work.metric("input_rows", 0); - scan_counters(&work, filter.is_some()); + let marked = self.runtime_filter.is_some(); + let gather_rows = self.gather_rows; + if marked { + scan_counters(&work, filter.is_some()); + } let stream = producer_stream( schema, Arc::new(work), @@ -70,7 +83,17 @@ impl ScanExec { ) .await .map_err(external)?; - stream_batches(read, filter.as_ref(), &binding, &declared, &memory, &sender).await + stream_batches( + read, + filter.as_ref(), + marked, + gather_rows, + &binding, + &declared, + &memory, + &sender, + ) + .await }, ); Ok(polled(&self.metrics, stream)) @@ -79,10 +102,17 @@ impl ScanExec { /// The batches of `read` under `declared`, each sieved by `filter` under its /// own `SCAN_BATCH` charge (a mixed selection's copy admitted before it is -/// built) and sent; an emptied batch is skipped, and the Lance batch let go. +/// built); an emptied batch is skipped, and the Lance batch let go. Kept rows +/// gather under one charge until they reach the session's batch rows (or the +/// `gather_rows` of a `Limit` over the scan) or the byte target and leave as +/// one batch: Lance yields a batch per fragment, and a per-batch consumer (a +/// traversal's index lookup) would otherwise pay once per fragment. `marked` scans record the runtime-filter counters, +/// unfiltered reads too. async fn stream_batches( read: NodeRead<'_>, filter: Option<&RuntimeFilter>, + marked: bool, + gather_rows: Option, binding: &str, declared: &SchemaRef, memory: &WorkMemory, @@ -97,8 +127,9 @@ async fn stream_batches( .await .map_err(external)?; let (_plan, mut stream) = memory.stream(plan)?; - let in_flight = memory.child("runtime filter input")?; + let in_flight = memory.child(SCAN_INPUT)?; let mut inert = false; + let mut gathered = Gathered::new(memory, gather_rows)?; while let Some(batch) = stream.next().await { let batch = batch?; in_flight.hold(&batch)?; @@ -106,7 +137,9 @@ async fn stream_batches( let kept = match filter { Some(filter) => filter.keep(&batch, &work, SCAN_BATCH, &mut inert)?, None => { - count_unsieved(&work, &batch); + if marked { + count_unsieved(&work, &batch); + } batch.clone() } }; @@ -127,9 +160,75 @@ async fn stream_batches( }; let prefixed = prefix_batch(&kept, binding).map_err(external)?; let batch = conform(prefixed, declared).map_err(external)?; - sender.send(batch, work).await?; + if gathered.push(batch)? { + gathered.send(declared, memory, sender).await?; + } } in_flight.release_work(); } - Ok(()) + gathered.send(declared, memory, sender).await +} + +/// Kept batches waiting to leave as one, held under their own `SCAN_BATCH` +/// charge until the send hands it to the consumer. +struct Gathered { + work: Arc, + batches: Vec, + rows: usize, + bytes: usize, + /// The session's batch rows, or the rows of a `Limit` over the scan. + row_target: usize, + byte_target: usize, +} + +impl Gathered { + fn new(memory: &WorkMemory, gather_rows: Option) -> DfResult { + let rows = memory.batch_rows(); + Ok(Self { + work: Arc::new(memory.child(SCAN_BATCH)?), + batches: Vec::new(), + rows: 0, + bytes: 0, + row_target: gather_rows.map_or(rows, |limit| limit.min(rows)), + byte_target: memory.batch_bytes(), + }) + } + + /// Hold `batch` and add it; `true` once the gathered rows reach the row + /// target or their bytes the byte target. + fn push(&mut self, batch: RecordBatch) -> DfResult { + self.work.hold(&batch)?; + self.rows += batch.num_rows(); + self.bytes = self.bytes.saturating_add(batch.get_array_memory_size()); + self.batches.push(batch); + Ok(self.rows >= self.row_target || self.bytes >= self.byte_target) + } + + /// Send what is gathered as one batch (concatenated under the charge, + /// which then holds only the result) and start again; nothing when empty. + async fn send( + &mut self, + declared: &SchemaRef, + memory: &WorkMemory, + sender: &BatchSender, + ) -> DfResult<()> { + if self.batches.is_empty() { + return Ok(()); + } + let next = Self { + work: Arc::new(memory.child(SCAN_BATCH)?), + batches: Vec::new(), + rows: 0, + bytes: 0, + row_target: self.row_target, + byte_target: self.byte_target, + }; + let gathered = std::mem::replace(self, next); + let batch = match <[RecordBatch; 1]>::try_from(gathered.batches) { + Ok([batch]) => batch, + Err(batches) => gathered.work.concat(declared, &batches)?, + }; + gathered.work.output(&batch)?; + sender.send(batch, gathered.work).await + } } diff --git a/crates/omnigraph/src/engine/push/executor.rs b/crates/omnigraph/src/engine/push/executor.rs index 377fdc2dc..3e57ff2cf 100644 --- a/crates/omnigraph/src/engine/push/executor.rs +++ b/crates/omnigraph/src/engine/push/executor.rs @@ -273,6 +273,7 @@ fn build_chain<'a>( | PhysicalNode::Projection { .. } | PhysicalNode::Aggregate { .. } | PhysicalNode::Sort { .. } + | PhysicalNode::HydrateColumns { .. } | PhysicalNode::Limit { .. }) => Err(ExecError::internal(format!( "`{}` is a read node; the engine's runner executes it, not the push pipeline", read.name() diff --git a/crates/omnigraph/src/engine/report/tests.rs b/crates/omnigraph/src/engine/report/tests.rs index bcdee1167..619e71a72 100644 --- a/crates/omnigraph/src/engine/report/tests.rs +++ b/crates/omnigraph/src/engine/report/tests.rs @@ -929,6 +929,57 @@ async fn a_ranked_scan_a_sort_a_projection_and_a_limit_build_one_operator_each() } } +/// A top-k over ten docs whose output alone reads `text`: the scan reads the +/// narrow columns and the row address, and one `HydrateExec` above the +/// limit takes the two kept rows' text by address. +#[tokio::test] +async fn a_hydrating_top_k_builds_one_operator_above_its_limit() { + let dir = tempfile::tempdir().unwrap(); + let db = doc_graph(&dir).await; + let source = r#"query two_texts() { + match { $d: Doc } + return { $d.slug, $d.text } + order { $d.slug desc } + limit 2 + }"#; + let (result, run) = captured(db.query( + ReadTarget::branch("main"), + source, + "two_texts", + &ParamMap::new(), + )) + .await; + let run = run.expect("the query ran on engine v2"); + let batch = result.unwrap().concat_batches().unwrap(); + let texts: Vec<&str> = batch + .column(1) + .as_any() + .downcast_ref::() + .unwrap() + .iter() + .map(Option::unwrap) + .collect(); + assert_eq!(texts, ["needle 9", "needle 8"]); + assert_covers(&run); + let hydrate = node_id(&run.plan, |node| { + matches!(node, PhysicalNode::HydrateColumns { .. }) + }); + assert_eq!( + hydrate, + run.plan.root(), + "the hydration sits above the limit" + ); + assert_eq!(operator(&run, hydrate), "HydrateExec"); + assert_eq!(row(&run, hydrate).attempts[0].actual_rows, 2); + let returns = node_id(&run.plan, |node| { + matches!(node, PhysicalNode::Projection { .. }) + }); + assert_eq!(row(&run, returns).attempts[0].actual_rows, 10, "every doc"); + for row in run.report.rows() { + assert_eq!(row.status, RowStatus::Executed, "{row:?}"); + } +} + #[tokio::test] async fn a_rank_fuse_is_one_operator_over_two_arm_scans() { let dir = tempfile::tempdir().unwrap(); diff --git a/crates/omnigraph/src/engine/scan.rs b/crates/omnigraph/src/engine/scan.rs index 85d0ccad9..1eed24322 100644 --- a/crates/omnigraph/src/engine/scan.rs +++ b/crates/omnigraph/src/engine/scan.rs @@ -9,6 +9,7 @@ use crate::table_store::{ScanTuning, TableStore}; use arrow_schema::SchemaRef; use datafusion::prelude::{Expr, col, lit as df_lit}; use datafusion::scalar::ScalarValue; +use lance_core::ROW_ADDR; use lance_index::scalar::FullTextSearchQuery; /// `id IN (ids)` as one structured DataFusion `Expr` — the scan-pushdown @@ -34,6 +35,12 @@ pub(crate) fn id_in_list_expr(ids: &[String], id_col: &str) -> datafusion::prelu /// Lance batches a pipelined read decodes ahead, which the pool does not see. const PIPELINED_READAHEAD: usize = 2; +/// Bytes Lance may hold fetched and not yet decoded for one pipelined read, +/// which the pool does not see either. Lance's default is 32 MiB per I/O +/// thread (2 GiB on a cloud store), read ahead of a consumer that may stop +/// after its first batch; two 32 MiB pages keep its throughput. +const PIPELINED_IO_BUFFER_BYTES: u64 = 64 * 1024 * 1024; + /// One node scan resolved before any Lance read: the dataset, the pushed /// filter (the literal filters, a gate's eligible set, a BM25 filter's member /// ids), the hoisted full-text query and the columns it reads. @@ -175,8 +182,9 @@ impl<'n> NodeRead<'n> { } /// The Lance plan of this read: the projection, the pushed filter as a - /// prefilter, the `(rows, bytes)` batch override and bounded readahead of - /// a pipelined read, the full-text query, then `configure` (nearest). + /// prefilter, the `(rows, bytes)` batch override, bounded readahead and + /// I/O buffer of a pipelined read, the full-text query, then `configure` + /// (nearest). pub(super) fn plan( &self, pipelined_batch: Option<(usize, usize)>, @@ -195,6 +203,7 @@ impl<'n> NodeRead<'n> { scanner.batch_size(rows); scanner.batch_size_bytes(bytes as u64); scanner.batch_readahead(PIPELINED_READAHEAD); + scanner.io_buffer_size(PIPELINED_IO_BUFFER_BYTES); } if let Some(fts_query) = &self.fts_query { scanner @@ -206,9 +215,11 @@ impl<'n> NodeRead<'n> { } } -/// Scan a node type under the supplied projection, filters and search mode. -/// Apply filters before search ranking, retain score columns, and widen an -/// underfilled ANN scan according to its reported probe outcomes. +/// The breaker read of a ranked scan: scan a node type under the supplied +/// projection, filters and search mode, apply filters before search ranking, +/// retain score columns, and widen an underfilled ANN scan according to its +/// reported probe outcomes. An unranked read streams instead +/// (`operators::scan::pipelined`). pub(super) async fn execute_node_scan( type_name: &str, variable: &str, @@ -569,7 +580,8 @@ pub(super) fn conjoin_fts_queries( pub(super) struct ScanColumns<'n> { pub(super) has_blobs: bool, pub(super) non_blob_cols: Vec<&'n str>, - /// `_distance` under a nearest target, `_score` under a text search. + /// `_distance` under a nearest target, `_score` under a text search, then + /// `_rowaddr` when the plan defers a return column to `HydrateColumns`. pub(super) search_cols: Vec<&'static str>, /// The plan's projection (`projection_pushdown`) plus the identity, the /// key and the search columns; `None` reads every non-blob column. @@ -608,13 +620,16 @@ impl<'n> ScanColumns<'n> { .filter(|f| !node_type.blob_properties.contains(f.name())) .map(|f| f.name().as_str()) .collect(); - let mut search_cols: Vec<&'static str> = Vec::with_capacity(2); + let mut search_cols: Vec<&'static str> = Vec::with_capacity(3); if search.distance { search_cols.push("_distance"); } if search.score { search_cols.push("_score"); } + if binding_columns.is_some_and(|NeededColumns(columns)| columns.contains(ROW_ADDR)) { + search_cols.push(ROW_ADDR); + } let pruned_cols: Option> = binding_columns.map(|NeededColumns(columns)| { non_blob_cols .iter() @@ -653,11 +668,14 @@ impl<'n> ScanColumns<'n> { }) .map(|f| f.as_ref().clone()) .collect(); - fields.extend( - self.search_cols - .iter() - .map(|col| Field::new(*col, DataType::Float32, true)), - ); + fields.extend(self.search_cols.iter().map(|col| { + let data_type = if *col == ROW_ADDR { + DataType::UInt64 + } else { + DataType::Float32 + }; + Field::new(*col, data_type, true) + })); RecordBatch::new_empty(Arc::new(Schema::new(fields))) } } diff --git a/crates/omnigraph/src/engine/typed_value.rs b/crates/omnigraph/src/engine/typed_value.rs index 4cc145f99..18c62ae1c 100644 --- a/crates/omnigraph/src/engine/typed_value.rs +++ b/crates/omnigraph/src/engine/typed_value.rs @@ -349,7 +349,13 @@ pub(super) fn check_output_schema(actual: &Schema, declared: &Schema, hidden: bo let visible = actual .fields() .iter() - .filter(|field| !hidden || !field.name().starts_with('~')) + .filter(|field| { + !hidden + || !(field.name().starts_with('~') + || field + .name() + .starts_with(omnigraph_planner::ROW_ADDRESS_PREFIX)) + }) .count(); if visible != declared.fields().len() { return Err(OmniError::manifest_internal( @@ -687,6 +693,7 @@ pub(super) fn validate_plan_values( PhysicalNode::SortMergeJoin { .. } | PhysicalNode::HashJoin { .. } | PhysicalNode::HydrateByAddress { .. } + | PhysicalNode::HydrateColumns { .. } | PhysicalNode::RowCompare { .. } | PhysicalNode::ClassifyThreeWay { .. } | PhysicalNode::Limit { .. } diff --git a/crates/omnigraph/src/table_store.rs b/crates/omnigraph/src/table_store.rs index 1c4db6b20..c44f0787b 100644 --- a/crates/omnigraph/src/table_store.rs +++ b/crates/omnigraph/src/table_store.rs @@ -248,6 +248,11 @@ impl ScanTuning<'_> { self } + pub(crate) fn io_buffer_size(&mut self, bytes: u64) -> &mut Self { + self.scanner.io_buffer_size(bytes); + self + } + pub(crate) fn prefilter(&mut self, should_prefilter: bool) -> &mut Self { self.scanner.prefilter(should_prefilter); self diff --git a/crates/omnigraph/tests/engine_v2.rs b/crates/omnigraph/tests/engine_v2.rs index 9eafd2f23..e84dc4000 100644 --- a/crates/omnigraph/tests/engine_v2.rs +++ b/crates/omnigraph/tests/engine_v2.rs @@ -282,8 +282,10 @@ async fn tiny_pool_refuses_a_large_fan_out_expand_with_the_typed_resource_error( } /// 100 hubs sharing 1,000 leaves: 100,000 pairs, about 240 B of accounting -/// each when retained as `ExpandedPairs` (24 MB, refused by a 16 MiB pool +/// each when retained as `ExpandedPairs` (24 MB, refused by a 20 MiB pool /// before the single hop streamed); per input batch, every output slice fits. +/// The pool leaves headroom over the hop's own neighbour map for the source +/// scan's read-ahead batches, which stream beside it. #[tokio::test] #[serial] async fn expand_streams_a_single_hop_under_a_pool_the_pairs_would_not_fit() { @@ -323,14 +325,14 @@ async fn expand_streams_a_single_hop_under_a_pool_the_pairs_would_not_fit() { let answered = with_query_memory_probes( probes.clone(), with_query_memory_limit( - 16 * 1024 * 1024, + 20 * 1024 * 1024, query_main(&v2, HOP_QUERIES, "one_hop", ¶ms(&[])), ), ) .await .unwrap_or_else(|error| { panic!( - "a streamed single hop fits a 16 MiB pool: {error}; refusals={:?}", + "a streamed single hop fits a 20 MiB pool: {error}; refusals={:?}", probes.refusals() ) }); @@ -535,6 +537,57 @@ async fn explain_omits_a_dataset_version_absent_from_the_snapshot() { assert_eq!(absent_scans, 1); } +/// Twenty one-row commits leave twenty fragments, and Lance reads a batch +/// per fragment: each streamed scan gathers them into one batch before its +/// consumer sees it, so a per-batch consumer (the hop's index lookup) runs +/// once whatever the fragment layout. +#[tokio::test] +#[serial] +async fn a_streamed_scan_gathers_small_fragments_into_one_batch() { + use omnigraph::instrumentation::{QueryMemoryProbes, with_query_memory_probes}; + + let dir = tempfile::tempdir().unwrap(); + let db = session( + Omnigraph::init(dir.path().to_str().unwrap(), FAN_OUT_SCHEMA) + .await + .unwrap(), + ); + for n in 0..20 { + let mut rows = vec![format!( + r#"{{"type":"Person","data":{{"name":"p{n:02}"}}}}"# + )]; + if n > 0 { + rows.push(format!( + r#"{{"edge":"Knows","from":"p{n:02}","to":"p00","data":{{}}}}"# + )); + } + db.load_jsonl(&rows.join("\n"), LoadMode::Append) + .await + .unwrap(); + } + let v2 = with_setting(&db, "engine", "v2"); + + let probes = QueryMemoryProbes::default(); + let answered = with_query_memory_probes( + probes.clone(), + query_main(&v2, HOP_QUERIES, "one_hop", ¶ms(&[])), + ) + .await + .unwrap(); + assert_eq!(answered.num_rows(), 19); + let scans: Vec = probes + .execution_metrics() + .into_iter() + .filter(|metric| metric.operator == "ScanExec") + .map(|metric| metric.values.get("output_batches").copied().unwrap_or(0)) + .collect(); + assert!(!scans.is_empty(), "the query scans Person"); + assert!( + scans.iter().all(|batches| *batches <= 1), + "a streamed scan sends its twenty fragments as one batch: {scans:?}" + ); +} + const HOP_QUERIES: &str = r#" query one_hop() { match { diff --git a/crates/omnigraph/tests/engine_v2_memory.rs b/crates/omnigraph/tests/engine_v2_memory.rs index c14d2a075..8254ad251 100644 --- a/crates/omnigraph/tests/engine_v2_memory.rs +++ b/crates/omnigraph/tests/engine_v2_memory.rs @@ -1266,18 +1266,180 @@ fn join_counter(probes: &QueryMemoryProbes, name: &str) -> Vec { counter(probes, "ContainsJoinExec", name) } -/// 64 MiB of passage text under a 48 MiB pool: the unfiltered Passage scan -/// refuses, the filtered one holds a batch at a time and the two citing rows. -/// Rust, not `.gqt`: rows cannot show the pool's cap or the refusing owner. +/// 64 MiB of payload under a 16 MiB pool: an unordered `limit 1` and a top-k +/// read of the wide column answer. The limit stops the streamed scan of the +/// narrow columns after a few batches, the top-k sorts them, and each fetches +/// its one kept row's payload by row address. The pool is released. GQT cannot set the +/// pool or read the operators' counters. The scale twin is +/// `cases_slow/v2/wide_column_scan_limit_answers.gqt`. #[tokio::test] #[serial] -async fn a_text_contains_join_answers_where_the_unfiltered_scan_refuses() { +async fn a_wide_column_read_under_a_limit_follows_its_result() { + let dir = tempfile::tempdir().unwrap(); + let v2 = graph_fixture(&dir, 4_096, 16 * 1024).await; + let limit = 16 * MIB; + + let any = r#"query any_payload() { + match { $p: Person } + return { $p.payload } + limit 1 + }"#; + let probes = QueryMemoryProbes::default(); + let result = with_query_memory_probes( + probes.clone(), + with_query_memory_limit(limit, query_main(&v2, any, "any_payload", ¶ms(&[]))), + ) + .await + .unwrap_or_else(|error| panic!("{error}; refusals={:?}", probes.refusals())); + assert_eq!(result.num_rows(), 1); + let emitted: usize = probes + .execution_metrics() + .iter() + .filter(|metric| metric.operator == "ScanExec") + .map(|metric| metric.output_rows) + .sum(); + assert!( + emitted < 4_097, + "the limit must stop the scan before it reads the type: {emitted} rows" + ); + assert_eq!(counter(&probes, "HydrateExec", "hydrated_rows"), [1]); + assert_released(&probes); + + let top = r#"query greatest() { + match { $p: Person } + return { $p.name, $p.payload } + order { $p.name desc } + limit 1 + }"#; + let probes = QueryMemoryProbes::default(); + let result = with_query_memory_probes( + probes.clone(), + with_query_memory_limit(limit, query_main(&v2, top, "greatest", ¶ms(&[]))), + ) + .await + .unwrap_or_else(|error| panic!("{error}; refusals={:?}", probes.refusals())); + let batch = result.concat_batches().unwrap(); + assert_eq!(batch.num_rows(), 1); + assert_eq!( + batch + .column(0) + .as_any() + .downcast_ref::() + .unwrap() + .value(0), + "leaf04095" + ); + assert_eq!( + batch + .column(1) + .as_any() + .downcast_ref::() + .unwrap() + .value(0) + .len(), + 16 * 1024 + ); + assert_eq!(counter(&probes, "HydrateExec", "hydrated_rows"), [1]); + assert_released(&probes); +} + +/// A join repeats a row once per output row, and hydration copies its +/// values once per repetition: a `limit 108` over a hub's 512 edges returns +/// copies of the hub's 40 KiB payload under a 16 MiB pool, and no chunk holds +/// more than `hydrate_chunk_bytes` (2 MiB) of fetched rows and copies. +/// Sixteen narrow sources come first, so the seed plans a window far wider +/// than the hub's rows allow. GQT cannot set the pool or read the operator's +/// gauge. +#[tokio::test] +#[serial] +async fn hydrated_copies_of_a_joined_row_stay_within_the_chunk_bound() { + let dir = tempfile::tempdir().unwrap(); + let db = session( + Omnigraph::init(dir.path().to_str().unwrap(), GRAPH_SCHEMA) + .await + .unwrap(), + ); + let person = |name: String, payload: String| { + serde_json::json!({"type":"Person", "data":{"name":name, "payload":payload}}).to_string() + }; + let knows = |from: String, to: String| { + serde_json::json!({"edge":"Knows", "from":from, "to":to}).to_string() + }; + let mut lines: Vec = (0..16) + .map(|narrow| person(format!("a{narrow:02}"), String::new())) + .collect(); + lines.push(person("hub".into(), "h".repeat(40 * 1024))); + lines.extend((0..512).map(|leaf| person(format!("leaf{leaf:03}"), String::new()))); + lines.extend((0..16).map(|narrow| knows(format!("a{narrow:02}"), format!("leaf{narrow:03}")))); + lines.extend((0..512).map(|leaf| knows("hub".into(), format!("leaf{leaf:03}")))); + db.load_jsonl(&lines.join("\n"), LoadMode::Overwrite) + .await + .unwrap(); + let v2 = with_setting(&db, "engine", "v2"); + let fanout = r#"query fanout() { + match { $a: Person $a knows $b } + return { $a.name, $a.payload } + limit 108 + }"#; + let probes = QueryMemoryProbes::default(); + let result = with_query_memory_probes( + probes.clone(), + with_query_memory_limit(16 * MIB, query_main(&v2, fanout, "fanout", ¶ms(&[]))), + ) + .await + .unwrap_or_else(|error| panic!("{error}; refusals={:?}", probes.refusals())); + let batch = result.concat_batches().unwrap(); + assert_eq!(batch.num_rows(), 108); + let column = |index: usize| { + batch + .column(index) + .as_any() + .downcast_ref::() + .unwrap() + .clone() + }; + let (names, payloads) = (column(0), column(1)); + let mut hub_rows = 0; + for row in 0..batch.num_rows() { + let width = if names.value(row) == "hub" { + hub_rows += 1; + 40 * 1024 + } else { + 0 + }; + assert_eq!(payloads.value(row).len(), width, "{}", names.value(row)); + } + assert!(hub_rows >= 92, "{hub_rows} of the rows are the hub's"); + assert_eq!(counter(&probes, "HydrateExec", "hydrated_rows"), [108]); + let peak = counter(&probes, "HydrateExec", "peak_chunk_bytes"); + assert_eq!(peak.len(), 1); + assert!( + peak[0] > 40 * 1024 && peak[0] as u64 <= omnigraph_planner::hydrate_chunk_bytes(16 * MIB), + "a chunk held {} bytes", + peak[0] + ); + assert!(probes.refusals().is_empty(), "{:?}", probes.refusals()); + assert_released(&probes); +} + +/// 64 MiB of passage text under a 48 MiB pool: the Passage scan streams a +/// batch at a time with or without needles, so the plain filtered product +/// and the contains join both answer; only the join's marked scan sieves, +/// and only a marked scan records the runtime-filter counters. +/// Rust, not `.gqt`: rows cannot show the pool's cap or the scan's counters. +#[tokio::test] +#[serial] +async fn a_passage_table_the_pool_cannot_hold_streams_with_and_without_needles() { let dir = tempfile::tempdir().unwrap(); let v2 = citation_fixture(&dir, 16_384, 4_096).await; let limit = 48 * MIB; + let cited = [ + ("mN-0001".to_string(), "p000007".to_string()), + ("mN-0003".to_string(), "p000011".to_string()), + ]; let probes = QueryMemoryProbes::default(); - let error = with_query_memory_probes( + let result = with_query_memory_probes( probes.clone(), with_query_memory_limit( limit, @@ -1285,15 +1447,12 @@ async fn a_text_contains_join_answers_where_the_unfiltered_scan_refuses() { ), ) .await - .unwrap_err(); - assert_memory_refusal(error, limit); + .unwrap_or_else(|error| panic!("{error}; refusals={:?}", probes.refusals())); + assert_eq!(cited_pairs(&result.concat_batches().unwrap()), cited); + assert!(probes.refusals().is_empty(), "{:?}", probes.refusals()); assert!( - probes - .refusals() - .iter() - .any(|owner| owner == "v2 scan attempt"), - "the unfiltered Passage scan must refuse its own collection: {:?}", - probes.refusals() + scan_counter(&probes, "runtime_filter_rows_read").is_empty(), + "an unmarked scan records no runtime-filter counters" ); assert_released(&probes); @@ -1304,13 +1463,7 @@ async fn a_text_contains_join_answers_where_the_unfiltered_scan_refuses() { ) .await .unwrap_or_else(|error| panic!("{error}; refusals={:?}", probes.refusals())); - assert_eq!( - cited_pairs(&result.concat_batches().unwrap()), - [ - ("mN-0001".to_string(), "p000007".to_string()), - ("mN-0003".to_string(), "p000011".to_string()), - ] - ); + assert_eq!(cited_pairs(&result.concat_batches().unwrap()), cited); assert!(probes.refusals().is_empty(), "{:?}", probes.refusals()); assert_eq!(scan_counter(&probes, "runtime_filter_rows_read"), [16_384]); assert_eq!( @@ -1464,6 +1617,7 @@ async fn empty_numbers_pair_with_the_shared_passage_batch() { /// The scan's sieve and the join's needle rows share the fill's one automaton, /// so a 14.5 MiB pool that fits one build of the 102 numbers' automaton pairs /// through it while the scan, every batch kept and its channel full, holds it. +/// The 2 MB of passages are several of the scan's byte-sized batches. #[tokio::test] #[serial] async fn the_scan_and_the_join_share_one_matcher() { @@ -1481,7 +1635,7 @@ async fn the_scan_and_the_join_share_one_matcher() { long.iter() .map(|(mid, number)| (mid.as_str(), number.as_str())), ); - let v2 = citation_graph(&dir, &matters, 2_048, 16).await; + let v2 = citation_graph(&dir, &matters, 2_048, 1_024).await; let probes = QueryMemoryProbes::default(); let result = with_query_memory_probes( probes.clone(), @@ -1571,7 +1725,8 @@ fn cited_by(conjunct: &str, extra: &str) -> String { /// Rows of `(key, text)`: matter ids with numbers, or passage ids with texts. type KeyedTexts = Vec<(String, Option)>; -/// 1,200 passages over more than one Lance batch and numbers that nest +/// 20,000 passages, whose sieved rows still fill more than one scan batch of +/// the session's 8,192 rows, and numbers that nest /// (`16`, `016`, `1016`, `x1016`), repeat, equal a whole text, hold `é` or /// `日本`, are empty or null; texts are sometimes empty or null. fn generated_citations() -> (KeyedTexts, KeyedTexts) { @@ -1593,7 +1748,7 @@ fn generated_citations() -> (KeyedTexts, KeyedTexts) { matters.push((mid.to_string(), Some(number.to_string()))); } matters.push(("m-null".to_string(), None)); - let passages = (0..1_200) + let passages = (0..20_000) .map(|i: usize| { let text = match i { 600 => Some("whole text of one passage".to_string()), @@ -1618,7 +1773,7 @@ fn generated_citations() -> (KeyedTexts, KeyedTexts) { )) } }; - (format!("p{i:04}"), text) + (format!("p{i:05}"), text) }) .collect(); (matters, passages) @@ -1626,7 +1781,7 @@ fn generated_citations() -> (KeyedTexts, KeyedTexts) { /// The contains join equals its `or` form (the filtered cross join) and /// `str::contains` over every pair, with and without the empty number. Rust, -/// not `.gqt`: generated inputs at Lance-batch scale. +/// not `.gqt`: generated inputs at scan-batch scale. #[tokio::test] #[serial] async fn a_contains_join_and_its_cross_join_agree_on_generated_texts_over_several_batches() { diff --git a/crates/omnigraph/tests/engine_v2_plan_replay.rs b/crates/omnigraph/tests/engine_v2_plan_replay.rs index 4e1134bbb..9d64a0181 100644 --- a/crates/omnigraph/tests/engine_v2_plan_replay.rs +++ b/crates/omnigraph/tests/engine_v2_plan_replay.rs @@ -1099,6 +1099,38 @@ async fn a_fusion_replays() { assert_eq!(result.len(), 3); } +/// A top-k whose output alone reads `text` replays with its `HydrateColumns` +/// node: the bound plan carries the hydrated binding and its table, and the +/// replay takes the same rows' text by address from the pinned snapshot. +#[tokio::test] +async fn a_hydrating_top_k_replays() { + let dir = tempfile::tempdir().unwrap(); + let db = docs(&dir).await; + let source = r#"query two_texts() { + match { $d: Doc } + return { $d.slug, $d.text } + order { $d.slug desc } + limit 2 + }"#; + let Replayed { result, plan, rows } = + replayed(&db, source, "two_texts", &ParamMap::new()).await; + assert_eq!( + result, + [ + serde_json::json!({"d.slug": "d09", "d.text": "needle 9"}), + serde_json::json!({"d.slug": "d08", "d.text": "needle 8"}), + ] + ); + assert_eq!( + plan.plan.node(plan.plan.root()).map(|node| node.name()), + Some("HydrateColumns") + ); + assert!( + rows.iter().any(|row| row["operator"] == "HydrateExec"), + "{rows:#?}" + ); +} + #[tokio::test] async fn the_skip_shapes_and_the_aggregate_replay() { let dir = tempfile::tempdir().unwrap(); diff --git a/docs/dev/execution.md b/docs/dev/execution.md index e1612f9b9..fa49518a3 100644 --- a/docs/dev/execution.md +++ b/docs/dev/execution.md @@ -233,6 +233,26 @@ before duplicating destination rows per edge. Limits remain above destination hydration and filtering; no expansion cap can discard candidates before those operators decide which rows survive. +Pass 6 (`late_materialization`) also runs on a query plan +(`optimizer::materialize_returns_late`). A return column that nothing but the +output reads (a bare `$b.property` item: not a key, the identity, a `Blob`, a +sort key, a column a filter, join, traversal or aggregate reads, or a return +the sort orders by alias) leaves its binding's physical scan, which reads +`_rowaddr` in its place; a `HydrateColumns` node above the root `Limit` names +each such binding's table and columns, and the return projection below carries +the binding's row address as `^binding` (`ROW_ADDRESS_PREFIX`) instead of the +column. The pass fires when the root is a `Limit` of `K > 0` rows, the plan +has no `RankFuse` or `Aggregate`, and the binding's scan is unique and +unranked; a binding whose scan row estimate (the manifest row count, one row +under a key equality) is at most `HYDRATE_ROW_RATIO` (4) times `K` keeps its +columns. A scan below a limit reads more rows than the limit keeps whenever a +sort, a filter or a traversal sits between them, and even a bare scan reads +ahead of a consumer that stops early: Lance schedules I/O up to the scan's +I/O buffer before the first batch is consumed. The logical plan keeps the full projection; the physical scan's is +the narrow one. Row addresses are safe to fetch by because one query reads +one pinned snapshot: the address read by the scan is the row's physical +location in that same table version. + `aggregate_pushdown` replaces a direct unfiltered `count($var)` over one scan with a `MetadataCount` leaf. Every return expression must count that same binding; property counts, grouping, filters, traversal, joins and search @@ -257,7 +277,7 @@ by `engine/scan.rs`, `engine/graph.rs`, `engine/expr.rs` and | `PhysicalNode` | Operator | Provider | |---|---|---| | `MetadataCount` | `MetadataCountExec`: exact live-row count from the pinned dataset, with the single output row charged to the query pool | omnigraph | -| `Scan` | `ScanExec`: `execute_node_scan` with the projection and pushed filters read off the `ScanSpec`, the schema declared before running. A `ranked` scan runs under a `SearchMode` built from its `RankedAccess` (the index, the ranked property, the query argument), the bound plan's value table (the query vector by node id, the parameters) and the pass's `Pass` (the overfetch rung, the gate's eligible set), so Lance ranks while scanning and appends `_distance` or `_score`. A scan whose `ScanSpec` carries a `runtime_filter` marker holds a `RuntimeFilterSlot` its `ContainsJoin` fills before the scan executes, and takes the closed `RuntimeFilter` enum from it (`TextContainsAny` its one variant). A marked plain table read (no `ranked` access, so no search mode) is a pipeline operator: with a filter or without one (an inert scan too) it reads Lance batches of at most `lance_batch_rows(batch_bytes)` rows under the byte target `batch_bytes` (Lance 11 decodes whichever bound is smaller; `LANCE_DEFAULT_BATCH_SIZE` replaces the row count), `PIPELINED_READAHEAD` (2) batches decoded ahead outside the pool; it holds each batch as `runtime filter input`, sieves it (a batch kept whole or emptied is a slice of itself; a mixed selection is copied only once the batch's `v2 scan batch` charge admits the copy) and releases the input, and the kept batch leaves as it is read, held as `v2 scan batch` only until the join takes it, so the table is never held whole; `runtime_filter_rows_read` counts every row the scan reads, an inert scan's too; every other scan (a ranked scan and its ANN ladder, a dependent scan, an unmarked table read) is a breaker through `execute_node_scan`, and both build the Lance scanner through one `NodeRead` | omnigraph | +| `Scan` | `ScanExec`: `execute_node_scan` with the projection and pushed filters read off the `ScanSpec`, the schema declared before running. A `ranked` scan runs under a `SearchMode` built from its `RankedAccess` (the index, the ranked property, the query argument), the bound plan's value table (the query vector by node id, the parameters) and the pass's `Pass` (the overfetch rung, the gate's eligible set), so Lance ranks while scanning and appends `_distance` or `_score`. A scan whose `ScanSpec` carries a `runtime_filter` marker holds a `RuntimeFilterSlot` its `ContainsJoin` fills before the scan executes, and takes the closed `RuntimeFilter` enum from it (`TextContainsAny` its one variant). Every unranked table read (no `ranked` access, so no search mode), marked or not, is a pipeline operator (`operators/scan/pipelined.rs`): it reads Lance batches of at most `lance_batch_rows(batch_bytes)` rows under the byte target `batch_bytes` (Lance 11 decodes whichever bound is smaller; `LANCE_DEFAULT_BATCH_SIZE` replaces the row count), `PIPELINED_READAHEAD` (2) batches decoded ahead and at most `PIPELINED_IO_BUFFER_BYTES` (64 MiB) fetched ahead, both outside the pool; it holds each batch as `v2 scan input`, sieves it when a join left a filter (a batch kept whole or emptied is a slice of itself; a mixed selection is copied only once the batch's `v2 scan batch` charge admits the copy) and releases the input, and kept batches gather under one `v2 scan batch` charge until they reach the session's batch rows (the rows of a `Limit` directly over the scan's projection, when one is there) or the byte target, then leave as one batch held until the consumer takes it: Lance reads a batch per fragment, and a per-batch consumer (an indexed hop's lookup) would otherwise pay once per fragment. The table is never held whole, and a consumer that stops (a `Limit`) stops the read; only a marked scan records `runtime_filter_rows_read` (every row it reads, an inert scan's too), `runtime_filter_rows_dropped` and `runtime_filter_inert`. A ranked scan and its ANN ladder is a breaker through `execute_node_scan`, which collects its batches; a dependent scan reads one slice of its input at a time; all of them build the Lance scanner through one `NodeRead`, and read `_rowaddr` when the plan defers a return column of their binding | omnigraph | | `HashJoin` | `HashJoinExec`: the build (a table `ScanExec` of the destination with the pushed filters and projection) drained under the query pool and hashed on `.`, the traversal (`probe`) joined in its own order, one `take` per side per probe batch, output charged as `hash join output`; the declared `id_lookup` fallback is a branch of the same operator (below); the first probe batch is read before the build, so an empty probe executes nothing on the build | omnigraph | | `RankFuse` | `RankFuseExec` over its two arm subtrees, each lowered once with its own ranked `Scan`; the operator ranks each arm by its score column, the fused binding's id and planner-declared downstream row keys before fusing. Selected edge keys place concrete type before id. Fusion still identifies and scores the fused node by its id; body `fuse_arms` | omnigraph | | `CrossJoin` | `CrossJoinExec`: the left input collected under the query pool, every left row paired with each right batch, output charged as `cross join output`; an empty left executes nothing on the right. The node's `filters` (conjuncts over both bindings) run in the join; every pair goes through one `PairBuffer`, which charges each right batch's row-size scratch before it grows it, filters the held pairs into a kept batch when it holds the session's batch size of pairs, when their estimated bytes reach the producer's batch bytes, and at each right batch's end, and sends the kept batches as one output once their rows reach the batch size or their bytes the batch bytes (the rest at the end), so an output exceeds the batch size by at most one kept batch | omnigraph | @@ -265,7 +285,8 @@ by `engine/scan.rs`, `engine/graph.rs`, `engine/expr.rs` and | `Filter` | `FilterExec`: every `IRFilter` of the node as one conjunction over the wide batch (`evaluate_filter`), so a filter over two bindings reads two columns | omnigraph | | `Expand` | `ExpandExec`: an unbudgeted named unbound single hop uses one vectorized walk per input batch (`operators/single_hop.rs`); unbudgeted named multi-hop drains its input into the BFS breaker `execute_expand`. Budgeted expansions, including selected multi-hop, consume fixed source windows through `SourceWindows`. Bound edges use the bounded pair producer, spillable pair ordering and incremental hydration within each input batch or budgeted source window. Output chunks contain at most 256 rows (`expand_pairs` counts pairs handed on); dropping the stream at a downstream `Limit` cancels the producer | omnigraph | | `AntiJoin` | `AntiJoinMaskExec` over the outer plan and the lowered inner plan: the bulk CSR degree mask when the predicate counts rows and the inner is one single-hop, filter-free, unbound expand over the `OuterReference`; else the outer rows are tagged, the inner plan runs over `OuterReferenceExec` under the same `TaskContext`, and `SubqueryAggregate` folds the tagged inner rows per outer row and applies the predicate | omnigraph | -| `Projection` | `ProjectionExec` over `GqProjectionExpr` per return expression, output charged as `projection output`; when a `Sort` consumes it, every column the sort reads and every declared tie-break metadata column follow the return columns under the hidden prefix `~`, which the sort drops | omnigraph | +| `Projection` | `ProjectionExec` over `GqProjectionExpr` per return expression, output charged as `projection output`; when a `Sort` consumes it, every column the sort reads and every declared tie-break metadata column follow the return columns under the hidden prefix `~`, which the sort drops; under a `HydrateColumns` it skips the deferred return items and carries each deferred binding's `_rowaddr` as `^binding` | omnigraph | +| `HydrateColumns` | `HydrateExec` (`operators/hydrate.rs`): per input batch, in chunks planned from the costliest output row seen (its share of the fetched rows and its copy; four rows first), the distinct `^binding` addresses of each binding, sorted, taken from its pinned table with Lance's `TakeBuilder::try_new_from_addresses` and the deferred columns' projection (an address the table does not hold is an integrity error), then copied to every output row that names them through `WorkMemory::take_once`, which admits each copy (one per output row, so a row a join repeats is copied per repetition; a null address costs a null row) as `hydrate output` before Arrow builds it. The chunk bound is `hydrate_chunk_bytes(pool)` (an eighth of the query pool, the node's declared `retained_limit`) over the fetched rows and their copies: fetched rows over it are retaken at half the rows, and a chunk whose copies would exceed it keeps a prefix halved until they fit, the rest starting the next chunk. A chunk is charged as `hydrate chunk`, keeps only its output once built, and is sent as it is built; `hydrated_rows` counts the rows and the `peak_chunk_bytes` gauge records the most one chunk held | omnigraph | | `Sort` | `SortExec`: with a `fetch`, a streaming top-k (every input batch merged into the retained best `fetch` rows and released, so the pool holds one batch and `fetch` rows at a time); without one, the whole input held under the query pool (no spill) and sorted once. Keys are `lexsort_to_indices` over the node's `order_by`, `nulls_first = !descending`, then the metadata columns of the node's declared `tiebreak`, ascending; the `~` columns are dropped on the way out. The planner (`optimizer::sort_tiebreak`) declares identity columns in binding-name order, with selected bound-edge type before its ID. Each metadata key already explicitly ordered is omitted independently; group rows and a `return` whose every expression is an order key need no hidden ties; `projection_pushdown` reads an id only for a declared tie-break, a traversal, a dependent scan, an anti-join, a ranked scan or an expression naming `@id`. The planner writes a search order's score key (`$d._score desc`, `$d._distance asc`) first and the query's plain keys after it, with the limit as `fetch`; a fusion plans no `Sort`, and an aggregate under a search order plans none | omnigraph | | `Limit` (`Page` in explain JSON) | `LimitExec`: passes batches and cuts the last one at the bound; a limit of zero executes nothing below it | omnigraph | | `Aggregate` | `AggregateExec` (`Single`) with the group keys and aggregate arguments as `GqProjectionExpr`s over the wide batch (the compiler stores `AggSignature`, the planner selects `AggregateSpec`, and lowering executes it; integer `sum` uses a `Decimal128(38,0)` accumulator and converts the total once to `Float64`; `avg` and floating-point `sum` use `Float64`); `count($v)` counts the identity column; DataFusion emits the group keys before the aggregates, and `run_plan` puts the collected result back in return order (`lower::in_order`) | DataFusion | @@ -310,12 +331,14 @@ Every omnigraph operator charges what it allocates to the query's join output`), the cross join its left side and its output (`cross join output`), the projection its output (`projection output`), the sort its retained rows and the batch it merges (its whole input when it has no -`fetch`). Shared Arrow buffers keep their existing charge. +`fetch`), the streamed scan the batch in flight (`v2 scan input`, `v2 scan +batch`), the hydration its taken rows and its output (`hydrate chunk`). +Shared Arrow buffers keep their existing charge. New output that exceeds the pool is refused with the typed `query_memory_bytes` error before downstream consumption. `AggregateExec` reserves its own state through DataFusion's pool and may spill. -Root `ScanExec`, `AntiJoinMaskExec` and `RankFuseExec` build a complete +A ranked `ScanExec`, `AntiJoinMaskExec` and `RankFuseExec` build a complete output batch before emitting it in `batch_size` slices; multi-hop `ExpandExec` retains its frontier and visited sets but emits its pairs in chunks of at most 256 as the walk finds them. Their `WorkMemory` reservations use the @@ -494,8 +517,12 @@ with the IR and the `SessionSettings` used by the first two only. Every run-time choice the run makes is data the planner wrote: - Compiler return items store their executed column name and declared type. - Aggregate, Projection and MetadataCount own exact output schemas; Sort and - Limit preserve those schemas. Projection and Aggregate capture named node + Aggregate, Projection and MetadataCount own exact output schemas; Sort, + Limit and HydrateColumns preserve those schemas. Under HydrateColumns the + projection's operator carries every declared column but the deferred ones, + plus a hidden row address per hydrated binding, and HydrateExec emits the + deferred columns with their declared fields, so the root again matches the + projection's declared schema. Projection and Aggregate capture named node object declarations from catalog members, including logical `@id`, member types and nullability. Fresh and replayed plans validate their result schemas using these stored declarations. Pipeline-node schemas remain conservative. diff --git a/docs/user/queries/explain.md b/docs/user/queries/explain.md index ceafa0797..67e8415e9 100644 --- a/docs/user/queries/explain.md +++ b/docs/user/queries/explain.md @@ -82,7 +82,13 @@ GQ spells them, and `residual`, the other conjuncts of the same filter as text; its second input is the table `Scan` of `$r`, whose `runtime_filter` object names the `column` the join filters at run time (`x`), the `needle` as `[binding, property]` and the `kind` (`text_contains_any`). A `Scan` row -carries no `runtime_filter` key unless a `ContainsJoin` marked it. Then one +carries no `runtime_filter` key unless a `ContainsJoin` marked it. A physical +`HydrateColumns` row at the root of a query plan (pass +`late_materialization`) carries `bindings`: per binding its `binding`, +`table` and the `columns` the output alone reads, which the scans below no +longer project (they read the row address instead) and which are fetched by +row address for the rows the root `limit` kept. Its `properties` carry +`retained_limit`, the bytes one hydrated chunk may hold. Then one `plan` row per optimizer pass that fired (`node` `pass`), and one per remaining field of the explain document: `route` (the engine route the plan describes), `logical_hash` (the structural hash of the logical plan), @@ -111,7 +117,7 @@ where equal rows are indistinguishable. The `datafusion` tree is the plan the query executes on the `v2` route: the physical tree lowered to operators, every read operator omnigraph's own (`ScanExec`, `ExpandExec`, `HashJoinExec`, `FilterExec`, `ProjectionExec`, -`SortExec`, `LimitExec`, `CrossJoinExec`, `ContainsJoinExec`, +`SortExec`, `LimitExec`, `HydrateExec`, `CrossJoinExec`, `ContainsJoinExec`, `AntiJoinMaskExec`, `RankFuseExec`, `MetadataCountExec`) except the aggregate, DataFusion's `AggregateExec`, one row per operator with `depth` from