LCOV - code coverage report
Current view: top level - dashql-core/src/formatter - pgq.rs Coverage Total Hit
Test: DashQL 915828b Lines: 96.5 % 231 223
Test Date: 2026-11-06 19:30:32

            Line data    Source code
       1              : //! Property graphs and graph queries.
       2              : 
       3              : use dashql_parser::attrs;
       4              : use super::{is_true, FmtReg, Formatter, FormattingJoinPolicy, K};
       5              : use dashql_buffers::parser::{Node, NodeType};
       6              : 
       7              : /// Binding strength of `A | B`
       8              : const GRAPH_LABEL_DISJUNCTION: u8 = 1;
       9              : /// Binding strength of `A & B`
      10              : const GRAPH_LABEL_CONJUNCTION: u8 = 2;
      11              : /// Binding strength of `!A`
      12              : const GRAPH_LABEL_NEGATION: u8 = 3;
      13              : /// Binding strength of a label or the wildcard
      14              : const GRAPH_LABEL_ATOM: u8 = 4;
      15              : 
      16              : impl<'a> Formatter<'a> {
      17           90 :     pub(super) fn format_property_graph(&self, node: &'a Node) -> FmtReg {
      18           90 :         let f = &self.fmt;
      19           90 :         let [vertices, edges] =
      20           90 :             attrs!(self.children(node), [K::SQL_PROPERTY_GRAPH_VERTEX_TABLES, K::SQL_PROPERTY_GRAPH_EDGE_TABLES]);
      21           90 :         let mut sections = Vec::new();
      22           90 :         if let Some(vertices) = vertices.filter(|v| v.children_count() > 0) {
      23           45 :             sections.push(f.concat(&[f.text("vertex tables "), f.parenthesized(self.reg(vertices))]));
      24           63 :         }
      25           90 :         if let Some(edges) = edges.filter(|e| e.children_count() > 0) {
      26           42 :             sections.push(f.concat(&[f.text("edge tables "), f.parenthesized(self.reg(edges))]));
      27           60 :         }
      28           90 :         if sections.is_empty() {
      29              :             // An empty text, not the empty document
      30           45 :             return f.text("");
      31           45 :         }
      32           45 :         self.space_join(&sections, self.pretty_policy(FormattingJoinPolicy::BreakOnOverflow), false)
      33           90 :     }
      34              : 
      35          135 :     pub(super) fn format_graph_element_table(&self, node: &'a Node) -> FmtReg {
      36          135 :         let f = &self.fmt;
      37          135 :         let [table, key, source, destination, labels] = attrs!(self.children(node), [
      38              :                 K::SQL_GRAPH_ELEMENT_TABLE_TABLE,
      39              :                 K::SQL_GRAPH_ELEMENT_TABLE_KEY,
      40              :                 K::SQL_GRAPH_ELEMENT_TABLE_SOURCE,
      41              :                 K::SQL_GRAPH_ELEMENT_TABLE_DESTINATION,
      42              :                 K::SQL_GRAPH_ELEMENT_TABLE_LABELS,
      43              :             ],
      44              :         );
      45          135 :         let Some(table) = table else { return self.format_unimplemented(node) };
      46          135 :         let [table_name, table_alias, table_query] =
      47          135 :             attrs!(self.children(table), [K::SQL_TABLEREF_NAME, K::SQL_TABLEREF_ALIAS, K::SQL_TABLEREF_TABLE]);
      48          135 :         let mut table_reg = self.reg(table);
      49          135 :         if let Some(table_alias) = table_alias {
      50           42 :             let mut base_reg = match (table_name, table_query) {
      51           24 :                 (Some(table_name), _) => self.reg(table_name),
      52           18 :                 (None, Some(table_query)) => self.reg(table_query),
      53            0 :                 (None, None) => 0,
      54              :             };
      55           42 :             let alias_reg = self.reg(table_alias);
      56           42 :             if base_reg == 0 || alias_reg == 0 {
      57            0 :                 return self.format_unimplemented(node);
      58           42 :             }
      59           42 :             if table_query.is_some_and(|q| q.node_type() == NodeType::OBJECT_SQL_SELECT) {
      60           12 :                 base_reg = f.parenthesized(base_reg);
      61           30 :             }
      62           42 :             table_reg = f.concat(&[base_reg, f.text(" as "), alias_reg]);
      63           93 :         }
      64          135 :         let mut parts = vec![table_reg];
      65          135 :         if let Some(key) = key {
      66          135 :             parts.push(f.concat(&[f.text("key "), f.parenthesized(self.reg(key))]));
      67          135 :         }
      68          135 :         if let Some(source) = source {
      69           48 :             parts.push(f.concat(&[f.text("source "), self.reg(source)]));
      70           87 :         }
      71          135 :         if let Some(destination) = destination {
      72           48 :             parts.push(f.concat(&[f.text("destination "), self.reg(destination)]));
      73           87 :         }
      74          135 :         if let Some(labels) = labels {
      75          135 :             parts.extend(self.array_regs(labels));
      76          135 :         }
      77          135 :         self.space_join(&parts, self.pretty_policy(FormattingJoinPolicy::BreakOnOverflow), true)
      78          135 :     }
      79              : 
      80           96 :     pub(super) fn format_graph_vertex_reference(&self, node: &'a Node) -> FmtReg {
      81           96 :         let f = &self.fmt;
      82           96 :         let [name, key, columns] = attrs!(self.children(node), [
      83              :                 K::SQL_GRAPH_VERTEX_REFERENCE_NAME,
      84              :                 K::SQL_GRAPH_VERTEX_REFERENCE_KEY,
      85              :                 K::SQL_GRAPH_VERTEX_REFERENCE_COLUMNS,
      86              :             ],
      87              :         );
      88           96 :         let Some(name) = name else { return self.format_unimplemented(node) };
      89           96 :         let Some(key) = key else { return self.reg(name) };
      90           66 :         let mut result =
      91           66 :             f.concat(&[f.text("key "), f.parenthesized(self.reg(key)), f.text(" references "), self.reg(name)]);
      92           66 :         if let Some(columns) = columns.filter(|c| c.children_count() > 0) {
      93           18 :             result = f.concat(&[result, f.parenthesized(self.reg(columns))]);
      94           60 :         }
      95           66 :         result
      96           96 :     }
      97              : 
      98          138 :     pub(super) fn format_graph_label(&self, node: &'a Node) -> FmtReg {
      99          138 :         let f = &self.fmt;
     100          138 :         let [name, is_default, properties] =
     101          138 :             attrs!(self.children(node), [K::SQL_GRAPH_LABEL_NAME, K::SQL_GRAPH_LABEL_DEFAULT, K::SQL_GRAPH_LABEL_PROPERTIES]);
     102          138 :         let Some(properties) = properties else { return self.format_unimplemented(node) };
     103          138 :         if name.is_none() && !is_true(is_default) {
     104            0 :             return self.format_unimplemented(node);
     105          138 :         }
     106          138 :         let label = match name {
     107           75 :             Some(name) => f.concat(&[f.text("label "), self.reg(name)]),
     108           63 :             None => f.text("default label"),
     109              :         };
     110          138 :         f.concat(&[label, f.text(" "), self.reg(properties)])
     111          138 :     }
     112              : 
     113          138 :     pub(super) fn format_graph_properties(&self, node: &'a Node) -> FmtReg {
     114          138 :         let f = &self.fmt;
     115          138 :         let [all, columns, exclude] = attrs!(self.children(node), [K::SQL_GRAPH_PROPERTIES_ALL, K::SQL_GRAPH_PROPERTIES_COLUMNS, K::SQL_GRAPH_PROPERTIES_EXCLUDE],
     116              :         );
     117          138 :         if is_true(all) {
     118           78 :             let mut result = f.text("properties are all columns");
     119           78 :             if let Some(exclude) = exclude.filter(|e| e.children_count() > 0) {
     120           12 :                 result = f.concat(&[result, f.text(" except "), f.parenthesized(self.reg(exclude))]);
     121           66 :             }
     122           78 :             return result;
     123           60 :         }
     124           60 :         if let Some(columns) = columns {
     125           48 :             return f.concat(&[f.text("properties "), f.parenthesized(self.reg(columns))]);
     126           12 :         }
     127           12 :         f.text("no properties")
     128          138 :     }
     129              : 
     130           78 :     pub(super) fn format_graph_property(&self, node: &'a Node) -> FmtReg {
     131           78 :         let f = &self.fmt;
     132           78 :         let [value, name] = attrs!(self.children(node), [K::SQL_GRAPH_PROPERTY_VALUE, K::SQL_GRAPH_PROPERTY_NAME]);
     133           78 :         let Some(value) = value else { return self.format_unimplemented(node) };
     134           78 :         match name {
     135           54 :             Some(name) => f.concat(&[self.reg(value), f.text(" as "), self.reg(name)]),
     136           24 :             None => self.reg(value),
     137              :         }
     138           78 :     }
     139              : 
     140          105 :     pub(super) fn format_graph_table(&self, node: &'a Node) -> FmtReg {
     141          105 :         let f = &self.fmt;
     142          105 :         let [graph, graph_match, rows, columns] = attrs!(self.children(node), [K::SQL_GRAPH_TABLE_GRAPH, K::SQL_GRAPH_TABLE_MATCH, K::SQL_GRAPH_TABLE_ROWS, K::SQL_GRAPH_TABLE_COLUMNS],
     143              :         );
     144          105 :         let (Some(graph), Some(graph_match)) = (graph, graph_match) else { return self.format_unimplemented(node) };
     145          105 :         let mut clauses = vec![self.reg(graph), self.reg(graph_match)];
     146          105 :         if let Some(rows) = rows {
     147           18 :             clauses.push(self.reg(rows));
     148           87 :         }
     149          105 :         if let Some(columns) = columns {
     150           99 :             clauses.push(f.concat(&[f.text("columns "), f.parenthesized(self.reg(columns))]));
     151           99 :         }
     152          105 :         let body = self.space_join(&clauses, self.pretty_policy(FormattingJoinPolicy::BreakOnOverflow), true);
     153          105 :         if self.is_pretty() {
     154           35 :             return f.concat(&[
     155           35 :                 f.text("graph_table("),
     156           35 :                 f.indented(f.concat(&[f.break_(), body])),
     157           35 :                 f.break_(),
     158           35 :                 f.text(")"),
     159           35 :             ]);
     160           70 :         }
     161           70 :         f.concat(&[f.text("graph_table"), f.parenthesized(body)])
     162          105 :     }
     163              : 
     164          105 :     pub(super) fn format_graph_match(&self, node: &'a Node) -> FmtReg {
     165          105 :         let f = &self.fmt;
     166          105 :         let [patterns, where_clause] = attrs!(self.children(node), [K::SQL_GRAPH_MATCH_PATTERNS, K::SQL_GRAPH_MATCH_WHERE]);
     167          105 :         let Some(patterns) = patterns else { return self.format_unimplemented(node) };
     168          105 :         let result = f.concat(&[f.text("match "), self.reg(patterns)]);
     169          105 :         match where_clause {
     170           18 :             Some(where_clause) => f.concat(&[result, f.text(" where "), self.reg(where_clause)]),
     171           87 :             None => result,
     172              :         }
     173          105 :     }
     174              : 
     175          318 :     pub(super) fn format_graph_path_element(&self, node: &'a Node) -> FmtReg {
     176          318 :         let f = &self.fmt;
     177          318 :         let [vertex, left, right, any, variable, label, where_clause, quantifier] = attrs!(self.children(node), [
     178              :                 K::SQL_GRAPH_PATH_ELEMENT_VERTEX,
     179              :                 K::SQL_GRAPH_PATH_ELEMENT_EDGE_LEFT,
     180              :                 K::SQL_GRAPH_PATH_ELEMENT_EDGE_RIGHT,
     181              :                 K::SQL_GRAPH_PATH_ELEMENT_EDGE_ANY,
     182              :                 K::SQL_GRAPH_PATH_ELEMENT_VARIABLE,
     183              :                 K::SQL_GRAPH_PATH_ELEMENT_LABEL,
     184              :                 K::SQL_GRAPH_PATH_ELEMENT_WHERE,
     185              :                 K::SQL_GRAPH_PATH_ELEMENT_QUANTIFIER,
     186              :             ],
     187              :         );
     188          318 :         let mut body_parts = Vec::new();
     189          318 :         if let Some(variable) = variable {
     190          237 :             body_parts.push(self.reg(variable));
     191          237 :         }
     192          318 :         if let Some(label) = label {
     193          216 :             body_parts.push(f.concat(&[f.text(":"), self.reg(label)]));
     194          234 :         }
     195          318 :         if let Some(where_clause) = where_clause {
     196            6 :             body_parts.push(f.concat(&[f.text("where "), self.reg(where_clause)]));
     197          312 :         }
     198              :         // Path element bodies are short and must not wrap, breaks happen between path elements instead.
     199          318 :         let mut spaced_parts = Vec::new();
     200          459 :         for (i, &part) in body_parts.iter().enumerate() {
     201          459 :             if i > 0 {
     202          153 :                 spaced_parts.push(f.text(" "));
     203          306 :             }
     204          459 :             spaced_parts.push(part);
     205              :         }
     206          318 :         let body = f.concat(&spaced_parts);
     207          318 :         let result = if is_true(vertex) {
     208          213 :             if body == 0 {
     209            6 :                 f.text("()")
     210              :             } else {
     211          207 :                 f.concat(&[f.text("("), body, f.text(")")])
     212              :             }
     213          105 :         } else if is_true(left) {
     214           18 :             f.concat(&[f.text("<-["), body, f.text("]-")])
     215           87 :         } else if is_true(right) {
     216           84 :             f.concat(&[f.text("-["), body, f.text("]->")])
     217            3 :         } else if is_true(any) {
     218            3 :             f.concat(&[f.text("-["), body, f.text("]-")])
     219              :         } else {
     220            0 :             return self.format_unimplemented(node);
     221              :         };
     222          318 :         match quantifier {
     223           21 :             Some(quantifier) => f.concat(&[result, self.reg(quantifier)]),
     224          297 :             None => result,
     225              :         }
     226          318 :     }
     227              : 
     228           21 :     pub(super) fn format_graph_quantifier(&self, node: &'a Node) -> FmtReg {
     229           21 :         let f = &self.fmt;
     230           21 :         let [lower, upper, fixed] = attrs!(self.children(node), [K::SQL_GRAPH_QUANTIFIER_LOWER, K::SQL_GRAPH_QUANTIFIER_UPPER, K::SQL_GRAPH_QUANTIFIER_FIXED]);
     231           21 :         if is_true(fixed) {
     232            3 :             let Some(lower) = lower else { return self.format_unimplemented(node) };
     233            3 :             return f.concat(&[f.text("{"), self.reg(lower), f.text("}")]);
     234           18 :         }
     235           18 :         let lower_reg = lower.map(|l| self.reg(l)).unwrap_or(0);
     236           18 :         let upper_reg = upper.map(|u| self.reg(u)).unwrap_or(0);
     237           18 :         f.concat(&[f.text("{"), lower_reg, f.text(","), upper_reg, f.text("}")])
     238           21 :     }
     239              : 
     240          288 :     pub(super) fn format_graph_label_expression(&self, node: &'a Node) -> FmtReg {
     241          288 :         let f = &self.fmt;
     242          288 :         let [label, wildcard, negated, left, right, disjunction] = attrs!(self.children(node), [
     243              :                 K::SQL_GRAPH_LABEL_EXPRESSION_LABEL,
     244              :                 K::SQL_GRAPH_LABEL_EXPRESSION_WILDCARD,
     245              :                 K::SQL_GRAPH_LABEL_EXPRESSION_NEGATED,
     246              :                 K::SQL_GRAPH_LABEL_EXPRESSION_LEFT,
     247              :                 K::SQL_GRAPH_LABEL_EXPRESSION_RIGHT,
     248              :                 K::SQL_GRAPH_LABEL_EXPRESSION_DISJUNCTION,
     249              :             ],
     250              :         );
     251          288 :         if let Some(label) = label {
     252          243 :             return self.reg(label);
     253           45 :         }
     254           45 :         if is_true(wildcard) {
     255            3 :             return f.text("%");
     256           42 :         }
     257              :         // The operand of an operator needs parentheses if it binds weaker (or equally strong on the right)
     258           72 :         let operand = |node: &'a Node, level: u8, is_right: bool| {
     259           72 :             let operand_level = self.graph_label_expression_level(node);
     260           72 :             let reg = self.reg(node);
     261           72 :             if operand_level < level || (is_right && operand_level == level) {
     262            9 :                 f.parenthesized(reg)
     263              :             } else {
     264           63 :                 reg
     265              :             }
     266           72 :         };
     267           42 :         if is_true(negated) {
     268           12 :             return match left {
     269           12 :                 Some(left) => f.concat(&[f.text("!"), operand(left, GRAPH_LABEL_NEGATION, false)]),
     270            0 :                 None => self.format_unimplemented(node),
     271              :             };
     272           30 :         }
     273           30 :         let (Some(left), Some(right)) = (left, right) else { return self.format_unimplemented(node) };
     274           30 :         let (op, level) = if is_true(disjunction) {
     275           15 :             (f.text(" | "), GRAPH_LABEL_DISJUNCTION)
     276              :         } else {
     277           15 :             (f.text(" & "), GRAPH_LABEL_CONJUNCTION)
     278              :         };
     279           30 :         f.concat(&[operand(left, level, false), op, operand(right, level, true)])
     280          288 :     }
     281              : 
     282              :     /// How strong a label expression binds, atoms bind strongest
     283           72 :     fn graph_label_expression_level(&self, node: &Node) -> u8 {
     284           72 :         if node.node_type() != NodeType::OBJECT_SQL_GRAPH_LABEL_EXPRESSION {
     285            0 :             return GRAPH_LABEL_ATOM;
     286           72 :         }
     287           72 :         let [negated, disjunction, right] = attrs!(self.children(node), [
     288              :             K::SQL_GRAPH_LABEL_EXPRESSION_NEGATED,
     289              :             K::SQL_GRAPH_LABEL_EXPRESSION_DISJUNCTION,
     290              :             K::SQL_GRAPH_LABEL_EXPRESSION_RIGHT,
     291              :         ]);
     292           72 :         if is_true(negated) {
     293            6 :             GRAPH_LABEL_NEGATION
     294           66 :         } else if right.is_none() {
     295           54 :             GRAPH_LABEL_ATOM
     296           12 :         } else if is_true(disjunction) {
     297            9 :             GRAPH_LABEL_DISJUNCTION
     298              :         } else {
     299            3 :             GRAPH_LABEL_CONJUNCTION
     300              :         }
     301           72 :     }
     302              : 
     303           18 :     pub(super) fn format_graph_rows(&self, node: &'a Node) -> FmtReg {
     304           18 :         let f = &self.fmt;
     305           18 :         let [per_match, per_step, source, edge, destination] = attrs!(self.children(node), [
     306              :                 K::SQL_GRAPH_ROWS_PER_MATCH,
     307              :                 K::SQL_GRAPH_ROWS_PER_STEP,
     308              :                 K::SQL_GRAPH_ROWS_SOURCE,
     309              :                 K::SQL_GRAPH_ROWS_EDGE,
     310              :                 K::SQL_GRAPH_ROWS_DESTINATION,
     311              :             ],
     312              :         );
     313           18 :         if is_true(per_match) {
     314            9 :             return f.text("one row per match");
     315            9 :         }
     316            9 :         let (Some(source), Some(edge), Some(destination)) = (source, edge, destination) else {
     317            0 :             return self.format_unimplemented(node);
     318              :         };
     319            9 :         if !is_true(per_step) {
     320            0 :             return self.format_unimplemented(node);
     321            9 :         }
     322            9 :         let vars = f.concat(&[self.reg(source), f.text(", "), self.reg(edge), f.text(", "), self.reg(destination)]);
     323            9 :         f.concat(&[f.text("one row per step"), f.parenthesized(vars)])
     324           18 :     }
     325              : }
        

Generated by: LCOV version 2.0-1