Qubit Metadata
A general-purpose, type-safe metadata model for Rust.
Overview
qubit-metadata provides a Metadata type for attaching explicitly typed
extension fields to data objects without hard-coding every auxiliary field into
the core model. Typical uses include:
- Document ingestion: keep
file_id,chunk_index,language,source, andconfidencewith each document chunk. - Vector search: store fields such as
tenant_id,doc_type,created_at,score, oracl_groupthat later become vector-database metadata columns or payload filters. - Event and message pipelines: carry
trace_id,request_id,tenant_id,route, and retry metadata through services. - External service integration: retain compact service result fields such as model version, latency, billing tag, and request ID for diagnostics and analytics.
Metadata stores values as qubit_value::Value, so scalar types remain clear:
i64 is different from u32, and f64 is different from String. If a caller
really needs nested data, it can store Value::Json explicitly. Common document
metadata, vector-database metadata, and request context are usually flat typed
field sets, which keeps schema validation and filter construction predictable.
Design Goals
- Typed values: preserve concrete runtime types with
qubit_value::Value. - Convenient construction: support both mutable
set()and fluentwith(). - Optional schema: validate field names, required fields, concrete
DataTypes, and filter compatibility. - Filter builder: build immutable
MetadataFiltervalues with a chainable builder. - Serde support: serialize metadata, schemas, and filters for config, storage, and service boundaries.
Features
1) Typed metadata storage
Metadata is an ordered String -> Value map. It supports typed get, set,
try_get, schema-checked set_checked / with_checked, fluent with,
iteration, merge, retain, and conversion to/from BTreeMap<String, Value>.
use Metadata;
let meta = new
.with
.with
.with;
assert_eq!;
assert_eq!;
2) Schema for validation and storage planning
MetadataSchema uses qubit_datatype::DataType. This is useful when a storage
backend requires metadata fields to be declared in advance, and it also lets the
filter builder validate field/operator compatibility early. Its
UnknownFieldPolicy applies to both metadata validation and filter validation:
declared fields are still checked strictly, while unknown filter fields are
accepted only when the policy is Allow.
use DataType;
use ;
let schema = builder
.required
.required
.optional
.build;
let meta = new
.with
.with;
schema.validate.unwrap;
3) Immutable filters built by builder
MetadataFilter::builder() creates a builder. Calling build() returns a
Result<MetadataFilter, MetadataError> so structurally invalid expressions such
as empty grouped expressions are reported instead of silently becoming no-ops.
build_checked(&schema) also validates referenced fields, operator
compatibility, and filter value types. Schema-level validation returns an
aggregate error so callers can report all independent issues in one pass.
use DataType;
use ;
let schema = builder
.required
.required
.build;
let filter = builder
.eq
.and_ge
.build_checked
.unwrap;
let meta = new
.with
.with;
assert!;
4) Filter DSL
| Method | Semantics |
|---|---|
eq, ne |
Equality / inequality |
gt, ge, lt, le |
Numeric or string range comparison |
exists, not_exists |
Key presence / absence |
in_set, not_in_set |
Membership / exclusion |
and_*, or_* |
Append one predicate with explicit connector |
and, or, and_not, or_not |
Append grouped subexpressions |
not |
Negate the current expression |
Grouped subexpressions are built with closures. The closure receives a fresh builder, and the resulting expression is appended as one grouped child:
use ;
let filter = builder
.eq
.and
.build
.unwrap;
let meta = new
.with
.with
.with;
assert!;
The expression above means:
status == "active" AND (score >= 80 OR tag == "rust")
Use and_not or or_not when the whole group should be negated:
let filter = builder
.eq
.and_not
.build
.unwrap;
Missing-key behavior for negative predicates is controlled by
MissingKeyPolicy. Mixed numeric comparison behavior for equality, membership,
and range predicates is controlled by NumberComparisonPolicy.
Grouped expressions must contain at least one predicate. For example,
and(|g| g) and or_not(|g| g) are rejected by build() because an empty group
is usually a caller bug.
Empty value sets are allowed. in_set("key", []) matches no metadata object.
not_in_set("key", []) matches when key exists; when key is missing, it
follows the configured MissingKeyPolicy, just like other negative predicates.
When a schema validates filters, mixed numeric compatibility follows the
filter's configured NumberComparisonPolicy, the same policy used at match
time. Conservative accepts exact or safely comparable numeric pairs and
rejects risky lossy comparisons; Approximate accepts mixed numeric pairs that
the runtime matcher can evaluate approximately. Actual
MetadataSchema::validate(&metadata) remains strict: stored metadata values
must use the concrete field type declared by the schema.
5) Versioned filter serde format
Serialized MetadataFilter values use an explicit wire format with version,
expr, and options fields. Expression nodes use type, and condition nodes
use stable operator names in op such as eq, ge, in, and not_exists.
Serialized Condition values use the same condition wire representation. The
internal expression tree is not part of the serialization contract. Policy enum
values in options are serialized as lowercase underscore values such as
match, no_match, conservative, and approximate. New serialization emits
MetadataFilter wire version 2; earlier filter wire versions are rejected.
Error Handling
Use try_get and schema validation when the caller needs diagnostics instead of
Option. Single-entry accessors return MetadataError; schema-level validation
returns MetadataValidationError, whose issues() method exposes all collected
MetadataError values:
use DataType;
use ;
let meta = new.with;
match meta.
Installation
Add to your Cargo.toml:
[]
= "0.5"
License
Licensed under the Apache License, Version 2.0.