LCOV - code coverage report
Current view: top level - dashql-core-profile/src - tests.rs Coverage Total Hit
Test: DashQL 6e74948 Lines: 99.8 % 576 575
Test Date: 2026-11-06 17:46:50

            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 : }
        

Generated by: LCOV version 2.0-1