cozodb / cozo

A transactional, relational-graph-vector database that uses Datalog for query. The hippocampus for AI!
https://cozodb.org
Mozilla Public License 2.0
3.24k stars 92 forks source link

Feature request: be able to build queries directly in rust, bypassing the script parser #224

Open skyrod-vactai opened 6 months ago

skyrod-vactai commented 6 months ago

What I wanted to do was be able to write cozo queries directly in rust, and use the various rust structs in the cozo lib to bypass the cozoscript parser. Then I wouldn't have to mess with formatting strings, and it would potentially be a lot cleaner.

However all those structs are all crate private, so I can't bypass the parser. I'm talking about InputProgram and the various other structs contained therein - I'd like to be able to instantiate those myself, and pass them to whatever function executes the parsed script.

a-0-dev commented 6 months ago

That sounds very useful indeed. I've been trying to use cozo in a project of mine (rusty, of course) for the past few months. It kind of works, but I have thought about writing some wrapper myself multiple times because it's not very pretty and feels error-prone (if you want to stay type-safe, you need to if let-unwrap anything the DB returns several times - that could be way nicer if there were inherent type guarantees for returns of fixed queries etc.)

My current (also ugly) workaround are these two macros, which provide some assistance for very basic DB operations and nothing else. I think the existence of this illustrates the pain one currently goes through when using cozo in some project:

#[macro_export]
macro_rules! build_query {
    ($payload:expr, $params:expr) => {{
        use cozo::DataValue;
        use std::collections::BTreeMap;
        // Build parameters map
        let mut params_map: BTreeMap<String, DataValue> = Default::default();
        let mut parameters_init = String::new();

        if $params.len() > 0 {
            for (name, value) in $params {
                let _: &str = name; // only for type annotation
                params_map.insert(name.to_string(), value);
            }

            // First line: Initialize parameters, make them available in CozoScript
            use itertools::Itertools;
            parameters_init += "?[";
            parameters_init += &params_map
                .iter()
                .map(|(name, _)| name)
                .format(", ")
                .to_string();
            parameters_init += "] <- [[";
            parameters_init += &params_map
                .iter()
                .map(|(name, _)| format!("${}", name))
                .format(", ")
                .to_string();
            parameters_init += "]]";
        }

        // Return query string and parameters map
        (format!("{}\n\n{}", parameters_init, $payload), params_map)
    }};
}

use build_query;

#[macro_export]
macro_rules! run_query {
    ($db:expr, $payload:expr, $params:expr, $mutability:expr) => {{
        let (query, parameters) = crate::state::queries::build_query!($payload, $params);
        $db.run_script(query.as_str(), parameters, $mutability)
    }};
}

An example insert-query wrapper function now looks like this:

pub fn add(
    db: &DbInstance,
    id: &AppId,
    last_access: &DateTime<Utc>,
    name: &str,
    description: &str,
) -> anyhow::Result<()> {
    let params = vec![
        (
            "id",
            DataValue::Str(serde_json::to_string(&id).unwrap().into()),
        ),
        (
            "last_access",
            DataValue::Num(Num::Int(last_access.timestamp())),
        ),
        ("name", DataValue::Str(name.into())),
        ("description", DataValue::Str(description.into())),
    ];

    match run_query!(
        &db,
        ":insert apps {id => last_access, name, description}",
        params,
        cozo::ScriptMutability::Mutable
    ) {
        Ok(_) => Ok(()),
        Err(report) => bail!(report),
    }
}

As I said, I'm still rather new to cozo so I don't want to judge any of the dev's decision and I'm grateful for their (your) work. Maybe there are better ways already. But to me as a novice, it's at least not ergonomic ;)

andrewbaxter commented 5 months ago

I'm not sure if this is a bad idea, but I'd like to do some security filtering on user queries:

AFAICT recursing the datalog and making changes should be pretty simple and comprehensive, if it's basically a typed tree of enums/structs.

I'd be happy with something explicitly unstable.