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

            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         6262 : fn is_enum_node(node_type: NodeType) -> bool {
      17         6262 :     let id = node_type.0;
      18         6262 :     id > NodeType::ENUM_KEYS_.0 && id < NodeType::OBJECT_KEYS_.0
      19         6262 : }
      20              : 
      21         3544 : fn append_text(out: &mut Vec<u8>, text: &[u8]) {
      22         3544 :     out.extend_from_slice(&(text.len() as u64).to_le_bytes());
      23         3544 :     out.extend_from_slice(text);
      24         3544 : }
      25              : 
      26        15287 : fn append_node(parsed: &ParsedScript, node: &Node, out: &mut Vec<u8>) {
      27        15287 :     let node_type = node.node_type();
      28        15287 :     out.extend_from_slice(&node_type.0.to_le_bytes());
      29        15287 :     out.extend_from_slice(&node.attribute_key().0.to_le_bytes());
      30         6262 :     match node_type {
      31            1 :         NodeType::NONE | NodeType::LITERAL_NULL => {}
      32          610 :         NodeType::BOOL => out.push((node.children_begin_or_value() != 0) as u8),
      33         2708 :         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          836 :         | NodeType::LITERAL_INTERVAL => {
      39          836 :             let scanned = &parsed.scanned;
      40          836 :             let span = resolve_text_span(&scanned.text, &scanned.symbols, *node.symbol_span());
      41          836 :             let bytes = scanned.text.as_bytes();
      42          836 :             let begin = (span.offset as usize).min(bytes.len());
      43          836 :             let end = (begin + span.length as usize).min(bytes.len());
      44          836 :             append_text(out, &bytes[begin..end]);
      45          836 :         }
      46         4870 :         NodeType::ARRAY => out.extend_from_slice(&(node.children_count()).to_le_bytes()),
      47         6262 :         _ if is_enum_node(node_type) => out.extend_from_slice(&node.children_begin_or_value().to_le_bytes()),
      48         5851 :         _ => out.extend_from_slice(&(node.children_count()).to_le_bytes()),
      49              :     }
      50        15287 : }
      51              : 
      52              : /// Two FNV-1a style 64 bit hashes over the bytes
      53         2227 : fn hash(bytes: &[u8]) -> [u64; 2] {
      54         2227 :     let mut digest = [14695981039346656037u64, 7809847782465536322u64];
      55       177173 :     for &b in bytes {
      56       177173 :         digest[0] = (digest[0] ^ b as u64).wrapping_mul(1099511628211);
      57       177173 :         digest[1] = (digest[1] ^ b.wrapping_add(0x9d) as u64).wrapping_mul(1099511628211);
      58       177173 :     }
      59         2227 :     digest
      60         2227 : }
      61              : 
      62              : /// Compute the signature of a parsed script. Fails for scripts with scanner or parser errors.
      63          963 : pub fn compute_signature(parsed: &ParsedScript) -> Result<String> {
      64          963 :     if !parsed.scanned.errors.is_empty() || !parsed.errors.is_empty() {
      65           18 :         return Err(Error::status(StatusCode::SCRIPT_NOT_PARSED));
      66          945 :     }
      67          945 :     let mut script_bytes = Vec::new();
      68          945 :     script_bytes.extend_from_slice(b"DQLSIG");
      69          945 :     script_bytes.push(SCRIPT_SIGNATURE_VERSION);
      70          945 :     script_bytes.extend_from_slice(&(parsed.statements.len() as u32).to_le_bytes());
      71          945 :     let mut statement_bytes = Vec::new();
      72         1282 :     for statement in &parsed.statements {
      73         1282 :         statement_bytes.clear();
      74         1282 :         let begin = statement.nodes_begin as usize;
      75        15287 :         for node in &parsed.nodes[begin..begin + statement.node_count as usize] {
      76        15287 :             append_node(parsed, node, &mut statement_bytes);
      77        15287 :         }
      78         1282 :         let digest = hash(&statement_bytes);
      79         1282 :         script_bytes.push(statement.statement_type.0);
      80         1282 :         script_bytes.extend_from_slice(&digest[0].to_le_bytes());
      81         1282 :         script_bytes.extend_from_slice(&digest[1].to_le_bytes());
      82              :     }
      83          945 :     let digest = hash(&script_bytes);
      84          945 :     Ok(format!("{:016x}{:016x}", digest[0], digest[1]))
      85          963 : }
        

Generated by: LCOV version 2.0-1