macro_rules! table {
($table:ident: $type:ty, $pk:ident: $pkty:ty, missing $errty:ty => $missing:expr, $($itype:ident $name:ident $prop:tt => $err:expr),*) => { ... };
($table:ident: $type:ty, $pk:ident: $pkty:ty, noautokey, missing $errty:ty => $missing:expr, $($itype:ident $name:ident $prop:tt => $err:expr),*) => { ... };
}
Expand description
§Table Macro
Generate database methods (insert, update and delete) for a table. This macro takes a definition of the database table and indices and generates helper methods used to safely insert, update and delete rows in the database.
§Usage
In order to use the macro, you need an error type for your database, a row type per table (for example, a User struct for your users table), and a struct for the database itself.
§Error Type
The error type is usually a simple enum. For every table, you need an error case for when a row is missing, as well as when it exists. Additionally, you need one error type per unique index.
Here is an example of the what the error type might look like:
pub enum Error {
UserIdExists,
UserEmailExists,
UserNotFound,
UserNameEmpty,
GroupNameExists,
GroupIdExists,
GroupNotFound,
GroupNotEmpty,
GroupNameEmpty,
}
§Row Types
For every table, you need a row type. This can just be a regular Rust struct. It needs to
derive Clone, and it needs to have some kind of primary key. The primary key uniquely
identifies the row for update and deletion operations. Usually, an integer type is
recommended here. For visual clarity, you can create a type alias for this field. We will
refer to the row type as RowType
and to the type of the primary key as RowId
.
Here is an example of what User and Group row types might look like.
type UserId = u64;
#[derive(Clone)]
pub struct User {
id: UserId,
name: String,
email: String,
group: GroupId,
}
type GroupId = u64;
#[derive(Clone)]
pub struct Group {
id: GroupId,
name: String,
}
§Database Struct
The database struct must contain one map per table and one per index.
The table maps must be of the shape MapType<RowPrimaryKey, RowType>
. The type of map that is used does not matter, although common choices are BTreeMap
and HashMap.
For every unique index, a map of the shape MapType<IndexType, RowId>
must be added.
For every index, a map of the shape MapType<IndexType, Set<RowId>>
needs to be added.
The set type that is used
does not matter, although common choices are BTreeSet
and HashSet.
The IndexType is the type of the field of the index. For instance, if
the index is on a field of type String, that is the IndexType.
Kind | Type |
---|---|
Table | MapType<RowId, RowType> |
Index | MapType<IndexType, Set<RowId>> |
Unique index | MapType<IndexType, RowId> |
For example, to define a database struct with two tables (users and groups), two unique indices (user_by_email and group_by_name), and one regular index (users_by_group), this struct definition could be used:
use std::collections::{BTreeMap, HashMap, BTreeSet, HashSet};
pub struct Database {
/// Users table
users: BTreeMap<UserId, User>,
/// User by email unique index
user_by_email: HashMap<String, UserId>,
/// Groups table
groups: HashMap<GroupId, Group>,
/// Users by group index
users_by_group: BTreeMap<GroupId, BTreeSet<UserId>>,
/// Group by name unique index
group_by_name: BTreeMap<String, GroupId>,
}
§Syntax
The basic syntax of the macro looks like this:
table!(
$table_name: RowType,
$id_field: RowId,
missing ErrorType => $missing_error,
primary $table_name $id_field => $exists_error,
<constraints...>
<indices...>
);
Here is an overview of what the various placeholders mean:
Placeholder | Example | Explanation |
---|---|---|
$table_name | users | Name of the table map in the database struct |
RowType | User | Name of the data type of the rows |
RowId | UserId | Name of the type of the primary keys for the rows |
$id_field | id | Name of the struct field of the Row type that contains the primary key |
ErrorType | Error | Name of the error type (enum) |
$missing_error | Error::UserNotFound | Error to throw when trying to delete a row that does not exists |
$exists_error | Error::UserIdExists | Error to throw when trying to insert a row that already exists |
<constraints...> | Definitions for the constraints (if any). | |
<indices...> | Definitions for the indices (explained in next section) |
§Constraints
The syntax for constraints is the following:
constraint fn_name _ => (),
In order to define constraints, you need to declare a method on your database struct. This
method should return Result<(), ErrorType>
. For example, you can create a constraint that
enforces that a user name should not be empty. The constraint method might look like this:
impl Database {
fn name_not_empty(&self, user: &User) -> Result<(), Error> {
if user.name.is_empty() {
return Err(Error::UserNameEmpty);
}
Ok(())
}
}
Defining the constraint in the macro then looks as follows:
constraint name_not_empty _ => ()
§Indices
The macro also needs to be told of the various indices. The syntax for indices looks like this:
$type $map $field => $error
Here, $type
refers to the type of index (can be index
, unique
, foreign
or
reverse
). $map
is the name of the map field in the database struct that represents this
index. $field
is the name of the field of the RowType
that this index is on. The field can
also be a compound key, by writing it as a tuple (for example (first_name, last_name)
. Finally,
$error
is the error that is thrown when this index is violated. Here is an overview of the
index types:
Type | Example | Explanation |
---|---|---|
Index | index users_by_group group => () | Defines a simple index to look up rows based on their group. Does not need an error. |
Foreign | foreign groups group => Error::GroupNotFound | Defines a foreign key constraint which enforces that the group field point to an existing row in the groups table. |
Unique | unique user_by_email email => Error::UserEmailExists | Defines a unique index which uses the user_by_email map and enforces that no two users share the same email. |
Reverse | reverse users_by_group id => Error::GroupHasUsers | Declares a reverse dependency (on an index by another table) that prevents a group row being deleted if there are still users with that group. |
The result of this is that the macro generates insertion, update and deletion methods for every table. It uses the table map name as the prefix for those methods. For example, calling it on a table with the name users results in three methods being generated:
impl Database {
/// Insert a User into the database, or return an error.
pub fn users_insert(row: User) -> Result<(), Error>;
/// Update a User row (identified by the primary key), returning the old row, or return
/// an error.
pub fn users_update(row: User) -> Result<User, Error>;
/// Delete a User row (identified by the primary key), returning the row, or return an
/// error.
pub fn users_delete(id: UserId) -> Result<User, Error>;
}
§Example
Here is an example invocation of the macro on the Database struct with two tables (users and groups), including indices and foreign key constraints:
use macrodb::table;
impl Database {
fn user_name_not_empty(&self, user: &User) -> Result<(), Error> {
if user.name.is_empty() {
return Err(Error::UserNameEmpty);
}
Ok(())
}
fn group_name_not_empty(&self, group: &Group) -> Result<(), Error> {
if group.name.is_empty() {
return Err(Error::GroupNameEmpty);
}
Ok(())
}
table!(
users: User,
id: UserId,
missing Error => Error::UserNotFound,
primary users id => Error::UserIdExists,
foreign groups group => Error::GroupNotFound,
index users_by_group group => (),
constraint user_name_not_empty _ => (),
unique user_by_email email => Error::UserEmailExists
);
table!(
groups: Group,
id: GroupId,
missing Error => Error::GroupNotFound,
primary users id => Error::GroupIdExists,
constraint group_name_not_empty _ => (),
reverse users_by_group id => Error::GroupNotEmpty,
unique group_by_name name => Error::GroupNameExists
);
}