Skip to content

Rust API

Mazhar Ahmed edited this page Aug 23, 2026 · 1 revision

Rust API

[dependencies]
qql = "0.1"
use qql::{Context, Error};

fn main() -> Result<(), Error> {
    let mut ctx = Context::new("./sources");

    for record in ctx.execute("Q:2:1-5,255;Q:1;")? {
        println!("{} — {}", record.collection, record.ar);
    }

    Ok(())
}

Context

Context owns the source registry and every data file loaded so far. Files load on first use and are cached until it drops.

let mut ctx = Context::new("./sources");   // infallible, reads nothing yet

ctx.execute("Q:2:255")?;          // Result<Vec<Record>, Error>
ctx.execute_value("Q:2:255");     // serde_json::Value — never fails
ctx.execute_json("Q:2:255");      // String — never fails
ctx.sources();                    // registered codes
ctx.data_dir();                   // the directory it reads

execute returns a Result because that is what Rust callers want. execute_json is the total version: it serializes the error rather than returning one, which is what the FFI layer wraps.

Parsing without data

parse touches no filesystem, so it works with no data directory at all — useful for validating input before you have anything to resolve against.

let query = qql::parse("Q:2:1-5,255")?;
assert_eq!(query.references.len(), 1);

let reference = &query.references[0];
reference.source.as_deref();   // Some("Q"), or None if omitted
reference.primary;             // Some(2), or None for the B::100 form
reference.ranges;              // the selector
reference.selects_all();       // true when ranges is empty
reference.is_flat();           // the `::` form
reference.is_search();         // carries a term

Records

pub struct Record {
    pub source: String,      // "Q"
    pub collection: String,  // "Quran"
    pub extra: BTreeMap<String, Value>,   // surah, ayah, chapter, score, …
    pub ar: String,
    pub en: String,
}

extra is flattened to the top level in JSON, so records are not forced into one shape. BTreeMap rather than HashMap keeps key order deterministic.

let record = &ctx.execute("Q:2:255")?[0];
let surah = record.extra["surah"].as_u64().unwrap();

Errors

match ctx.execute("Q:115") {
    Ok(records) => …,
    Err(e) => {
        e.code();       // "QQL_REFERENCE_NOT_FOUND"
        e.position();   // Option<usize>, a byte offset
        e.to_string();  // human-readable
        e.to_json("Q:115");   // the canonical envelope
    }
}

See Errors for the full list.

Adding sources at runtime

ctx.register_spec(spec);                        // a SourceSpec
ctx.add_sources_from("other-sources.json")?;    // a manifest
ctx.load_manifest()?;                           // <data>/qql-sources.json

Sources are searched newest-first, so registering an existing code shadows it. The data-directory manifest loads on the first query, so it lands after anything registered manually — call load_manifest() first if you mean to override something it defines. See Custom Sources.

Implementing the Source trait directly is also supported, for data too irregular for a declarative mapping.

Threading

execute takes &mut self, so the compiler prevents concurrent use of one context. Context is Send, so moving one to another thread is fine and separate contexts on separate threads are safe by construction.

There is no shared mutable state and no global cache.

Optional features

qql = { version = "0.1", features = ["vector", "fulltext"] }

Both are off by default; the crate is serde + serde_json without them. See Search and Building.

Clone this wiki locally