LCOV - code coverage report
Current view: top level - dashql-core/src - signature.rs Coverage Total Hit
Test: DashQL d805a4b Lines: 100.0 % 58 58
Test Date: 2026-11-09 20:22:13

            Line data    Source code
       1              : //! Script signatures, the persisted cache keys of compiled queries.
       2              : //!
       3              : //! The signature hashes the statement types and, per statement, the node types, attribute keys,
       4              : //! names and literal texts in node order. Signatures are persisted, so bump
       5              : //! `SCRIPT_SIGNATURE_VERSION` when the encoding changes.
       6              : 
       7              : use crate::error::{Error, Result};
       8              : use dashql_buffers::parser::{Node, NodeType};
       9              : use dashql_buffers::status::StatusCode;
      10              : use dashql_parser::parser::context::resolve_text_span;
      11              : use dashql_parser::parser::ParsedScript;
      12              : 
      13              : /// The version of the signature encoding
      14              : const SCRIPT_SIGNATURE_VERSION: u8 = 2;
      15              : 
      16         6659 : fn is_enum_node(node_type: NodeType) -> bool {
      17         6659 :     let id = node_type.0;
      18         6659 :     id > NodeType::ENUM_KEYS_.0 && id < NodeType::OBJECT_KEYS_.0
      19         6659 : }
      20              : 
      21         4018 : fn append_text(out: &mut Vec<u8>, text: &[u8]) {
      22         4018 :     out.extend_from_slice(&(text.len() as u64).to_le_bytes());
      23         4018 :     out.extend_from_slice(text);
      24         4018 : }
      25              : 
      26        16436 : fn append_node(parsed: &ParsedScript, node: &Node, out: &mut Vec<u8>) {
      27        16436 :     let node_type = node.node_type();
      28        16436 :     out.extend_from_slice(&node_type.0.to_le_bytes());
      29        16436 :     out.extend_from_slice(&node.attribute_key().0.to_le_bytes());
      30         6659 :     match node_type {
      31            1 :         NodeType::NONE | NodeType::LITERAL_NULL => {}
      32          617 :         NodeType::BOOL => out.push((node.children_begin_or_value() != 0) as u8),
      33         3032 :         NodeType::NAME => append_text(out, parsed.scanned.names.get(node.children_begin_or_value()).text.as_bytes()),
      34              :         NodeType::OPERATOR
      35              :         | NodeType::LITERAL_INTEGER
      36              :         | NodeType::LITERAL_FLOAT
      37              :         | NodeType::LITERAL_STRING
      38          986 :         | NodeType::LITERAL_INTERVAL => {
      39          986 :             let scanned = &parsed.scanned;
      40          986 :             let span = resolve_text_span(&scanned.text, &scanned.symbols, *node.symbol_span());
      41          986 :             let bytes = scanned.text.as_bytes();
      42          986 :             let begin = (span.offset as usize).min(bytes.len());
      43          986 :             let end = (begin + span.length as usize).min(bytes.len());
      44          986 :             append_text(out, &bytes[begin..end]);
      45          986 :         }
      46         5141 :         NodeType::ARRAY => out.extend_from_slice(&(node.children_count()).to_le_bytes()),
      47         6659 :         _ if is_enum_node(node_type) => out.extend_from_slice(&node.children_begin_or_value().to_le_bytes()),
      48         6241 :         _ => out.extend_from_slice(&(node.children_count()).to_le_bytes()),
      49              :     }
      50        16436 : }
      51              : 
      52              : /// Two FNV-1a style 64 bit hashes over the bytes
      53         2396 : fn hash(bytes: &[u8]) -> [u64; 2] {
      54         2396 :     let mut digest = [14695981039346656037u64, 7809847782465536322u64];
      55       193290 :     for &b in bytes {
      56       193290 :         digest[0] = (digest[0] ^ b as u64).wrapping_mul(1099511628211);
      57       193290 :         digest[1] = (digest[1] ^ b.wrapping_add(0x9d) as u64).wrapping_mul(1099511628211);
      58       193290 :     }
      59         2396 :     digest
      60         2396 : }
      61              : 
      62              : /// Compute the signature of a parsed script. Fails for scripts with scanner or parser errors.
      63         1030 : pub fn compute_signature(parsed: &ParsedScript) -> Result<String> {
      64         1030 :     if !parsed.scanned.errors.is_empty() || !parsed.errors.is_empty() {
      65           18 :         return Err(Error::status(StatusCode::SCRIPT_NOT_PARSED));
      66         1012 :     }
      67         1012 :     let mut script_bytes = Vec::new();
      68         1012 :     script_bytes.extend_from_slice(b"DQLSIG");
      69         1012 :     script_bytes.push(SCRIPT_SIGNATURE_VERSION);
      70         1012 :     script_bytes.extend_from_slice(&(parsed.statements.len() as u32).to_le_bytes());
      71         1012 :     let mut statement_bytes = Vec::new();
      72         1384 :     for statement in &parsed.statements {
      73         1384 :         statement_bytes.clear();
      74         1384 :         let begin = statement.nodes_begin as usize;
      75        16436 :         for node in &parsed.nodes[begin..begin + statement.node_count as usize] {
      76        16436 :             append_node(parsed, node, &mut statement_bytes);
      77        16436 :         }
      78         1384 :         let digest = hash(&statement_bytes);
      79         1384 :         script_bytes.push(statement.statement_type.0);
      80         1384 :         script_bytes.extend_from_slice(&digest[0].to_le_bytes());
      81         1384 :         script_bytes.extend_from_slice(&digest[1].to_le_bytes());
      82              :     }
      83         1012 :     let digest = hash(&script_bytes);
      84         1012 :     Ok(format!("{:016x}{:016x}", digest[0], digest[1]))
      85         1030 : }
        

Generated by: LCOV version 2.0-1