Introduction
π€·ββοΈ What's EgdeDB ?
EdgeDB is an open-source database designed as a spiritual successor to SQL and the relational paradigm and powered by the postgresql query engine.
It's like a relational database with an object-oriented data model, or a graph database with strict schema.
EdgeDB already provides first-party clients for several programming langages such as:
- TypeScript
- Golang
- Deno
- Python
- Rust (Work still in progress)
π€·πΌββοΈ What's Edgedb-query project ?
Edgedb-query is a rust crate project that aims to provide a bunch attruibute macros that facilitate writing of edgeql queries when using edgedb-rust crate.
Installation
In order to use derive macros provided by edgedb-query crate you need to add several crates in your cargo.toml file.
[dependencies]
edgedb-query = "0.2"
edgedb-query-derive = "0.2"
You also need to add edgedb-rust crates π
edgedb = "0.1
edgedb-tokio = "0.2"
edgedb-derive = "0.4"
edgedb-protocol = "0.4"
uuid = "1.3"
Since you are going to use edgedb-tokio, you will also need to add tokio crate π
tokio = { version = "1.19.2", features = ["full"] }
Tags
Edgedb-query-derive crate provide 9 types of attributes that can be used to decorate queries struct fields :
- #[field]
- #[filter]
- #[param]
- #[backlink]
- #[set]
- #[value]
- #[nested_query]
- #[unless_conflcit]
- #[options]
Field
Field attribute gives the query struct field information.
There are two types of field attributes:
π For queries field
#[field(column_name, param, scalar)]
Argument | Optional | Description |
---|---|---|
column_name | yes | The name of the edgeDB table column represented by the field. By default: the name of the field |
param | yes | The query parameter name. By default: the name of the field |
scalar | yes | The field scalar type (example : "default::str"). By default: the scalar type corresponding to the field type |
link_property (bool) | yes | Marks a field as link property. By default: false |
Usage
struct InsertUser {
#[field(column_name= "first_name", param = "username", scalar = "<default::str>")]
name: String
}
π For query result field
#[field(column_name, wrapper_fn, default_value)]
Argument | Optional | Description |
---|---|---|
column_name | yes | The name of the edgeDB table column represented by the field. By default: the name of the field |
wrapper_fn | yes | The function to apply to the field value |
defaut_value | yes | The result field default value |
link_property (bool) | yes | Marks a field as link property. By default: false |
Usage
struct UserResult {
#[field(column_name= "first_name", wrapper_fn ="str_upper", default_value="John")]
name: String
}
Filter
Filter attribute represents a filter statement in a edgeDB query.
#[filter(operator, wrapper_fn)]
When several filters are applied in a query, only the first filter can be represented by attribute #[filter]. The others filters should be decorated with #[and_filter] or #[or_filter].
#[and_filter(operator, wrapper_fn)]
#[or_filter(operator, wrapper_fn)]
Argument | Optional | Description |
---|---|---|
operator | no | The filter operator. |
wrapper_fn | yes | The function to apply to the edgeDB column value before applying the filter |
The operator argument can take one of following values (the case does not matter):
- exists
- notexists or !exists
- is or =
- isnot or !=
- like
- ilike
- in
- notIn
- greaterthan or >
- lesserthan or <
- greaterthanorequal or >=
- lesserthanorequal or <=
Usage
struct FindUserByNameAndAge {
#[filter(operator="Is", wrapper_fn="str_lower")]
pub name: String,
#[and_filter(operator=">=")]
pub age: i8
}
Param
Param attribute represents query parameter. It's take the name of the query parameter as argument.
#[param()]
Usage
struct FindUser {
#[param("username")]
pub name: String
}
Back Link
Back Link attribute is used in a query result struct. It indicates that the field's value is the result of a backlink.
#[back_link(module, source_table, target_table, target_column)]
Argument | Optional | Description |
---|---|---|
module | yes | The edgeDB module. By default : 'default' |
source_table | no | The backlink source table name |
target_table | no | The backlink target table name |
target_column | no | The backlink target column name |
Usage
struct UserResult {
#[back_link(
module="users",
source_table="User",
target_table="Friend",
target_column="friend"
)]
friend: Friend,
}
Set
Set attribute represents a update query set statement.
#[set(option)]
Option can take following values:
- assign or :=
- concat or ++ (only for string)
- push or += (only for vec)
Usage
struct UpdateUser {
#[set(option="assign")]
pub name: String,
...
}
Value
Value attribute is used in enum field. It takes value of the EdgeDB enum variant.
#[value()]
Usage
enum Gender {
#[value("man")] Male,
#[value("woman")] Female
}
Nested Query
Nested Query attribute indicates that the field type is a query.
#[nested_query]
Usage
struct FindUser {
#[nested_query]
pub credentials: FindCredentials
}
struct FindCredentials {
...
}
Unless Conflict
Unless Conflict attribute represents a unless conflict else statement.
The decorated field must be of type
edgedb_query::queries::conflict::UnlessConflict or edgedb_query::queries::conflict::UnlessConflictElse<T: ToEdgeQuery>.
#[unless_conflict(on)]
on attribute (optional) lists conflict column's names separated by a comma.
Usage
#[insert_query(table="Users")]
struct InsertUser {
#[field(column_name="firstname")]
pub first_name: String,
#[field(column_name="lastname")]
pub last_name: String,
pub age: u8,
#[unless_conflict(on="firstname, lastname")]
pub conflict: UnlessConflictElse<FindByUserName>
}
#[select_query(table="Users")]
struct FindByUserName {
...
}
#Options
Options attribute marks a field as a select option (order and pagination options). The decorated fied must be of type edgedb_query::queries::select::SelectOptions
#[options]
Usage
#[select_query(table="Users")]
struct FindUser {
#[options]
pub options: SelectOptions
}
Query result
#[query_result]{
#[field]
#[backlink]
}
#[query_result] attribute marks a struct as a result of a edgeDB query.
When decorating a struct with #[query_result] attribute, the resulting struct is decorated with edgedb_derive::Queryable macro derive.
For this reason, a struct decorated #[query_result] β οΈ must have a field id: uuid::Uuid
β οΈ
Usage
This is an example of usage of the field attribute π
#[query_reqult]
pub struct UserResult {
pub id: uuid::Uuid,
#[field(column_name="pseudo", wrapper_fn="str_upper", default_value="john")]
pub login: String,
}
fn main() {
let shape = UserResult::shape();
assert_eq!(shape, "{id, login := (select <str>str_upper(.pseudo)) ?? (select <str>'john')}")
}
And then, an example of usage of backlink attribute π
Consider the following edgeDB schema :
module cinema {
type Movie {
required property title -> str;
multi link actors -> Actor;
}
type Actor {
required property name -> str;
}
}
To query the Actor table and get the actor's name and all the movies he's been in, we need to write the following query :
select Actor
{
name,
movies := (
select Actor.<actors[is Movie] {
title
}
)
}
Using #[query_result] attribute and its backlink attribute we can do things like this π
#[query_result]
pub struct MovieResult {
pub id: uuid::Uuid,
pub title: String,
}
#[query_result]
pub struct Actor {
pub id: uuid::Uuid,
pub name: String,
#[back_link(
module="cinema",
source_table="Actor",
target_table="Movie",
target_column="actors"
)]
pub movies: Vec<MovieResult>,
}
fn main() {
let rm_spaces = |s: &str| s.split_whitespace().collect::<String>();
let shape = Actor::shape();
let expected_shape = r#"
{
id,
name,
movies := (
select cinema::Actor.<actors[is cinema::Movie] {
title
}
)
}
"#;
assert_eq!(rm_spaces(shape.as_str()), rm_spaces(expected_shape));
}
Edgedb Enum
#[edgedb_enum]{
#[value]
}
An enum decorated #[edgedb_enum] is a representation of an edgeDB scalar enum type.
The value
argument is used when the rust enum variant name does not match the edgeDB enum variant name.
Usage
The following scalar enum types π
scalar type Gender extending enum<Man, Woman>;
scalar type Status extending enum<Opened, InProgress, Done, Closed>;
can then be represented by π
#[edgedb_enum]
pub enum Gender {
#[value("Man")]
Male,
#[value("Woman")]
Female,
}
#[edgedb_enum]
pub enum Status {
Opened,
InProgress,
#[value("Done")]
Finished,
Closed
}
Edgedb Filters
#[edgedb_filters]{
#[field]
#[filter]
#[and_filter]
#[or_filter]
}
#[edgedb_filters] attribute is used to decorate a struct that group a list of filters.
The decorated type can be used as type of a query struct field. In this case, the field is decorated with a #[filters] attribute.
Usage
#[edgedb_filters]
pub struct UserFilter {
#[field(column_name="identity.first_name", param = "first_name")]
#[filter(operator="=", wrapper_fn="str_lower")]
pub name: String,
#[or_filter(operator=">=")]
pub age: i8
}
#[select_query]
pub struct FindUser {
#[filters]
pub filters: UserFilter
}
EdgedbSet
#[edgedb_sets]{
#[field]
#[set]
#[nested_query]
}
#[edgedb_sets] macro attribute indicates that a struct groups a list of #[set].
β οΈ
- a #[set] statement can be a nested query.
The decorated type can be used as type of a query struct field. In this case, the field is decorated with a #[sets] attribute.
Usage
#[edgedb_sets]
pub struct PersonSets {
#[field(column_name="first_name", param = "user_name", scalar="<str>")]
#[set(option="Concat")]
pub name: String,
#[field(scalar="default::State")]
pub status: Status,
#[nested_query]
pub users: FindUsers
}
#[edged_enum]
pub enum Status {
#[value("started")] On,
#[value("finished")] Off
}
#[select_query(table="Users")]
pub struct FindUsers {
#[filter(operator="Is")]
pub name: String
}
#[update_query(table="Person")]
pub struct UpdatePerson {
#[sets]
pub sets: PersonSets
}
Queries attribute macros
Edgedb-query-derive crate provide 5 macro attributes that represent a edgeDB query:
Queries attributes (except #[file_query] ) take three arguments π
Argument | Optional | Description |
---|---|---|
module | yes | The name of the edgeDB module on which the query is executed. By default: 'default' |
table | no | The name of the edgeDB table on which the query is executed. |
result | yes | The query result type. By default: BasicResult |
When a struct is decorated with one of those queries macro attributes, several trait implementations are created for this one:
The following example shows how to get a parameterized query from a struct decorated with a query macro attribute ( #[insert_query] for example) :
Consider the following struct π:
#[insert_query(module="humans", table="Person")]
pub struct InsertPerson {
pub name: String,
pub age: i8
}
To get a parameterized query from this struct, we can write the following code:
#[tokio::main]
fn main() -> Result<()>{
let insert_person = InsertPerson {
name: "John".to_string(),
age: 20
};
let edge_query: EdgeQuery = insert_person.to_edge_query();
}
to_edge_query()
( from ToEdgeQuery trait ) method returns a EdgeQuery struct that contains the query string and the query parameters.
The query's cardinality (Cardinality::Many
by default) can also be set while getting the query by calling to_edge_query_with_cardinality()
method instead of to_edge_query()
.
let edge_query: EdgeQuery = insert_person.to_edge_query_with_cardinality(Cardinality::One);
With the got query, we can now execute request using edgedb-client like this:
if let EdgeQuery{ query, args: Some(params), .. } = edge_query {
let result: BasicResult = client.query_required_single(query.as_str(), params).await?;
}
InsertQuery
#[insert_query(module, table, result)] {
#[field]
#[nested_query]
#[unless_conflict]
}
insert_query attribute macro indicates that the struct represents an edgeDB insert query.
Its fields can be decorated with one of following tags :
Usage
Consider the following edgeDB schema π
module models {
scalar type Gender extending enum<Male, Female>
type Person {
required property user_name -> str {
constraint exclusive;
}
required property age -> int16;
required property gender -> Gender;
link address -> Address;
}
type Address {
required property num -> int16;
required property street -> str;
required property city -> str;
required property zipcode -> int32;
}
}
Let's write a struct that represents query to insert a new Person into the database
#[insert_query(module="models", table="Person", result="Person")]
pub struct InsertPerson {
#[field(column_name="user_name")]
pub name: String,
#[field(scalar="<int16>")]
pub age: u8,
#[field(column_name="gender", scalar="<models::Gender>")]
pub sex: Sex,
}
#[edgedb_enum]
pub enum Sex {
#[value("Male")]
Man,
#[value("Female")]
Woman
}
#[query_result]
pub struct Person {
pub user_name: String,
pub age: u8
}
π€·ββοΈ But what about the person's addressβ
Since the address is stored in a separate database table we need to insert a new Address while creating a new Person, right ?
Ok, so let's write the address's insert query corresponding struct.
#[insert_query(module="models", table="Address")]
pub struct InsertAddress {
#[field(column_name="num", scalar="<int16>")]
pub number: u16,
pub street: String,
pub city: String,
#[field(column_name="zipcode", scalar="<int32>")]
pub zip_code: u32
}
To insert both entities with a single query, add the insert address query as a nested query of the person insert query.
#[insert_query(module="models", table="Person", result="Person")]
pub struct InsertPerson {
...
#[nested_query]
pub address: InsertAddress
}
Okay, great! Now we can persist a Person with address.
π€·ββοΈ But what if a Person already exists with the same name β
Remember !!!
In the edgeDB schema, the type Person has an exclusive constraint on its field user_name.
required property user_name -> str {
constraint exclusive;
}
To handle this case we need to use an unless conflict statement.
#[insert_query(module="models", table="Person", result="Person")]
pub struct InsertPerson {
...
#[unless_conflict(on="user_name")]
pub handle_conflict: edgedb_query::queries::conflict::UnlessConflict
}
The new field handle_conflict decorated with #[unless_conflict] tag add a unless conflict on .user_name
statement to the query.
It is possible to add an else query to the unless conflict statement by using a edgedb_query::queries::conflict::UnlessConflictElse<T: ToEdgeQuery> type instead of an UnlessConflict type.
#[insert_query(module="models", table="Person", result="Person")]
pub struct InsertPerson {
...
#[unless_conflict(on="user_name")]
pub handle_conflict: edgedb_query::queries::conflict::UnlessConflictElse<FindUserByName>
}
#[select_query]
pub struct FindUserByName {
...
}
SelectQuery
#[select_query(module, table, result)] {
#[field]
#[filter]
#[and_filter]
#[or_filter]
#[filters]
#[options]
}
select_query attribute macro indicates that the struct represents an edgeDB select query.
Each field of SelectQuery can be decorated with following tags:
β οΈ
- #[filter] (#[and_filter] or #[or_filter]) and #[filters] can not be used to together.
Usage
Consider the following edgeDB schema π
module models {
type Person {
required property user_name -> str;
required property age -> int16;
required property gender -> Gender;
link address -> Address;
}
}
To perform a select query using edgedb-tokio we can write code as follows π
#[select_query(module="models", table="Person", result="Person")]
pub struct SelectPerson {
#[field(column_name="user_name")]
#[filter(operator = "Like")]
pub name: String,
#[and_filter(operator = "<=")]
pub age: u16,
#[options]
options: SelectOptions
}
#[query_result]
pub struct Person {
pub id: uuid::Uuid,
pub user_name: String,
pub age: i8
}
#[tokio::main]
async fn main() -> Result<()> {
let client = edgedb_tokio::create_client().await?;
let select_person = SelectPerson {
name: "%oe".to_owned(),
age: 18,
options: SelectOptions {
table_name: "Person",
module: Some("models"),
order_options: Some(OrderOptions {
order_by: "name".to_string(),
order_direction: Some(OrderDir::Desc)
}),
page_options: None
}
};
let edge_query: EdgeQuery = select_person.to_edge_query();
let args = &edge_query.args.unwrap();
let query = edge_query.query.as_str();
if let Some(persons) = client.query::<Person, _>(query, args).await? {
assert!(persons.len() > 0 );
} else {
unreachable!();
}
}
UpdateQuery
#[update_query(module, table, result)] {
#[field]
#[set]
#[sets]
#[filter]
#[and_filter]
#[or_filter]
#[filters]
}
update_query attribute macro indicates that the struct represents an edgeDB update query.
Each field of UpdateQuery can be decorated with following tags:
β οΈ
- #[filter] (#[and_filter] or #[or_filter]) and #[filters] can not be used to together.
- #[set] and #[sets] can not be used to together.
- A field not decorated with #[filter] (#[and_filter] or #[or_filter]) is considered to be a #[set] field.
Usage
Consider the following edgeDB schema π
module models {
type Person {
required property user_name -> str;
required property age -> int16;
}
}
To perform an update query using edgedb-tokio we can write code as follows π
#[update_query(module="models", table="Person")]
pub struct UpdatePerson {
#[field(column_name="user_name", param="new_name")]
pub name: String,
#[field(column_name="user_name", param="searched_name")]
#[filter(operator="like")]
pub filter: String
}
#[tokio::main]
async fn main() -> Result<()> {
let client = edgedb_tokio::create_client().await?;
let update_person = UpdatePerson {
name: "Mark".to_owned() ,
filter: "%oe".to_owned()
};
let edge_query: EdgeQuery = update_person.to_edge_query_with_cardinality(Cardinality::One);
let args = &edge_query.args.unwrap();
let query = edge_query.query.as_str();
if let Some(result) = client.query_required_single::<BasicResult, _>(query, args).await? {
assert_ne!(result.id.to_string(), String::default())
} else {
unreachable!()
}
}
DeleteQuery
#[delete_query(module, table)] {
#[field]
#[filter]
#[and_filter]
#[or_filter]
#[filters]
}
delete_query attribute macro indicates that the struct represents an edgeDB delete query.
Each field of DeleteQuery can be decorated with following tags:
β οΈ
- #[filter] (#[and_filter] or #[or_filter]) and #[filters] can not be used to together.
Usage
Consider the following edgeDB schema π
module models {
type Person {
required property user_name -> str;
required property age -> int16;
}
}
To perform a 'delete query' using edgedb-tokio we can write code as follows π
#[delete_query(module="models", table="Person")]
pub struct DeletePerson {
#[field(column_name="user_name")]
#[filter(operator="Is")]
pub name: String
}
#[tokio::main]
async fn main() -> Result<()> {
let client = edgedb_tokio::create_client().await?;
let del_person = DeletePerson {
name: "Mark".to_owned()
};
let edge_query: EdgeQuery = del_person.to_edge_query();
let args = &edge_query.args.unwrap();
let query = edge_query.query.as_str();
let _= client.query_single<BasicResult, _>(query, args).await?;
}
Query
#[query(value)] {
#[param]
}
#[query] is a query macro attribute that takes directly the query string as value parameter.
The argument value
represents the query string value.
The decorated struct fields represent the query parameters.
They can be annotated #[param]
when the field name doesn't match the parameter label.
Usage
#[query(value=r#"
insert default::Person {
name := <str>$name,
places_visited := (
insert default::City {
name := <str>$city_name,
}
)
}
"#)]
pub struct InsertPerson {
name: String,
#[param("city_name")]
city: String
}
Field city
represents the city's name that is under the query parameter <str>$city_name
.
FileQuery
#[file_query(src)] {
#[param]
}
#[file_query] is a special query macro attribute that is based on a query source file.
The argument src
represents the path to the source file (relative to the current working directory). This file should have .edgeql
extension.
The decorated struct fields represent the query parameters.
They can be annotated #[param]
when the field name doesn't match the parameter label.
Usage
Consider this .edgeql
file ( query.edgeql ) located in directory ./queries π
insert default::Person {
name := <str>$name,
places_visited := (
insert default::City {
name := <str>$city_name,
}
)
}
Based on this file, we can write a file query struct like this π
#[file_query(src="queries/query.edgeql")]
pub struct InsertPerson {
name: String,
#[param("city_name")]
city: String
}
Field city
represents the city's name that is under the query parameter <str>$city_name
.