LCOV - code coverage report
Current view: top level - dashql-core-session/src - touched_relations.rs Coverage Total Hit
Test: DashQL d805a4b Lines: 94.7 % 95 90
Test Date: 2026-11-09 20:22:13

            Line data    Source code
       1              : //! The relations that statements create, change or drop.
       2              : //!
       3              : //! The catalog of a connection holds what the connection reports. After a successful statement the host
       4              : //! asks the connection about the relations that the statement touched, the parser names them. A text
       5              : //! that doesn't parse touches nothing that we know of.
       6              : 
       7              : use dashql_buffers::analyzer::QualifiedTableNameT;
       8              : use dashql_buffers::catalog::TouchedRelationsT;
       9              : use dashql_buffers::parser::{AttributeKey, Node, NodeType, StatementType};
      10              : use dashql_core::catalog::Catalog;
      11              : use dashql_core::pack::{pack_parsed_statement, read_name_path};
      12              : use dashql_core::script::Script;
      13              : use dashql_parser::parser::ParsedScript;
      14              : use dashql_parser::parser::context::resolve_text_span;
      15              : 
      16              : /// A relation name from its parts, an empty part is not named
      17          778 : fn relation_name(database: Option<String>, schema: Option<String>, table: String) -> QualifiedTableNameT {
      18          778 :     let mut name = QualifiedTableNameT::default();
      19          778 :     name.database_name = database;
      20          778 :     name.schema_name = schema;
      21          778 :     name.table_name = Some(table);
      22          778 :     name
      23          778 : }
      24              : 
      25              : /// Do the options of an `EXPLAIN` run its statement? Unknown arguments of `analyze` count as running it.
      26           80 : pub fn explain_executes(parsed: &ParsedScript, options: Option<&Node>) -> bool {
      27           80 :     let scanned = &parsed.scanned;
      28           80 :     let option_text = |n: Option<&Node>| -> String {
      29           38 :         let Some(n) = n.filter(|n| n.node_type() != NodeType::NONE) else { return String::new() };
      30           38 :         let span = resolve_text_span(&scanned.text, &scanned.symbols, *n.symbol_span());
      31           38 :         let text = scanned.text[span.offset as usize..(span.offset + span.length) as usize].to_ascii_lowercase();
      32           38 :         let b = text.as_bytes();
      33           38 :         if b.len() >= 2 && (b[0] == b'\'' || b[0] == b'"') && b[b.len() - 1] == b[0] {
      34            3 :             return text[1..text.len() - 1].to_string();
      35           35 :         }
      36           35 :         text
      37           38 :     };
      38           80 :     let mut executes = false;
      39           80 :     for option in options.filter(|o| o.node_type() == NodeType::ARRAY).iter().flat_map(|o| parsed.children(o)) {
      40           35 :         if option.node_type() != NodeType::OBJECT_SQL_GENERIC_OPTION {
      41            0 :             continue;
      42           35 :         }
      43           35 :         let key = attribute_of(parsed, option, AttributeKey::SQL_GENERIC_OPTION_KEY);
      44           35 :         let value = attribute_of(parsed, option, AttributeKey::SQL_GENERIC_OPTION_VALUE);
      45           35 :         let name = option_text(key);
      46           35 :         if name == "analyze" || name == "analyse" {
      47           26 :             executes |= value.is_none_or(|v| v.node_type() != NodeType::BOOL || v.children_begin_or_value() != 0);
      48            9 :         } else if name == "format" && matches!(option_text(value).as_str(), "analyze" | "analyse") {
      49            3 :             executes = true;
      50            6 :         }
      51              :     }
      52           80 :     executes
      53           80 : }
      54              : 
      55              : /// The child of a node with an attribute key
      56          140 : fn attribute_of<'a>(parsed: &'a ParsedScript, node: &Node, key: AttributeKey) -> Option<&'a Node> {
      57          216 :     parsed.children(node).iter().find(|c| c.attribute_key() == key)
      58          140 : }
      59              : 
      60              : /// A name from the parts of a name path, an empty table names a whole schema
      61            8 : fn name_of_path(path: &[String], database: &Option<String>, schema: &Option<String>) -> Option<QualifiedTableNameT> {
      62            8 :     match path {
      63            6 :         [name] => Some(relation_name(database.clone(), schema.clone(), name.clone())),
      64            2 :         [new_schema, name] => Some(relation_name(database.clone(), Some(new_schema.clone()), name.clone())),
      65            0 :         [new_database, new_schema, name] => Some(relation_name(Some(new_database.clone()), Some(new_schema.clone()), name.clone())),
      66            0 :         _ => None,
      67              :     }
      68            8 : }
      69              : 
      70              : /// The relations that a statement touches, named as written. A statement that moves a relation touches its old and its
      71              : /// new name, one that changes a schema touches everything in it: its name has an empty table. Empty for other statements
      72              : /// and for a statement whose relation the parser could not name.
      73         1209 : pub fn touched_relations_of_statement(parsed: &ParsedScript, statement_id: usize) -> Vec<QualifiedTableNameT> {
      74         1209 :     let Some(statement) = parsed.statements.get(statement_id) else { return Vec::new() };
      75         1209 :     let root = &parsed.nodes[statement.root as usize];
      76         1209 :     let read = |node: &Node| read_name_path(parsed, node);
      77         1213 :     let attribute = |node: &Node, key: AttributeKey| attribute_of(parsed, node, key);
      78         1209 :     match statement.statement_type {
      79              :         StatementType::CREATE_TABLE
      80              :         | StatementType::CREATE_TABLE_AS
      81              :         | StatementType::CREATE_VIEW
      82              :         | StatementType::SELECT_INTO
      83              :         | StatementType::DROP_TABLE
      84              :         | StatementType::DROP_VIEW
      85              :         | StatementType::ALTER_TABLE
      86          758 :         | StatementType::ALTER_VIEW => {}
      87              :         StatementType::DROP_SCHEMA | StatementType::ALTER_SCHEMA => {
      88            6 :             let Some(path) = attribute(root, AttributeKey::SQL_SCHEMA_NAME).map(read) else { return Vec::new() };
      89            6 :             let (database, schema) = match path.as_slice() {
      90            5 :                 [schema] => (None, schema.clone()),
      91            1 :                 [database, schema] => (Some(database.clone()), schema.clone()),
      92            0 :                 _ => return Vec::new(),
      93              :             };
      94            6 :             let mut out = vec![relation_name(database.clone(), Some(schema), String::new())];
      95            6 :             if let Some([new_schema]) = attribute(root, AttributeKey::SQL_SCHEMA_NEW_NAME).map(read).as_deref() {
      96            2 :                 out.push(relation_name(database, Some(new_schema.clone()), String::new()));
      97            4 :             }
      98            6 :             return out;
      99              :         }
     100              :         // A statement that `EXPLAIN` runs creates its table
     101              :         StatementType::EXPLAIN => {
     102            5 :             let options = attribute(root, AttributeKey::EXT_EXPLAIN_OPTIONS);
     103            5 :             let body = attribute(root, AttributeKey::EXT_EXPLAIN_STATEMENT);
     104            5 :             let Some(body) = body.filter(|b| b.node_type() == NodeType::OBJECT_SQL_CREATE_AS && explain_executes(parsed, options)) else { return Vec::new() };
     105            1 :             let Some(path) = attribute(body, AttributeKey::SQL_CREATE_AS_NAME).map(read) else { return Vec::new() };
     106            1 :             return name_of_path(&path, &None, &None).into_iter().collect();
     107              :         }
     108          440 :         _ => return Vec::new(),
     109              :     }
     110          758 :     let Some(target) = pack_parsed_statement(parsed, statement_id).target else { return Vec::new() };
     111          758 :     let Some(table) = target.relation_name.clone().filter(|n| !n.is_empty()) else { return Vec::new() };
     112          758 :     let (database, schema) = (target.database_name.clone(), target.schema_name.clone());
     113          758 :     let mut out = vec![relation_name(database.clone(), schema.clone(), table.clone())];
     114          758 :     if matches!(statement.statement_type, StatementType::ALTER_TABLE | StatementType::ALTER_VIEW) {
     115           36 :         let actions = attribute(root, AttributeKey::SQL_ALTER_TABLE_ACTIONS);
     116           40 :         for action in actions.iter().flat_map(|actions| parsed.children(actions)) {
     117           40 :             match action.node_type() {
     118              :                 // A new name without a schema stays where the relation is
     119              :                 NodeType::OBJECT_SQL_ALTER_RENAME_TABLE => {
     120            7 :                     if let Some(path) = attribute(action, AttributeKey::SQL_ALTER_ACTION_NEW_TABLE).map(read) {
     121            7 :                         out.extend(name_of_path(&path, &database, &schema));
     122            7 :                     }
     123              :                 }
     124              :                 NodeType::OBJECT_SQL_ALTER_SET_SCHEMA => {
     125            4 :                     if let Some([new_schema]) = attribute(action, AttributeKey::SQL_ALTER_ACTION_NEW_NAME).map(read).as_deref() {
     126            4 :                         out.push(relation_name(database.clone(), Some(new_schema.clone()), table.clone()));
     127            4 :                     }
     128              :                 }
     129           29 :                 _ => {}
     130              :             }
     131              :         }
     132          722 :     }
     133          758 :     out
     134         1209 : }
     135              : 
     136              : /// The relations that the statements of a text touch
     137           48 : pub fn touched_relations(text: &str) -> TouchedRelationsT {
     138           48 :     let mut script = Script::new(&Catalog::new());
     139           48 :     script.replace_text(text);
     140           48 :     script.parse();
     141           48 :     let mut touched = TouchedRelationsT::default();
     142           48 :     let Some(parsed) = script.parsed.as_ref() else {
     143            0 :         return touched;
     144              :     };
     145           51 :     touched.relations = Some((0..parsed.statements.len()).flat_map(|id| touched_relations_of_statement(parsed, id)).collect());
     146           48 :     touched
     147           48 : }
        

Generated by: LCOV version 2.0-1