Skip to content
You are viewing the corvid 0.3.0 release snapshot — frozen at the 0.3.0 engine release.Current documentation

Equality semantics

“Equal” means different things in different places, on purpose. The engine pins each rule with conformance tests; this page is the consolidated table.

ConstructEquality rule
Predicates (eq/ne, ordered ops, is_in, between)Typed total-order comparison: NaN never equals anything (not even NaN); Int(2) equals Float(2.0) numerically — mixed comparisons convert the integer through f64, exact up to 2^53.
compare_and_set expected valueSemantic value equality: NaN == NaN regardless of payload, −0.0 == 0.0, containers element-wise.
Unique constraintsStorage-level semantic equality (NaN == NaN), enforced per field value on write.
JoinsAn Int foreign key matches a Text key via its decimal-string encoding: Int(7) joins to the key "7".
Group keys (group_count/group_sum/group_avg, count_distinct)Type-tagged canonical keys: bare for text (blog), i:/f:/b: tags for non-text, t: escape for text that would look tagged — distinct types stay distinct.

Two rules coexist by design:

  • Predicate comparisons — NaN matches nothing, not even NaN. A NaN filter value selects the empty set under eq and ordered operators; ne selects everything else. This keeps filter semantics a clean total order without an NaN special case.
  • Storage equalitycompare_and_set expectations and unique constraints treat NaN as equal to NaN (payload-agnostic) and −0.0 equal to 0.0. These constructs answer “is the stored value this value?”, where rejecting NaN would make NaN-valued fields (common in float pipelines) unusable.
# use corvid::{Db, Value, field};
# use std::collections::BTreeMap;
# let db = Db::open_in_memory()?; let c = db.collection("t");
// field("v") traverses maps, so each doc carries its value under "v"
let doc = |v: Value| {
let mut m = BTreeMap::new();
m.insert("v".to_string(), v);
Value::Map(m)
};
// Predicate: NaN selects nothing, ne selects the rest
c.insert(b"a", &doc(Value::Float(f64::NAN)))?;
c.insert(b"b", &doc(Value::Int(2)))?;
c.insert(b"c", &doc(Value::Float(2.0)))?;
c.query().filter(field("v").eq(Value::Float(f64::NAN))).count()?; // 0
c.query().filter(field("v").eq(Value::Int(2))).count()?; // 2 — numeric interop
c.query().filter(field("v").ne(Value::Int(2))).count()?; // 1 (the NaN doc)
// Storage equality: NaN matches NaN in compare_and_set, element-wise
// inside containers too — this deletes doc "a"
let matched = c.compare_and_set(b"a", Some(&doc(Value::Float(f64::NAN))), None)?; // true
# let _ = matched;
# Ok::<(), corvid::Error>(())

Unique constraints make NaN==NaN visible too: a second document with NaN in a unique Float field is rejected with Error::SchemaViolation, whether or not the payloads differ.

Join foreign keys are document values; the target is a byte key. Text foreign keys compare as bytes; an Int foreign key encodes through its decimal string (Int(7)"7") so integer ids join text keys naturally. Unusable shapes (containers, vectors, floats) retain the row with a None right side — a lookup join never errors on data shape, it misses.

Next: ordering rules.