#![allow(missing_docs)]
use std::collections::BTreeMap;
use std::sync::Arc;
use cyrs_schema::SchemaProvider;
use cyrs_sema::ty::Type;
use smol_str::SmolStr;
use crate::{CypherDb, DialectMode};
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct AnalysisOptions {
pub dialect: DialectMode,
pub warn_shadowing: bool,
pub parameter_hints: BTreeMap<SmolStr, Type>,
}
impl AnalysisOptions {
#[must_use]
pub fn digest(&self) -> u64 {
let mut h = Fnv64::new();
h.write_u8(match self.dialect {
DialectMode::GqlAligned => 0,
DialectMode::OpenCypherV9 => 1,
});
h.write_u8(u8::from(self.warn_shadowing));
h.write_u64(self.parameter_hints.len() as u64);
for (key, ty) in &self.parameter_hints {
let kb = key.as_bytes();
h.write_u64(kb.len() as u64);
h.write_bytes(kb);
h.write_u8(type_tag(ty));
}
h.finish()
}
}
fn type_tag(ty: &Type) -> u8 {
match ty {
Type::Any => 0,
Type::Null => 1,
Type::Bool => 2,
Type::Int => 3,
Type::Float => 4,
Type::Num => 5,
Type::String => 6,
Type::Date => 7,
Type::Datetime => 8,
Type::List(_) => 9,
Type::Map(_) => 10,
Type::Node(_) => 11,
Type::Relationship(_) => 12,
Type::Path => 13,
Type::Union(_) => 14,
Type::Unknown => 15,
}
}
struct Fnv64(u64);
const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
impl Fnv64 {
fn new() -> Self {
Self(FNV_OFFSET)
}
fn write_u8(&mut self, b: u8) {
self.0 ^= u64::from(b);
self.0 = self.0.wrapping_mul(FNV_PRIME);
}
fn write_u64(&mut self, v: u64) {
for b in v.to_le_bytes() {
self.write_u8(b);
}
}
fn write_bytes(&mut self, bs: &[u8]) {
for &b in bs {
self.write_u8(b);
}
}
fn finish(self) -> u64 {
self.0
}
}
#[allow(missing_docs)]
#[salsa::input]
pub struct FileOptions {
pub options: AnalysisOptions,
}
#[allow(missing_docs)]
#[salsa::input]
pub struct WorkspaceInputs {
pub schema: Option<Arc<dyn SchemaProvider>>,
}
#[salsa::tracked]
pub fn options_digest(db: &dyn CypherDb, file_opts: FileOptions) -> u64 {
file_opts.options(db).digest()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::CypherDatabase;
use salsa::Setter as _;
#[test]
fn digest_is_order_independent() {
let mut hints_a = BTreeMap::new();
hints_a.insert(SmolStr::new("x"), Type::Int);
hints_a.insert(SmolStr::new("y"), Type::String);
let mut hints_b = BTreeMap::new();
hints_b.insert(SmolStr::new("y"), Type::String);
hints_b.insert(SmolStr::new("x"), Type::Int);
let a = AnalysisOptions {
dialect: DialectMode::GqlAligned,
warn_shadowing: false,
parameter_hints: hints_a,
};
let b = AnalysisOptions {
dialect: DialectMode::GqlAligned,
warn_shadowing: false,
parameter_hints: hints_b,
};
assert_eq!(
a.digest(),
b.digest(),
"digest must be independent of insertion order"
);
}
#[test]
fn digest_changes_on_warn_shadowing_toggle() {
let opts_false = AnalysisOptions {
warn_shadowing: false,
..Default::default()
};
let opts_true = AnalysisOptions {
warn_shadowing: true,
..Default::default()
};
assert_ne!(
opts_false.digest(),
opts_true.digest(),
"toggling warn_shadowing must change the digest"
);
}
#[test]
fn digest_changes_on_hint_addition() {
let base = AnalysisOptions::default();
let mut hints = BTreeMap::new();
hints.insert(SmolStr::new("p"), Type::Bool);
let with_hint = AnalysisOptions {
parameter_hints: hints,
..Default::default()
};
assert_ne!(
base.digest(),
with_hint.digest(),
"adding a parameter hint must change the digest"
);
}
#[test]
fn digest_changes_on_hint_type_change() {
let mut hints_int = BTreeMap::new();
hints_int.insert(SmolStr::new("p"), Type::Int);
let opts_int = AnalysisOptions {
parameter_hints: hints_int,
..Default::default()
};
let mut hints_str = BTreeMap::new();
hints_str.insert(SmolStr::new("p"), Type::String);
let opts_str = AnalysisOptions {
parameter_hints: hints_str,
..Default::default()
};
assert_ne!(
opts_int.digest(),
opts_str.digest(),
"changing a hint type must change the digest"
);
}
#[test]
fn digest_changes_on_dialect_change() {
let gql = AnalysisOptions {
dialect: DialectMode::GqlAligned,
..Default::default()
};
let oc = AnalysisOptions {
dialect: DialectMode::OpenCypherV9,
..Default::default()
};
assert_ne!(
gql.digest(),
oc.digest(),
"changing dialect must change the digest"
);
}
#[test]
fn options_digest_invalidates_on_dialect_change() {
let mut db = CypherDatabase::new();
let file_opts = FileOptions::new(
&db,
AnalysisOptions {
dialect: DialectMode::GqlAligned,
..Default::default()
},
);
let d1 = options_digest(&db, file_opts);
file_opts.set_options(&mut db).to(AnalysisOptions {
dialect: DialectMode::OpenCypherV9,
..Default::default()
});
let d2 = options_digest(&db, file_opts);
assert_ne!(d1, d2, "options_digest must change after dialect mutation");
}
#[test]
fn options_digest_stable_for_equal_options() {
let db = CypherDatabase::new();
let mut hints_a = BTreeMap::new();
hints_a.insert(SmolStr::new("x"), Type::Int);
hints_a.insert(SmolStr::new("y"), Type::String);
let mut hints_b = BTreeMap::new();
hints_b.insert(SmolStr::new("y"), Type::String); hints_b.insert(SmolStr::new("x"), Type::Int);
let file_opts_a = FileOptions::new(
&db,
AnalysisOptions {
parameter_hints: hints_a,
..Default::default()
},
);
let file_opts_b = FileOptions::new(
&db,
AnalysisOptions {
parameter_hints: hints_b,
..Default::default()
},
);
let d_a = options_digest(&db, file_opts_a);
let d_b = options_digest(&db, file_opts_b);
assert_eq!(
d_a, d_b,
"equal options (any insertion order) must produce the same digest"
);
}
#[test]
fn options_digest_invalidates_on_hint_change() {
let mut db = CypherDatabase::new();
let mut hints = BTreeMap::new();
hints.insert(SmolStr::new("p"), Type::Int);
let file_opts = FileOptions::new(
&db,
AnalysisOptions {
parameter_hints: hints,
..Default::default()
},
);
let d1 = options_digest(&db, file_opts);
let mut new_hints = BTreeMap::new();
new_hints.insert(SmolStr::new("p"), Type::String);
file_opts.set_options(&mut db).to(AnalysisOptions {
parameter_hints: new_hints,
..Default::default()
});
let d2 = options_digest(&db, file_opts);
assert_ne!(
d1, d2,
"changing a parameter hint must invalidate options_digest"
);
}
#[test]
fn workspace_inputs_none_schema() {
let db = CypherDatabase::new();
let ws = WorkspaceInputs::new(&db, None);
assert!(ws.schema(&db).is_none());
}
#[test]
fn workspace_inputs_schema_round_trip() {
use cyrs_schema::EmptySchema;
let mut db = CypherDatabase::new();
let ws = WorkspaceInputs::new(&db, None);
assert!(ws.schema(&db).is_none());
let schema: Arc<dyn SchemaProvider> = Arc::new(EmptySchema);
ws.set_schema(&mut db).to(Some(schema.clone()));
assert!(ws.schema(&db).is_some());
}
}