Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
skyrod-vactai opened this issue Dec 31, 2023 · 3 comments · Fixed by #282
Closed

Comments

@skyrod-vactai
Copy link

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
Copy link

a-0-dev commented Jan 9, 2024

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
Copy link

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

  • Restricting queried relations
  • Adding membership clauses to relationship atoms

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.

@andrewbaxter
Copy link

I made a PR, let's see how this goes...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants