A query-building & utility crate for SurrealDB and its SQL querying language that aims to be simple

Overview

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(generic_const_exprs)]

Examples

A complete example can be found in the tests project. For an explanation of what each component in the crate does, refer to the chapters below.

The model macro

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

example
use surreal_simple_querybuilder::prelude::*;

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

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

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.

Relations between your models

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

mod account {
  use surreal_simple_querybuilder::prelude::*;
  use super::project::schema::Project;

  model!(Account {
    id,

    ->manage->Project as managed_projects
  });
}

mod project {
  use surreal_simple_querybuilder::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.

use surreal_simple_querybuilder::prelude::*;

let my_label = "John".as_named_label("Account");
assert_eq!("Account:John", &my_label);

let my_relation = my_label
  .with("FRIEND")
  .with("Mark".as_named_label("Account"));

assert_eq!("Account:John->FRIEND->Account:Mark", my_relation);

The QueryBuilder type

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

Simple example
use surreal_simple_querybuilder::prelude::*;

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

assert_eq!("SELECT * FROM Account", &query);
Complex example
use surreal_simple_querybuilder::prelude::*;

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

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

let should_fetch_authors = true;
let query = QueryBuilder::new()
  .select("*")
  .from("File")
  .if_then(should_fetch_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:

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

// 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
/// 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<String>,
  handle: String,
  password: String,
  email: String,
}

impl IntoKey<String> for Account {
  fn into_key<E>(&self) -> Result<String, E>
  where
    E: serde::ser::Error,
  {
    self
      .id
      .as_ref()
      .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:

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

file.author.allow_value_serialize();

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

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

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

You might also like...
⚛ Quantum computing crate for Rust programming language.

🦀 Quantum crab Quantum crab is a quantum computing crate for Rust programming language. use quantum_crab::{ backend::Backend, quantum_circuit

A developer-friendly framework for building user interfaces in Rust
A developer-friendly framework for building user interfaces in Rust

Reading: "Fru" as in "fruit" and "i" as in "I" (I am). What is Frui? Frui is a developer-friendly UI framework that makes building user interfaces eas

A tutorial of building an LSM-Tree storage engine in a week! (WIP)

LSM in a Week Build a simple key-value storage engine in a week! Tutorial The tutorial is available at https://skyzh.github.io/mini-lsm. You can use t

A repository for showcasing my knowledge of the Rust programming language, and continuing to learn the language.

Learning Rust I started learning the Rust programming language before using GitHub, but increased its usage afterwards. I have found it to be a fast a

Nyah is a programming language runtime built for high performance and comes with a scripting language.

🐱 Nyah ( Unfinished ) Nyah is a programming language runtime built for high performance and comes with a scripting language. 🎖️ Status Nyah is not c

DWARF packaging utility, written in Rust, supporting GNU extension and DWARF 5 package formats.

thorin thorin is an DWARF packaging utility for creating DWARF packages (*.dwp files) out of input DWARF objects (*.dwo files; or *.o files with .dwo

The utility is designed to check the availability of peers and automatically update them in the Yggdrasil configuration file, as well as using the admin API - addPeer method.

Yggrasil network peers checker / updater The utility is designed to check the availability of peers and automatically update them in the Yggdrasil con

Mewl, program in cats' language; A just-for-fun language
Mewl, program in cats' language; A just-for-fun language

Mewl The programming language of cats' with the taste of lisp 🤔 What,Why? Well, 2 years ago in 2020, I created a esoteric programming language called

lelang programming language is a toy language based on LLVM.

lelang leang是一门使用Rust编写,基于LLVM(inkwell llvm safe binding library)实现的编程语言,起初作为课程实验项目,现在为个人长期维护项目。 Target Features 支持8至64位的整形类型和32/64位浮点 基本的函数定义,调用,声明外部

Comments
  • as_named_label doesn't look right

    as_named_label doesn't look right

    • For what follow code is too complex
    account.handle.as_named_label(&account.to_string())  // account:handle
    

    maybe it is better switch to

    account.label("id_hash")  // account:id_hash
    
    opened by huang12zheng 4
  • feat: add fn with_id

    feat: add fn with_id

    fix: test dependencies error with replace _ to -

    if I just open workspace at tests. it get some error about loss config args about "workspace"

    opened by huang12zheng 2
  • support macro for QueryBuilderSetObject

    support macro for QueryBuilderSetObject

    • before
    impl QueryBuilderSetObject for File {
      fn set_querybuilder_object<'a>(mut querybuilder: QueryBuilder<'a>) -> QueryBuilder {
        let a = &[querybuilder.hold(user.name.equals_parameterized())];
    
        querybuilder.set_many(a)
      }
    }
    
    • after #[derive(QueryBuilderSetObject)] struct File{}

    maybe need skip or something also

    opened by huang12zheng 0
  • support macro for IntoKey

    support macro for IntoKey

    • before
    impl IntoKey<String> for User {
      fn into_key<E>(&self) -> Result<String, E>
      where
        E: serde::ser::Error,
      {
        self
          .id
          .as_ref()
          .map(String::clone)
          .ok_or(serde::ser::Error::custom("The user has no ID"))
      }
    }
    
    • after
    #[derive(IntoKey)
    User {}
    
    opened by huang12zheng 0
Releases(v0.4.0)
  • v0.4.0(Nov 27, 2022)

    SQL serialization, from QueryBuilderSetObject to automatic serialization using models/schemas from the model macro

    Removes the QueryBuilderSetObject trait that was used to easily serialize a struct into a series of SQL set statements. The trait was replaced with a set of new utility functions that use an internal SQL serializer and that accept the Schema type (the output of the model!() macro).

    The model macro now accepts the pub keyword in front of the fields to mark them as public and serializable:

    #![allow(incomplete_features)]
    #![feature(generic_const_exprs)]
    
    use surreal_simple_querybuilder::prelude::*;
    
    model!(User {
      // 👇 note how id is not `pub`
      id,
    
      // 👇 while these two fields are
      pub age,
      pub name,
    });
    
    fn main() -> Result<(), SqlSerializeError> {
      use schema::model as user;
    
      let query = QueryBuilder::new()
        .create(user)
        // 👇 all `pub` fields will be serialized while the others won't.
        .set_model(&user)?
        .build();
    
      // CREATE User SET age = $age , name = $name
      println!("query: {query}");
    
      Ok(())
    }
    

    QueryBuilder, from custom type QueryBuilderSegment to Cow<str>

    The QueryBuilder methods now accept Cow<str> instead of a custom type, allowing the user to pass either an owned string or a reference for more flexibility.

    Source code(tar.gz)
    Source code(zip)
  • v0.3.0(Oct 11, 2022)

    Add support for relations in the model macro.

    mod account {
      use surreal_simple_querybuilder::prelude::*;
      use super::project::schema::Project;
    
      model!(Account {
        id,
    
        ->manage->Project as managed_projects
      });
    }
    
    mod project {
      use surreal_simple_querybuilder::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);
    }
    
    Source code(tar.gz)
    Source code(zip)
Owner
Thibault H
Thibault H
Create custom ID types that are guaranteed to be valid `RecordID` in SurrealDB

surreal-id The surreal-id crate offers a standardized way to create and validate IDs in your application for usage with SurrealDB. Using the NewId tra

Liam Woodleigh-Hardinge 4 Oct 5, 2023
🐱 A high-speed JIT programming language and its runtime, meow~

?? A high-speed JIT programming language and its runtime, meow~

EnabledFish 30 Dec 22, 2022
Oxido is a dynamic interpreted programming language basing most of its syntax on Rust.

Oxido Table of Contents: Oxido Installation Uninstallation Usage Syntax Data types Variables Reassignments Printing If statements Loop statements Func

Oxido 6 Oct 6, 2022
Manas project aims to create a modular framework and ecosystem to create robust storage servers adhering to Solid protocol in rust.

मनस् | Manas Solid is a web native protocol to enable interoperable, read-write, collaborative, and decentralized web, truer to web's original vision.

Manomayam 17 Oct 5, 2023
This blog provides detailed status updates and useful information about Theseus OS and its development

The Theseus OS Blog This blog provides detailed status updates and useful information about Theseus OS and its development. Attribution This blog was

Theseus OS 1 Apr 14, 2022
Grimsby is an Erlang Port written in Rust that can close its standard input while retaining standard output (and error)

Grimsby An Erlang Port provides the basic mechanism for communication from Erlang with the external world. From the Ports and Port Drivers: Erlang Ref

Peter Morgan 5 May 29, 2023
A Rust crate providing utility functions and macros.

介绍 此库提供四类功能:异常处理、http post收发对象、格式转换、语法糖。 在 Cargo.toml 里添加如下依赖项 [dependencies.xuanmi_base_support] git = "https://github.com/taiyi-research-institute/x

null 17 Mar 22, 2023
Generate rust structs & query functions from diesel schema files

dsync A utility to generate database structs and querying code from diesel schema files. Primarily built for create-rust-app. Currently, it's more adv

Haris 20 Feb 12, 2023
Dataflow system for building self-driving car and robotics applications.

ERDOS ERDOS is a platform for developing self-driving cars and robotics applications. Getting started The easiest way to get ERDOS running is to use o

ERDOS 163 Dec 29, 2022
A backend framework for building fast and flexible APIs rapidly.

Andromeda Andromeda is a backend framework for Rust, to simplify the development of the kinds of basic API services that we developers have to build s

Framesurge 7 Dec 28, 2022