Line data Source code
1 : //! Unit tests of reading Hyper plans into the query profile and laying them out.
2 :
3 : use super::QueryProfile;
4 : use dashql_buffers::profile as fb;
5 :
6 19 : fn default_config() -> fb::ProfileLayoutConfig {
7 19 : let mut config = fb::ProfileLayoutConfig::default();
8 19 : config.set_level_height(64.0);
9 19 : config.set_node_height(32.0);
10 19 : config.set_node_padding_left(8.0);
11 19 : config.set_node_padding_right(8.0);
12 19 : config.set_max_label_chars(20);
13 19 : config.set_width_per_label_char(8.5);
14 19 : config
15 19 : }
16 :
17 25 : fn pack_plan_with(plan: &str, config: &fb::ProfileLayoutConfig) -> Vec<u8> {
18 25 : let mut model = QueryProfile::new();
19 25 : model.configure(config);
20 25 : model.parse_hyper_plan(plan.as_bytes()).expect("plan must parse");
21 25 : model.compute_layout();
22 25 : model.pack_buffer()
23 25 : }
24 :
25 17 : fn pack_plan(plan: &str) -> Vec<u8> {
26 17 : pack_plan_with(plan, &default_config())
27 17 : }
28 :
29 25 : fn root(buffer: &[u8]) -> fb::QueryProfile<'_> {
30 25 : flatbuffers::root::<fb::QueryProfile>(buffer).expect("valid query profile")
31 25 : }
32 :
33 6 : fn make_layout_config(margin: f64) -> fb::ProfileLayoutConfig {
34 6 : let mut config = fb::ProfileLayoutConfig::default();
35 6 : config.set_level_height(20.0);
36 6 : config.set_node_height(8.0);
37 6 : config.set_node_margin_horizontal(margin);
38 6 : config.set_node_padding_left(0.25);
39 6 : config.set_node_padding_right(0.25);
40 6 : config.set_max_label_chars(20);
41 6 : config.set_width_per_label_char(1.25);
42 6 : config
43 6 : }
44 :
45 63 : fn string<'a>(plan: &fb::QueryProfile<'a>, id: u32) -> &'a str {
46 63 : plan.string_dictionary().unwrap().get(id as usize)
47 63 : }
48 :
49 2 : fn expect_same_level_nodes_do_not_overlap(plan: &fb::QueryProfile, margin: f64) {
50 2 : let operators = plan.operators().unwrap();
51 9 : for i in 0..operators.len() {
52 9 : let left = *operators.get(i).layout_rect();
53 22 : for j in i + 1..operators.len() {
54 22 : let right = *operators.get(j).layout_rect();
55 22 : if (left.y() - right.y()).abs() > 0.001 {
56 17 : continue;
57 5 : }
58 5 : let (first, second) = if left.x() > right.x() { (right, left) } else { (left, right) };
59 5 : assert!(
60 5 : second.x() - second.width() / 2.0 >= first.x() + first.width() / 2.0 + margin - 0.001,
61 : "operators {i} and {j}"
62 : );
63 : }
64 : }
65 2 : }
66 :
67 2 : fn expect_bounds_contain_nodes(plan: &fb::QueryProfile) {
68 2 : let bounds = plan.layout_rect().unwrap();
69 9 : for op in plan.operators().unwrap() {
70 9 : let rect = op.layout_rect();
71 9 : assert!(rect.x() - rect.width() / 2.0 >= bounds.x() - 0.001);
72 9 : assert!(rect.x() + rect.width() / 2.0 <= bounds.x() + bounds.width() + 0.001);
73 : }
74 2 : }
75 :
76 : /// The operator id serialized by Hyper, read from the attributes of an operator
77 6 : fn source_operator_id(plan: &fb::QueryProfile, operator_id: u32) -> u64 {
78 6 : let op = plan.operators().unwrap().get(operator_id as usize);
79 12 : for i in 0..op.attribute_count() {
80 12 : let attribute = plan.attributes().unwrap().get((op.attributes_begin() + i) as usize);
81 12 : let name = string(plan, attribute.name());
82 12 : if name == "operatorId" || name == "operator-id" {
83 6 : return string(plan, attribute.value_json()).parse().unwrap();
84 6 : }
85 : }
86 0 : 0
87 6 : }
88 :
89 : #[test]
90 1 : fn explicit_pipelines_and_properties() {
91 1 : let buffer = pack_plan(
92 1 : r#"{
93 1 : "operator":"executiontarget","operatorId":1,"cardinality":5,
94 1 : "input":{"operator":"tablescan","operatorId":2,"metadata":{"cost":1}},
95 1 : "pipelines":[{"pipelineId":10,"operators":[2,1]}]
96 1 : }"#,
97 : );
98 1 : let plan = root(&buffer);
99 1 : assert_eq!(plan.engine(), fb::ProfileEngine::HYPER);
100 1 : assert_eq!(plan.operators().unwrap().len(), 2);
101 1 : assert_eq!(plan.pipelines().unwrap().len(), 1);
102 1 : let pipeline = plan.pipelines().unwrap().get(0);
103 1 : assert_eq!(string(&plan, pipeline.engine_id()), "10");
104 1 : assert_eq!(pipeline.fragment(), 0);
105 1 : assert_eq!(pipeline.slot_count(), 2);
106 1 : let slots = plan.slots().unwrap();
107 1 : let (scan, target) = (slots.get(0), slots.get(1));
108 1 : assert_eq!((scan.pipeline(), scan.operator(), scan.role()), (0, 0, fb::ProfileSlotRole::SOURCE));
109 1 : assert_eq!((target.pipeline(), target.operator(), target.role()), (0, 1, fb::ProfileSlotRole::SINK));
110 : // The edge from the scan to the execution target is carried by the slot of the scan
111 1 : let edge = plan.operator_edges().unwrap().get(0);
112 1 : assert_eq!((edge.child(), edge.parent(), edge.slot()), (0, 1, 0));
113 2 : for (operator, slot) in [(0, 0), (1, 1)] {
114 2 : let op = plan.operators().unwrap().get(operator);
115 2 : assert_eq!(op.slot_count(), 1);
116 2 : assert_eq!(plan.operator_slots().unwrap().get(op.slots_begin() as usize), slot);
117 : }
118 1 : assert_eq!(string(&plan, plan.operators().unwrap().get(0).engine_id()), "2");
119 1 : assert!(plan.operators().unwrap().get(0).attribute_count() > 0);
120 1 : }
121 :
122 : #[test]
123 1 : fn pipeline_slots_have_roles() {
124 : // A join in two pipelines: the build side ends at the join, the probe side passes through it
125 1 : let buffer = pack_plan(
126 1 : r#"{
127 1 : "operator":"executiontarget","operatorId":1,
128 1 : "input":{"operator":"join","operatorId":2,
129 1 : "left":{"operator":"tablescan","operatorId":3},
130 1 : "right":{"operator":"tablescan","operatorId":4}},
131 1 : "pipelines":[{"pipelineId":1,"operators":[3,2]},{"pipelineId":2,"operators":[4,2,1],"cost":5}]
132 1 : }"#,
133 : );
134 1 : let plan = root(&buffer);
135 1 : let slots = plan.slots().unwrap();
136 5 : let roles: Vec<_> = slots.iter().map(|s| (s.pipeline(), s.operator(), s.role())).collect();
137 : use fb::ProfileSlotRole as R;
138 1 : assert_eq!(roles, vec![(0, 0, R::SOURCE), (0, 2, R::SINK), (1, 1, R::SOURCE), (1, 2, R::THROUGH), (1, 3, R::SINK)]);
139 1 : let join = plan.operators().unwrap().get(2);
140 1 : assert_eq!(join.kind(), fb::ProfileOperatorKind::JOIN);
141 1 : assert_eq!(join.slot_count(), 2);
142 : // The pipeline members other than the id and the operators are attributes
143 1 : let probe = plan.pipelines().unwrap().get(1);
144 1 : assert_eq!(probe.attribute_count(), 1);
145 1 : assert_eq!(string(&plan, plan.attributes().unwrap().get(probe.attributes_begin() as usize).name()), "cost");
146 1 : }
147 :
148 : #[test]
149 1 : fn resolves_operator_cross_edges() {
150 1 : let buffer = pack_plan(
151 1 : r#"{
152 1 : "operator":"executiontarget","operatorId":1,
153 1 : "input":{"operator":"unionall","operatorId":2,"input":[
154 1 : {"operator":"explicitscan","operatorId":3,
155 1 : "input":{"operator":"groupby","operatorId":4,"input":{"operator":"tablescan","operatorId":5}}},
156 1 : {"operator":"explicitscan","operatorId":6,"input":4},
157 1 : {"operator":"leftouterjoin","operatorId":7,"magic":4,
158 1 : "left":{"operator":"tablescan","operatorId":8,"earlyProbes":[
159 1 : {"builder":7,"attributes":[0],"type":"lookup"},
160 1 : {"builder":4,"attributes":[1],"type":"minmaxonly"}
161 1 : ]},
162 1 : "right":{"operator":"earlyprobe","operatorId":9,"builder":7}},
163 1 : {"operator":"iteration","operator-id":10,
164 1 : "input":{"operator":"iterationincrement","operator-id":11,"source":10}}
165 1 : ]}
166 1 : }"#,
167 : );
168 1 : let plan = root(&buffer);
169 :
170 1 : let cross_edges = plan.operator_cross_edges().unwrap();
171 1 : assert_eq!(cross_edges.len(), 6);
172 1 : let attributes = plan.attributes().unwrap();
173 :
174 1 : let mut kinds = Vec::new();
175 1 : let mut edges_by_kind = std::collections::HashMap::new();
176 6 : for edge in cross_edges {
177 6 : assert!((edge.source() as usize) < plan.operators().unwrap().len());
178 6 : assert!((edge.target() as usize) < plan.operators().unwrap().len());
179 6 : assert!(edge.attribute_count() >= 1);
180 6 : let attribute = attributes.get(edge.attributes_begin() as usize);
181 6 : assert_eq!(string(&plan, attribute.name()), "kind");
182 6 : let kind = string(&plan, attribute.value_json());
183 6 : kinds.push(kind);
184 6 : edges_by_kind.entry(kind).or_insert(edge);
185 : }
186 30 : let count = |kind: &str| kinds.iter().filter(|k| **k == kind).count();
187 1 : assert_eq!(count(r#""input""#), 1);
188 1 : assert_eq!(count(r#""magic""#), 1);
189 1 : assert_eq!(count(r#""early-probe""#), 2);
190 1 : assert_eq!(count(r#""builder""#), 1);
191 1 : assert_eq!(count(r#""source""#), 1);
192 1 : assert_eq!(source_operator_id(&plan, edges_by_kind[r#""input""#].source()), 4);
193 1 : assert_eq!(source_operator_id(&plan, edges_by_kind[r#""input""#].target()), 6);
194 1 : assert_eq!(source_operator_id(&plan, edges_by_kind[r#""source""#].source()), 10);
195 1 : assert_eq!(source_operator_id(&plan, edges_by_kind[r#""source""#].target()), 11);
196 :
197 11 : for (operator_id, op) in plan.operators().unwrap().iter().enumerate() {
198 11 : for i in 0..op.cross_edge_count() {
199 6 : assert_eq!(cross_edges.get((op.cross_edges_begin() + i) as usize).source(), operator_id as u32);
200 : }
201 : }
202 1 : assert_eq!(plan.operator_edges().unwrap().len(), plan.operators().unwrap().len() - 1);
203 1 : assert_eq!(plan.root_operators().unwrap().len(), 1);
204 1 : }
205 :
206 : #[test]
207 1 : fn resolves_hyphenated_explicit_scan_input_cross_edge() {
208 1 : let buffer = pack_plan(
209 1 : r#"{
210 1 : "operator":"result-sink","operator-id":1,"inputs":[
211 1 : {"operator":"explicit-scan","operator-id":4,
212 1 : "input":{"operator":"share","operator-id":5,
213 1 : "inputs":[{"operator":"scan","operator-id":6}]}},
214 1 : {"operator":"explicit-scan","operator-id":8,"input":5}
215 1 : ]
216 1 : }"#,
217 : );
218 1 : let plan = root(&buffer);
219 1 : let cross_edges = plan.operator_cross_edges().unwrap();
220 1 : assert_eq!(cross_edges.len(), 1);
221 1 : let edge = cross_edges.get(0);
222 1 : assert_eq!(source_operator_id(&plan, edge.source()), 5);
223 1 : assert_eq!(source_operator_id(&plan, edge.target()), 8);
224 1 : }
225 :
226 : #[test]
227 1 : fn ignores_unknown_and_unrelated_numeric_properties() {
228 1 : let buffer = pack_plan(
229 1 : r#"{
230 1 : "operator":"executiontarget","operatorId":1,"cardinality":42,
231 1 : "input":{"operator":"explicitscan","operatorId":2,"input":999,"relationId":1}
232 1 : }"#,
233 : );
234 1 : let plan = root(&buffer);
235 1 : assert_eq!(plan.operator_cross_edges().unwrap().len(), 0);
236 1 : }
237 :
238 : #[test]
239 1 : fn execution_target_creates_root_fragment_but_does_not_infer_pipelines() {
240 1 : let buffer = pack_plan(
241 1 : r#"{
242 1 : "operator":"executiontarget","operatorId":1,
243 1 : "input":{"operator":"tablescan","operatorId":2}
244 1 : }"#,
245 : );
246 1 : let plan = root(&buffer);
247 1 : assert_eq!(plan.operators().unwrap().len(), 2);
248 1 : assert_eq!(plan.fragments().unwrap().len(), 1);
249 1 : let fragment = plan.fragments().unwrap().get(0);
250 1 : assert_eq!(fragment.root_operator(), 1);
251 1 : assert_eq!(fragment.hidden(), 1);
252 1 : assert_eq!(fragment.parent_fragment(), u32::MAX);
253 1 : assert_eq!(fragment.operator_count(), 2);
254 1 : let fragment_operators = plan.fragment_operators().unwrap();
255 1 : assert_eq!(fragment_operators.get(fragment.operators_begin() as usize), 1);
256 1 : assert_eq!(fragment_operators.get(fragment.operators_begin() as usize + 1), 0);
257 1 : assert_eq!(plan.pipelines().unwrap().len(), 0);
258 2 : for op in plan.operators().unwrap() {
259 2 : assert_eq!(op.fragment(), 0);
260 : }
261 1 : }
262 :
263 : /// The operators of a fragment
264 7 : fn fragment_operators(plan: &fb::QueryProfile, fragment: &fb::ProfileFragment) -> Vec<u32> {
265 7 : let operators = plan.fragment_operators().unwrap();
266 16 : (0..fragment.operator_count()).map(|i| operators.get((fragment.operators_begin() + i) as usize)).collect()
267 7 : }
268 :
269 : #[test]
270 1 : fn federate_creates_fragment_from_reachable_children() {
271 1 : let buffer = pack_plan(
272 1 : r#"{
273 1 : "operator":"output",
274 1 : "inputs":[{
275 1 : "inputs":[{
276 1 : "operator":"join",
277 1 : "left":{"operator":"scan"},
278 1 : "right":{"operator":"scan"}
279 1 : }],
280 1 : "operator":"federate"
281 1 : }]
282 1 : }"#,
283 : );
284 1 : let plan = root(&buffer);
285 1 : assert_eq!(plan.fragments().unwrap().len(), 1);
286 1 : let fragment = plan.fragments().unwrap().get(0);
287 1 : assert_eq!(fragment.root_operator(), 3);
288 1 : assert_eq!(fragment.hidden(), 0);
289 1 : assert_eq!(fragment_operators(&plan, fragment), vec![3, 2, 0, 1]);
290 : // The output above the federate belongs to no fragment
291 1 : assert_eq!(plan.operators().unwrap().get(4).fragment(), u32::MAX);
292 1 : }
293 :
294 : #[test]
295 1 : fn execution_target_root_fragment_contains_federated_fragments() {
296 1 : let buffer = pack_plan(
297 1 : r#"{
298 1 : "operator":"executiontarget",
299 1 : "input":{"operator":"federate","inputs":[{"operator":"scan"}]}
300 1 : }"#,
301 : );
302 1 : let plan = root(&buffer);
303 1 : assert_eq!(plan.fragments().unwrap().len(), 2);
304 1 : let root_fragment = plan.fragments().unwrap().get(0);
305 1 : assert_eq!(root_fragment.root_operator(), 2);
306 1 : assert_eq!(root_fragment.hidden(), 1);
307 1 : assert_eq!(fragment_operators(&plan, root_fragment), vec![2]);
308 1 : let federated = plan.fragments().unwrap().get(1);
309 1 : assert_eq!(federated.root_operator(), 1);
310 1 : assert_eq!(federated.parent_fragment(), 0);
311 1 : assert_eq!(federated.hidden(), 0);
312 1 : assert_eq!(fragment_operators(&plan, federated), vec![1, 0]);
313 1 : }
314 :
315 : #[test]
316 1 : fn sibling_federates_create_distinct_fragments() {
317 1 : let buffer = pack_plan(
318 1 : r#"{
319 1 : "operator":"join",
320 1 : "left":{"operator":"federate","inputs":[{"operator":"scan"}]},
321 1 : "right":{"operator":"federate","inputs":[{"operator":"map","input":{"operator":"scan"}}]}
322 1 : }"#,
323 : );
324 1 : let plan = root(&buffer);
325 1 : assert_eq!(plan.fragments().unwrap().len(), 2);
326 1 : let left = plan.fragments().unwrap().get(0);
327 1 : assert_eq!(left.root_operator(), 3);
328 1 : assert_eq!(fragment_operators(&plan, left), vec![3, 0]);
329 1 : let right = plan.fragments().unwrap().get(1);
330 1 : assert_eq!(right.root_operator(), 4);
331 1 : assert_eq!(fragment_operators(&plan, right), vec![4, 2, 1]);
332 1 : }
333 :
334 : #[test]
335 1 : fn nested_federates_nest_fragments() {
336 1 : let buffer = pack_plan(
337 1 : r#"{
338 1 : "operator":"federate",
339 1 : "inputs":[{
340 1 : "operator":"map",
341 1 : "input":{"operator":"federate","inputs":[{"operator":"scan"}]}
342 1 : }]
343 1 : }"#,
344 : );
345 1 : let plan = root(&buffer);
346 1 : assert_eq!(plan.fragments().unwrap().len(), 2);
347 1 : let outer = plan.fragments().unwrap().get(0);
348 1 : assert_eq!(outer.root_operator(), 3);
349 1 : assert_eq!(outer.parent_fragment(), u32::MAX);
350 1 : assert_eq!(fragment_operators(&plan, outer), vec![3, 2]);
351 1 : let inner = plan.fragments().unwrap().get(1);
352 1 : assert_eq!(inner.root_operator(), 1);
353 1 : assert_eq!(inner.parent_fragment(), 0);
354 1 : assert_eq!(fragment_operators(&plan, inner), vec![1, 0]);
355 4 : let fragment_of: Vec<u32> = plan.operators().unwrap().iter().map(|op| op.fragment()).collect();
356 1 : assert_eq!(fragment_of, vec![1, 1, 0, 0]);
357 1 : }
358 :
359 : #[test]
360 1 : fn infers_scan_label_from_defined_attributes() {
361 1 : let buffer = pack_plan(
362 1 : r#"{
363 1 : "operator":"output",
364 1 : "operator-id":1,
365 1 : "inputs":[{
366 1 : "operator":"scan",
367 1 : "operator-id":2,
368 1 : "type":"parquet",
369 1 : "attributes":[
370 1 : {"defines":{"id":"partsupp.partkey","type":{"type":"bigint"}},"name":"ps_partkey"},
371 1 : {"defines":{"id":"partsupp.suppkey","type":{"type":"bigint"}},"name":"ps_suppkey"}
372 1 : ]
373 1 : }]
374 1 : }"#,
375 : );
376 1 : let plan = root(&buffer);
377 1 : assert_eq!(plan.operators().unwrap().len(), 2);
378 1 : let scan = plan.operators().unwrap().get(0);
379 1 : assert_ne!(scan.label(), u32::MAX);
380 1 : assert_eq!(string(&plan, scan.label()), "partsupp");
381 1 : }
382 :
383 : #[test]
384 1 : fn reads_kebab_case_debug_name() {
385 1 : let buffer = pack_plan(
386 1 : r#"{
387 1 : "operator":"parquetscan",
388 1 : "operator-id":15,
389 1 : "attributes":[
390 1 : {"col-id":0,"name":"s_suppkey","iu":["supplier.suppkey",["BigInt","nullable"]]},
391 1 : {"col-id":3,"name":"s_nationkey","iu":["supplier.nationke",["Integer","nullable"]]}
392 1 : ],
393 1 : "debug-name":{"classification":"customer","value":"supplier"}
394 1 : }"#,
395 : );
396 1 : let plan = root(&buffer);
397 1 : assert_eq!(plan.operators().unwrap().len(), 1);
398 1 : let scan = plan.operators().unwrap().get(0);
399 1 : assert_ne!(scan.label(), u32::MAX);
400 1 : assert_eq!(string(&plan, scan.label()), "supplier");
401 1 : }
402 :
403 : #[test]
404 1 : fn exposes_operator_statistics() {
405 : const PLAN: &str = r#"{
406 : "operator":"parquetscan",
407 : "operator-id":15,
408 : "estimated-rows":10000.5,
409 : "estimated-rows-in-table":12000.25,
410 : "statistics":{
411 : "pipeline":8,
412 : "column-count":2,
413 : "memory-bytes":4294967297,
414 : "output-rows":1987,
415 : "processed-rows":10000,
416 : "rows-matching-restrictions":1987,
417 : "running":false
418 : }
419 : }"#;
420 1 : let buffer = pack_plan(PLAN);
421 1 : let plan = root(&buffer);
422 1 : assert_eq!(plan.operators().unwrap().len(), 1);
423 1 : let op = plan.operators().unwrap().get(0);
424 1 : assert_eq!(op.estimated_rows(), 10000.5);
425 1 : assert_eq!(op.kind(), fb::ProfileOperatorKind::SCAN);
426 :
427 : // The counters of the analyzed plan are the execution state of the operator
428 1 : let mut model = QueryProfile::new();
429 1 : model.parse_hyper_plan(PLAN.as_bytes()).unwrap();
430 1 : let state = analyzed_operator(&model, 0);
431 1 : assert_eq!(state.stats.tuples_in, Some(10000));
432 1 : assert_eq!(state.stats.tuples_out, Some(1987));
433 1 : assert_eq!(state.stats.memory_bytes, Some(4294967297));
434 : use fb::ProfileStatsField as F;
435 1 : let fields = [F::TUPLES_IN, F::TUPLES_OUT, F::MEMORY_BYTES];
436 3 : assert_eq!(state.stats.reported(), fields.iter().fold(0, |bits, field| bits | field.0));
437 :
438 1 : let mut found_raw_statistics = false;
439 5 : for i in 0..op.attribute_count() {
440 5 : let attribute = plan.attributes().unwrap().get((op.attributes_begin() + i) as usize);
441 5 : found_raw_statistics |= string(&plan, attribute.name()) == "statistics";
442 5 : }
443 1 : assert!(found_raw_statistics);
444 1 : }
445 :
446 : #[test]
447 1 : fn reads_nested_estimated_statistics() {
448 1 : let buffer = pack_plan(
449 1 : r#"{
450 1 : "operator":"scan",
451 1 : "statistics":{"estimated-rows":4.8375,"estimated-rows-in-table":25}
452 1 : }"#,
453 : );
454 1 : let plan = root(&buffer);
455 1 : assert_eq!(plan.operators().unwrap().get(0).estimated_rows(), 4.8375);
456 1 : }
457 :
458 : #[test]
459 1 : fn operators_without_estimate_have_nan_rows() {
460 1 : let buffer = pack_plan(r#"{"operator":"scan"}"#);
461 1 : assert!(root(&buffer).operators().unwrap().get(0).estimated_rows().is_nan());
462 1 : }
463 :
464 : /// The execution state of an operator of an analyzed plan
465 3 : fn analyzed_operator(model: &QueryProfile, operator: u32) -> super::EntityState {
466 3 : model
467 3 : .snapshot_events()
468 3 : .into_iter()
469 3 : .find(|u| u.entity == fb::ProfileEntity::OPERATOR && u.index == operator)
470 3 : .map(|u| u.state)
471 3 : .unwrap_or_default()
472 3 : }
473 :
474 : #[test]
475 1 : fn reads_camel_case_memory_bytes() {
476 1 : let mut model = QueryProfile::new();
477 1 : model.parse_hyper_plan(br#"{"operator":"scan","statistics":{"memoryBytes":2048}}"#).unwrap();
478 1 : assert_eq!(analyzed_operator(&model, 0).stats.memory_bytes, Some(2048));
479 1 : }
480 :
481 : #[test]
482 1 : fn reads_standard_json_only() {
483 : use dashql_buffers::status::StatusCode;
484 5 : for plan in
485 1 : ["", "{\"operator\":\"scan\",}", "/* c */ {\"operator\":\"scan\"}", "{\"operator\":\"scan\"}\0", "[NaN]"]
486 : {
487 5 : let mut model = QueryProfile::new();
488 5 : assert_eq!(
489 5 : model.parse_hyper_plan(plan.as_bytes()),
490 : Err(StatusCode::VIEWMODEL_INPUT_JSON_PARSER_ERROR),
491 : "{plan:?}"
492 : );
493 : }
494 1 : }
495 :
496 : #[test]
497 1 : fn serializes_attributes_and_ignores_counts_that_are_not_unsigned() {
498 : const PLAN: &str = r#"{
499 : "operator":"scan",
500 : "values":[1, -0.0, 1.5e3, "a\u0000\"b\/"],
501 : "sqlpos":[[-1, 4294967296]],
502 : "statistics":{"processed-rows":-1,"output-rows":2.5,"memory-bytes":7}
503 : }"#;
504 1 : let buffer = pack_plan(PLAN);
505 1 : let plan = root(&buffer);
506 1 : let op = plan.operators().unwrap().get(0);
507 2 : let attribute = |name: &str| {
508 2 : (op.attributes_begin()..op.attributes_begin() + op.attribute_count())
509 3 : .map(|i| plan.attributes().unwrap().get(i as usize))
510 3 : .find(|a| string(&plan, a.name()) == name)
511 2 : .map(|a| string(&plan, a.value_json()))
512 2 : };
513 1 : assert_eq!(attribute("operator"), Some(r#""scan""#));
514 1 : assert_eq!(attribute("values"), Some(r#"[1,-0.0,1500.0,"a\u0000\"b/"]"#));
515 1 : assert_eq!(op.source_location().offset(), 0);
516 1 : assert_eq!(op.source_location().length(), 0);
517 1 : let mut model = QueryProfile::new();
518 1 : model.parse_hyper_plan(PLAN.as_bytes()).unwrap();
519 1 : let stats = analyzed_operator(&model, 0).stats;
520 1 : assert_eq!((stats.tuples_in, stats.tuples_out, stats.memory_bytes), (None, None, Some(7)));
521 1 : }
522 :
523 : #[test]
524 1 : fn variable_width_nodes_do_not_overlap() {
525 1 : let margin = 0.25;
526 1 : let config = make_layout_config(margin);
527 1 : let buffer = pack_plan_with(
528 1 : r#"{
529 1 : "operator":"unionall",
530 1 : "input":[
531 1 : {"operator":"map","input":{"operator":"map","input":{"operator":"tablescan","debugName":{"value":"a_very_long_table"}}}},
532 1 : {"operator":"map","input":{"operator":"tablescan","debugName":{"value":"x"}}},
533 1 : {"operator":"tablescan","debugName":{"value":"medium_table"}}
534 1 : ]
535 1 : }"#,
536 1 : &config,
537 : );
538 1 : let plan = root(&buffer);
539 1 : expect_same_level_nodes_do_not_overlap(&plan, margin);
540 1 : expect_bounds_contain_nodes(&plan);
541 1 : }
542 :
543 : #[test]
544 1 : fn symbol_width_sizes_operators_without_a_label_of_their_own() {
545 1 : let plan = r#"{"operator":"map","input":{"operator":"futureoperator","input":{"operator":"tablescan","debugName":{"value":"lineitem"}}}}"#;
546 2 : let widths = |config: &fb::ProfileLayoutConfig| {
547 2 : let buffer = pack_plan_with(plan, config);
548 2 : let plan = root(&buffer);
549 : // The operators are stored post-order: tablescan, futureoperator, map
550 6 : plan.operators().unwrap().iter().map(|op| op.layout_rect().width()).collect::<Vec<_>>()
551 2 : };
552 : // Without a symbol width, every operator is sized for its label or type name
553 1 : assert_eq!(widths(&default_config()), vec![8.0 + 8.0 * 8.5 + 8.0, 8.0 + 14.0 * 8.5 + 8.0, 8.0 + 3.0 * 8.5 + 8.0]);
554 : // The map shows its symbol, the table scan its table, the unknown operator its type name
555 1 : let mut config = default_config();
556 1 : config.set_symbol_width(18.0);
557 1 : assert_eq!(widths(&config), vec![8.0 + 8.0 * 8.5 + 8.0, 8.0 + 14.0 * 8.5 + 8.0, 8.0 + 18.0 + 8.0]);
558 1 : }
559 :
560 : #[test]
561 1 : fn unary_child_remains_centered_across_uneven_sibling_contours() {
562 1 : let buffer = pack_plan(
563 1 : r#"{
564 1 : "operator":"result-sink","operator-id":1,"inputs":[
565 1 : {"operator":"map","input":
566 1 : {"operator":"explicit-scan","operator-id":4,"input":
567 1 : {"operator":"share","operator-id":5,"inputs":[
568 1 : {"operator":"scan","operator-id":6}
569 1 : ]}
570 1 : }
571 1 : },
572 1 : {"operator":"map","input":
573 1 : {"operator":"explicit-scan","operator-id":8,"input":5}
574 1 : }
575 1 : ]
576 1 : }"#,
577 : );
578 1 : let plan = root(&buffer);
579 1 : let mut explicit_scan = None;
580 1 : let mut share = None;
581 7 : for op in plan.operators().unwrap() {
582 13 : for i in 0..op.attribute_count() {
583 13 : let attribute = plan.attributes().unwrap().get((op.attributes_begin() + i) as usize);
584 13 : if string(&plan, attribute.name()) != "operator-id" {
585 8 : continue;
586 5 : }
587 5 : match string(&plan, attribute.value_json()).parse::<u64>().unwrap() {
588 1 : 4 => explicit_scan = Some(op),
589 1 : 5 => share = Some(op),
590 3 : _ => {}
591 : }
592 : }
593 : }
594 1 : let (explicit_scan, share) = (explicit_scan.unwrap(), share.unwrap());
595 1 : assert!((explicit_scan.layout_rect().x() - share.layout_rect().x()).abs() <= 0.001);
596 1 : }
597 :
598 : #[test]
599 1 : fn independent_roots_are_packed_without_overlap() {
600 1 : let margin = 0.5;
601 1 : let config = make_layout_config(margin);
602 1 : let buffer = pack_plan_with(
603 1 : r#"[
604 1 : {"operator":"tablescan","debugName":{"value":"first_table"}},
605 1 : {"operator":"tablescan","debugName":{"value":"second_table_is_wider"}}
606 1 : ]"#,
607 1 : &config,
608 : );
609 1 : let plan = root(&buffer);
610 1 : assert_eq!(plan.root_operators().unwrap().len(), 2);
611 1 : expect_same_level_nodes_do_not_overlap(&plan, margin);
612 1 : expect_bounds_contain_nodes(&plan);
613 1 : }
614 :
615 4 : fn left_to_right_config() -> fb::ProfileLayoutConfig {
616 4 : let mut config = make_layout_config(4.0);
617 4 : config.set_node_margin_vertical(2.0);
618 4 : config.set_orientation(fb::ProfileLayoutOrientation::LEFT_TO_RIGHT);
619 4 : config
620 4 : }
621 :
622 : /// A join of a deep and a shallow input, below a union with a third input
623 : const LEFT_TO_RIGHT_PLAN: &str = r#"{
624 : "operator":"unionall",
625 : "input":[
626 : {"operator":"join","input":[
627 : {"operator":"map","input":{"operator":"map","input":{"operator":"tablescan","debugName":{"value":"a_very_long_table"}}}},
628 : {"operator":"tablescan","debugName":{"value":"x"}}
629 : ]},
630 : {"operator":"map","input":{"operator":"tablescan","debugName":{"value":"medium_table"}}},
631 : {"operator":"tablescan","debugName":{"value":"y"}}
632 : ]
633 : }"#;
634 :
635 : #[test]
636 1 : fn left_to_right_plans_grow_right_and_down_from_the_root() {
637 1 : let buffer = pack_plan_with(LEFT_TO_RIGHT_PLAN, &left_to_right_config());
638 1 : let plan = root(&buffer);
639 1 : let operators = plan.operators().unwrap();
640 1 : let root_rect = *operators.get(plan.root_operators().unwrap().get(0) as usize).layout_rect();
641 9 : for op in operators {
642 9 : let rect = op.layout_rect();
643 : // Nothing is above or left of the root
644 9 : assert!(rect.y() >= root_rect.y() - 0.001);
645 9 : assert!(rect.x() - rect.width() / 2.0 >= root_rect.x() - root_rect.width() / 2.0 - 0.001);
646 : // An operator shares its row with its first input, its inputs are in the column right of it
647 9 : if op.children_count() > 0 {
648 5 : let first = operators.get(op.children_begin() as usize).layout_rect();
649 5 : assert!((first.y() - rect.y()).abs() <= 0.001);
650 8 : for i in 0..op.children_count() {
651 8 : let child = operators.get((op.children_begin() + i) as usize).layout_rect();
652 8 : assert!(child.x() - child.width() / 2.0 >= rect.x() + rect.width() / 2.0 + 4.0 - 0.001);
653 : }
654 4 : }
655 : }
656 : // The root sits at the top left corner of the bounds
657 1 : assert!((root_rect.y() - root_rect.height() / 2.0).abs() <= 0.001);
658 1 : assert!((root_rect.x() - root_rect.width() / 2.0).abs() <= 0.001);
659 1 : }
660 :
661 : #[test]
662 1 : fn left_to_right_siblings_do_not_overlap() {
663 1 : let buffer = pack_plan_with(LEFT_TO_RIGHT_PLAN, &left_to_right_config());
664 1 : let plan = root(&buffer);
665 1 : let operators = plan.operators().unwrap();
666 1 : let bounds = plan.layout_rect().unwrap();
667 9 : for i in 0..operators.len() {
668 9 : let a = *operators.get(i).layout_rect();
669 9 : assert!(a.y() - a.height() / 2.0 >= bounds.y() - 0.001);
670 9 : assert!(a.y() + a.height() / 2.0 <= bounds.y() + bounds.height() + 0.001);
671 9 : assert!(a.x() + a.width() / 2.0 <= bounds.x() + bounds.width() + 0.001);
672 36 : for j in i + 1..operators.len() {
673 36 : let b = *operators.get(j).layout_rect();
674 : // Operators of a column share their left edge
675 36 : if (a.x() - a.width() / 2.0 - (b.x() - b.width() / 2.0)).abs() > 0.001 {
676 30 : continue;
677 6 : }
678 6 : let (upper, lower) = if a.y() > b.y() { (b, a) } else { (a, b) };
679 6 : assert!(lower.y() - lower.height() / 2.0 >= upper.y() + upper.height() / 2.0 + 2.0 - 0.001, "operators {i} and {j}");
680 : }
681 : }
682 1 : }
683 :
684 : #[test]
685 1 : fn left_to_right_plans_are_one_row_per_leaf() {
686 1 : let buffer = pack_plan_with(LEFT_TO_RIGHT_PLAN, &left_to_right_config());
687 1 : let plan = root(&buffer);
688 : // Four leaves of height 8 with a margin of 2 between them
689 1 : let bounds = plan.layout_rect().unwrap();
690 1 : assert!((bounds.height() - (4.0 * 8.0 + 3.0 * 2.0)).abs() <= 0.001, "height {}", bounds.height());
691 : // Five levels, as wide as their widest node, with a margin of 4 between them
692 1 : let mut columns: Vec<(f64, f64)> = Vec::new();
693 9 : for op in plan.operators().unwrap() {
694 9 : let rect = op.layout_rect();
695 9 : let left = rect.x() - rect.width() / 2.0;
696 24 : match columns.iter_mut().find(|(x, _)| (x - left).abs() <= 0.001) {
697 4 : Some(column) => column.1 = column.1.max(rect.width()),
698 5 : None => columns.push((left, rect.width())),
699 : }
700 : }
701 1 : assert_eq!(columns.len(), 5);
702 1 : let width: f64 = columns.iter().map(|(_, width)| width).sum::<f64>() + 4.0 * 4.0;
703 1 : assert!((bounds.width() - width).abs() <= 0.001, "width {} != {width}", bounds.width());
704 1 : }
705 :
706 : #[test]
707 1 : fn left_to_right_roots_are_stacked() {
708 1 : let margin = 2.0;
709 1 : let buffer = pack_plan_with(
710 1 : r#"[
711 1 : {"operator":"map","input":{"operator":"tablescan","debugName":{"value":"first_table"}}},
712 1 : {"operator":"tablescan","debugName":{"value":"second_table_is_wider"}}
713 1 : ]"#,
714 1 : &left_to_right_config(),
715 : );
716 1 : let plan = root(&buffer);
717 1 : let roots = plan.root_operators().unwrap();
718 1 : let operators = plan.operators().unwrap();
719 1 : let (first, second) = (operators.get(roots.get(0) as usize).layout_rect(), operators.get(roots.get(1) as usize).layout_rect());
720 1 : assert!((first.x() - first.width() / 2.0 - (second.x() - second.width() / 2.0)).abs() <= 0.001);
721 1 : assert!(second.y() - second.height() / 2.0 >= first.y() + first.height() / 2.0 + margin - 0.001);
722 1 : }
723 :
724 2 : fn tpch_config() -> fb::ProfileLayoutConfig {
725 2 : let mut config = fb::ProfileLayoutConfig::default();
726 2 : config.set_level_height(20.0);
727 2 : config.set_node_height(8.0);
728 2 : config.set_node_padding_left(2.0);
729 2 : config.set_node_padding_right(2.0);
730 2 : config.set_max_label_chars(20);
731 2 : config.set_width_per_label_char(2.0);
732 2 : config.set_node_min_width(8.0);
733 2 : config
734 2 : }
735 :
736 : #[test]
737 1 : fn tpch_q18() {
738 1 : let mut model = QueryProfile::new();
739 1 : model.configure(&tpch_config());
740 1 : model.parse_hyper_plan(super::tests_tpch::TPCH_Q18.as_bytes()).expect("plan must parse");
741 1 : model.compute_layout();
742 1 : }
743 :
744 : #[test]
745 1 : fn tpch_q22() {
746 1 : let mut model = QueryProfile::new();
747 1 : model.configure(&tpch_config());
748 1 : model.parse_hyper_plan(super::tests_tpch::TPCH_Q22.as_bytes()).expect("plan must parse");
749 1 : model.compute_layout();
750 1 : }
|