Surreal simple querybuilder

A simple query-builder for the Surreal Query Language, for SurrealDB. Aims at being simple to use and not too verbose first.

Summary

Why a query-builder

Query builders allow you to dynamically build your queries with some compile time checks to ensure they result in valid SQL queries. Unlike ORMs, query-builders are built to be lightweight and easy to use, meaning you decide when and where to use one. You could stick to hard coded string for the simple queries but use a builder for complex ones that require parameters & variables and may change based on these variables for example.

While the crate is first meant as a query-building utility, it also comes with macros and generic types that may help you while managing you SQL models in your rust code. Refer to the node macro and the Foreign type example

SQL injections

The strings you pass to the query builder are not sanitized in any way. Please use parameters in your queries like SET username = $username with surrealdb parameters to avoid injection issues. However the crate comes with utility functions to easily create parameterized fields, refer to the NodeBuilder trait.

Compiler requirements/features

The crate uses const expressions for its model creation macros in order to use stack based arrays with sizes deduced by the compiler. For this reason any program using the crate has to add the following at the root of the main file: ```

![allow(incomplete_features)]

![feature(genericconstexprs)]

```

Examples

The model macro allows you to quickly create structs (aka models) with fields that match the nodes of your database.

example

```rust use surrealsimplequerybuilder::prelude::*;

struct Account { id: Option, handle: String, password: String, email: String, friends: Foreign> }

model!(Account { id, handle, password, friends> });

fn main() { // the schema module is created by the macro use schema::model as account;

let query = format!("select {} from {account}", account.handle);
assert_eq!("select handle from Account", query);

} ```

This allows you to have compile time checked constants for your fields, allowing you to reference them while building your queries without fearing of making a typo or using a field you renamed long time ago.

public & private fields in models

The QueryBuilder type offers a series of methods to quickly list the fields of your models in SET or UPDATE statements so you don't have to write the fields and the variable names one by one. Since you may not want to serialize some of the fields like the id for example the model macro has the pub keyword to mark a field as serializable. Any field without the pub keyword in front of it will not be serialized by these methods.

```rs model!(Project { id, // <- won't be serialized pub name, // <- will be serialized })

fn example() { use schema::model as project;

let query = QueryBuilder::new() .set_model(project) .build();

assert_eq!(query, "SET name = $name"); } ```

Relations between your models

If you wish to include relations (aka edges) in your models, the model macro has a special syntax for them:

```rust mod account { use surrealsimplequerybuilder::prelude::*; use super::project::schema::Project;

model!(Account { id,

->manage->Project as managed_projects

}); }

mod project { use surrealsimplequerybuilder::prelude::*; use super::project::schema::Project;

model!(Project { id, name,

<-manage<-Account as authors

}); }

fn main() { use account::schema::model as account;

let query = format!("select {} from {account}", account.managed_projects);
assert_eq!("select ->manage->Project from Account");

let query = format!("select {} from {account}", account.managed_projects().name.as_alias("project_names"))
assert_eq!("select ->manage->Project.name as project_names from Account", query);

} ```

The NodeBuilder traits

These traits add a few utility functions to the String and str types that can be used alongside the querybuilder for even more flexibility.

```rust use surrealsimplequerybuilder::prelude::*;

let mylabel = "John".asnamedlabel("Account"); asserteq!("Account:John", &my_label);

let myrelation = mylabel .with("FRIEND") .with("Mark".asnamedlabel("Account"));

asserteq!("Account:John->FRIEND->Account:Mark", myrelation); ```

The QueryBuilder type

It allows you to dynamically build complex or simple queries out of segments and easy to use methods.

Simple example

```rust use surrealsimplequerybuilder::prelude::*;

let query = QueryBuilder::new() .select("*") .from("Account") .build();

assert_eq!("SELECT * FROM Account", &query); ```

Complex example

```rust use surrealsimplequerybuilder::prelude::*;

let shouldfetchauthors = false; let query = QueryBuilder::new() .select("*") .from("File") .ifthen(shouldfetch_authors, |q| q.fetch("author")) .build();

assert_eq!("SELECT * FROM Account", &query);

let shouldfetchauthors = true; let query = QueryBuilder::new() .select("*") .from("File") .ifthen(shouldfetch_authors, |q| q.fetch("author")) .build();

assert_eq!("SELECT * FROM Account FETCH author", &query); ```

The ForeignKey and Foreign types

SurrealDB has the ability to fetch the data out of foreign keys. For example: ```sql create Author:JussiAdlerOlsen set name = "Jussi Adler-Olsen"; create File set name = "Journal 64", author = Author:JussiAdlerOlsen;

select * from File; select * from File fetch author; which gives us json // without FETCH author { "author": "Author:JussiAdlerOlsen", "id":"File:rg30uybsmrhsf7o6guvi", "name":"Journal 64" }

// with FETCH author { "author": { "id":"Author:JussiAdlerOlsen", "name":"Jussi Adler-Olsen" }, "id":"File:rg30uybsmrhsf7o6guvi", "name":"Journal 64" } ```

The "issue" with this functionality is that our results may either contain an ID to the author, no value, or the fully fetched author with its data depending on the query and whether it includes fetch or not.

The ForeignKey types comes to the rescue. It is an enum with 3 variants: - The loaded data for when it was fetched - The key data for when it was just an ID - The unloaded data when it was null (if you wish to support missing data you must use the #serde(default) attribute to the field)

The type comes with an implementation of the Deserialize and Serialize serde traits so that it can fallback to whatever data it finds or needs. However any type that is referenced by a ForeignKey must implement the IntoKey trait that allows it to safely serialize it into an ID during serialization.

example

```rust /// For the tests, and as an example we are creating what could be an Account in /// a simple database. #[derive(Debug, Serialize, Deserialize, Default)] struct Account { id: Option, handle: String, password: String, email: String, }

impl IntoKey for Account { fn intokey(&self) -> Result where E: serde::ser::Error, { self .id .asref() .map(String::clone) .ok_or(serde::ser::Error::custom("The account has no ID")) } }

#[derive(Debug, Serialize, Deserialize)] struct File { name: String,

/// And now we can set the field as a Foreign node
author: Foreign<Account>,

}

fn main() { // ...imagine query is a function to send a query and get the first result... let file: File = query("SELECT * from File FETCH author");

if let Some(user) = file.author.value() {
  // the file had an author and it was loaded
  dbg!(&user);
}

// now we could also support cases where we do not want to fetch the authors
// for performance reasons...
let file: File = query("SELECT * from File");

if let Some(user_id) = file.author.key() {
  // the file had an author ID, but it wasn't fetched
  dbg!(&user_id);
}

// we can also handle the cases where the field was missing
if file.author.is_unloaded {
  panic!("Author missing in file {file}");
}

} ```

ForeignKey and loaded data during serialization

A ForeignKey always tries to serialize itself into an ID by default. Meaning that if the foreign-key holds a value and not an ID, it will call the IntoKey trait on the value in order to get an ID to serialize.

There are cases where this may pose a problem, for example in an API where you wish to serialize a struct with ForeignKey fields so the users can get all the data they need in a single request.

By default if you were to serialize a File (from the example above) struct with a fetched author, it would automatically be converted into the author's id.

The ForeignKey struct offers two methods to control this behaviour: ``rust // ...imaginequery` is a function to send a query and get the first result... let file: File = query("SELECT * from File FETCH author");

file.author.allowvalueserialize();

// ... serializing file will now serialize its author field as-is.

// to go back to the default behaviour file.author.disallowvalueserialize(); ```

You may note that mutability is not needed, the methods use interior mutability to work even on immutable ForeignKeys if needed.