macrodb/
lib.rs

1//! # MacroDB
2//!
3//! This crate exports a macro that can be used to turn an appropriate Rust struct into an
4//! in-memory database that guarantees consistency and supports indices. The Rust struct needs
5//! to be written by hand, the macro only generates a safe interface for using the in-memory
6//! database.
7//!
8//! ## Example
9//!
10//! Here is a full example of building a user database. An error type is defined, that is used
11//! by the database methods to return information failures. A struct for the record type that is
12//! to be stored is defined (the User struct). Finally, a struct for the database is defined
13//! that contains two indices. The [table][macro@table] macro generates methods for inserting, updating
14//! and deleting rows.
15//!
16//! ```rust
17//! use std::collections::{BTreeMap, BTreeSet};
18//! use macrodb::table;
19//!
20//! /// Error type for database interactions.
21//! pub enum Error {
22//!     UserIdExists,
23//!     UserEmailExists,
24//!     UserNotFound,
25//! }
26//!
27//! type UserId = u64;
28//!
29//! /// Record type for a user (a row in the database).
30//! #[derive(Clone)]
31//! pub struct User {
32//!     id: UserId,
33//!     name: String,
34//!     email: String,
35//! }
36//!
37//! /// Database definition.
38//! pub struct Database {
39//!     /// Users table.
40//!     users: BTreeMap<UserId, User>,
41//!     /// Unique index of users by email.
42//!     user_by_email: BTreeMap<String, UserId>,
43//! }
44//!
45//! // The table macro will autogenerate the users_insert(), users_update() and users_delete()
46//! // methods.
47//! impl Database {
48//!     table!(
49//!         users: User,
50//!         id: UserId,
51//!         missing Error => Error::UserNotFound,
52//!         primary users id => Error::UserIdExists,
53//!         unique user_by_email email => Error::UserEmailExists
54//!     );
55//! }
56//! ```
57//!
58//! See the documentation on [table](macro@table) for more information.
59#![macro_use]
60
61/// Re-expport of paste, which is used internally.
62pub use paste::paste;
63
64#[doc(hidden)]
65#[macro_export]
66macro_rules! table_next_id {
67    ($table:ident: $type:ty) => {
68        $crate::paste! {
69            pub fn [<$table _next_id>](&self) -> $type {
70                self.$table
71                    .keys()
72                    .max()
73                    .map(|key| *key + 1)
74                    .unwrap_or_default()
75            }
76        }
77    };
78}
79
80#[doc(hidden)]
81#[macro_export]
82macro_rules! table_insert {
83    ($table:ident: $type:ty, $pk:ident, $errty:ty) => {
84        $crate::paste! {
85            pub fn [<$table _insert>](&mut self, data: $type) -> Result<(), $errty> {
86                self.[<$table _insert_check>](&data)?;
87                self.[<$table _insert_indices>](&data);
88                self.$table.insert(data.$pk.clone(), data);
89                Ok(())
90            }
91        }
92    };
93}
94
95#[doc(hidden)]
96#[macro_export]
97macro_rules! table_insert_check {
98    ($self:expr, foreign, $table:ident, $data:ident, $expr:expr, $err:expr) => {
99        if $self.$table.get(&$expr).is_none() {
100            return Err($err);
101        }
102    };
103    ($self:expr, unique, $table:ident, $data:ident, $expr:expr, $err:expr) => {
104        if $self.$table.get(&$expr).is_some() {
105            return Err($err);
106        }
107    };
108    ($self:expr, primary, $table:ident, $data:ident, $expr:expr, $err:expr) => {
109        if $self.$table.get(&$expr).is_some() {
110            return Err($err);
111        }
112    };
113    ($self:expr, constraint, $table:ident, $data:ident, $expr:expr, $err:expr) => {
114        if let Err(error) = $self.$table($data) {
115            return Err(error);
116        }
117    };
118    ($self:expr, $other:ident, $table:ident, $data:ident, $expr:expr, $err:expr) => {};
119}
120
121#[doc(hidden)]
122#[macro_export]
123macro_rules! table_insert_checks {
124    ($table:ident: $type:ty, $errty:ty, $($itype:ident $name:ident $prop:tt => $err:expr),*) => {
125        $crate::paste! {
126            fn [<$table _insert_check>](&mut self, data: &$type) -> Result<(), $errty> {
127                $($crate::table_insert_check!(self, $itype, $name, data, $crate::table_prop!(data, $prop), $err);)*
128                Ok(())
129            }
130        }
131    }
132}
133
134#[doc(hidden)]
135#[macro_export]
136macro_rules! table_delete {
137    ($table:ident: $type:ty, $pk:ty, $errty:ty) => {
138        $crate::paste! {
139            pub fn [<$table _delete>](&mut self, id: $pk) -> Result<$type, $errty> {
140                let data = self.[<$table _delete_check>](id.clone())?;
141                self.[<$table _delete_indices>](&data);
142                self.$table.remove(&id);
143                Ok(data)
144            }
145        }
146    };
147}
148
149#[doc(hidden)]
150#[macro_export]
151macro_rules! table_insert_index {
152    ($self:expr, $pk:expr, unique, $name:ident, $prop:expr) => {
153        if $self.$name.insert($prop.clone(), $pk.clone()).is_some() {
154            panic!(concat!(stringify!($name), " index entry already existsted"));
155        }
156    };
157    ($self:expr, $pk:expr, index, $name:ident, $prop:expr) => {
158        if !$self
159            .$name
160            .entry($prop.clone())
161            .or_default()
162            .insert($pk.clone())
163        {
164            panic!(concat!(stringify!($name), " index already had new user"));
165        }
166    };
167    ($self:expr, $pk:expr, $other:ident, $name:ident, $prop:expr) => {};
168}
169
170#[doc(hidden)]
171#[macro_export]
172macro_rules! table_prop {
173    ($data:expr, $prop:ident) => {
174        $data.$prop
175    };
176    ($data:expr, ($($prop:ident),*)) => {
177        ($($crate::table_prop!(@inner, $data, $prop)),*)
178    };
179    (@inner, $data:expr, $prop:ident) => { $data.$prop.clone() };
180    ($data:expr, $prop:tt) => { $prop };
181}
182
183#[doc(hidden)]
184#[macro_export]
185macro_rules! table_insert_indices {
186    ($table:ident: $type:ty, $pk:ident, $($itype:ident $name:ident $prop:tt => $err:expr),*) => {
187        $crate::paste! {
188            fn [<$table _insert_indices>](&mut self, data: &$type) {
189                $($crate::table_insert_index!(self, data.$pk, $itype, $name, $crate::table_prop!(data, $prop));)*
190            }
191        }
192    }
193}
194
195#[doc(hidden)]
196#[macro_export]
197macro_rules! table_delete_index {
198    ($self:expr, $pk:expr, unique, $name:ident, $prop:expr) => {
199        match $self.$name.remove(&$prop) {
200            None => panic!(concat!(stringify!($name), " unique index missing item")),
201            Some(value) if value != $pk => {
202                panic!(concat!(stringify!($name), " unique index had wrong key"))
203            }
204            _ => {}
205        }
206    };
207    ($self:expr, $pk:expr, reverse, $name:ident, $prop:expr) => {
208        match $self.$name.remove(&$pk) {
209            Some(value) if !value.is_empty() => {
210                panic!(concat!(stringify!($name, " reverse index not empty")))
211            }
212            _ => {}
213        }
214    };
215    ($self:expr, $pk:expr, index, $name:ident, $prop:expr) => {
216        let values = $self
217            .$name
218            .get_mut(&$prop)
219            .expect(concat!(stringify!($name), " index missing"));
220        if !values.remove(&$pk) {
221            panic!(concat!(stringify!($name), " index already had new user"));
222        }
223        if values.is_empty() {
224            $self.$name.remove(&$prop);
225        }
226    };
227    ($self:expr, $pk:expr, $other:ident, $name:ident, $prop:expr) => {};
228}
229
230#[doc(hidden)]
231#[macro_export]
232macro_rules! table_delete_indices {
233    ($table:ident: $type:ty, $pk:ident, $($itype:ident $name:ident $prop:tt => $err:expr),*) => {
234        $crate::paste! {
235            fn [<$table _delete_indices>](&mut self, data: &$type) {
236                $($crate::table_delete_index!(self, data.$pk, $itype, $name, $crate::table_prop!(data, $prop));)*
237            }
238        }
239    }
240}
241
242#[doc(hidden)]
243#[macro_export]
244macro_rules! table_update_index {
245    ($self:expr, $pk:expr, index, $name:ident, $old:expr, $new:expr) => {
246        if $old != $new {
247            $crate::table_delete_index!($self, $pk, index, $name, $old);
248            $crate::table_insert_index!($self, $pk, index, $name, $new);
249        }
250    };
251    ($self:expr, $pk:expr, unique, $name:ident, $old:expr, $new:expr) => {
252        if $old != $new {
253            $crate::table_delete_index!($self, $pk, unique, $name, $old);
254            $crate::table_insert_index!($self, $pk, unique, $name, $new);
255        }
256    };
257    ($self:expr, $pk:expr, reverse, $name:ident, $old:expr, $new:expr) => {
258        if $old != $new {
259            $crate::table_delete_index!($self, $pk, reverse, $name, $old);
260            $crate::table_insert_index!($self, $pk, reverse, $name, $new);
261        }
262    };
263    ($self:expr, $pk:expr, $other:ident, $name:ident, $old:expr, $new:expr) => {};
264}
265
266#[doc(hidden)]
267#[macro_export]
268macro_rules! table_update_indices {
269    ($table:ident: $type:ty, $pk:ident, $($itype:ident $name:ident $prop:tt => $err:expr),*) => {
270        $crate::paste! {
271            fn [<$table _update_indices>](&mut self, old: &$type, new: &$type) {
272                $($crate::table_update_index!(self, old.$pk, $itype, $name, $crate::table_prop!(old, $prop), $crate::table_prop!(new, $prop));)*
273            }
274        }
275    }
276}
277
278#[doc(hidden)]
279#[macro_export]
280macro_rules! table_update_check {
281    ($self:expr, $pk:expr, unique, $name:ident, $data:ident, $old:expr, $new:expr, $err:expr) => {
282        if $old != $new {
283            $crate::table_insert_check!($self, unique, $name, $data, $new, $err);
284        }
285    };
286    ($self:expr, $pk:expr, foreign, $name:ident, $data:ident, $old:expr, $new:expr, $err:expr) => {
287        if $old != $new {
288            $crate::table_insert_check!($self, foreign, $name, $data, $new, $err);
289        }
290    };
291    ($self:expr, $pk:expr, constraint, $name:ident, $data:ident, $old:expr, $new:expr, $err:expr) => {
292        $crate::table_insert_check!($self, constraint, $name, $data, $new, $err);
293    };
294    ($self:expr, $pk:expr, $other:ident, $name:ident, $data:ident, $old:expr, $new:expr, $err:expr) => {};
295}
296
297#[doc(hidden)]
298#[macro_export]
299macro_rules! table_update_checks {
300    ($table:ident: $type:ty, $pk:ident, $errty:ty, $($itype:ident $name:ident $prop:tt => $err:expr),*) => {
301        $crate::paste! {
302            fn [<$table _update_check>](&mut self, old: &$type, new: &$type) -> Result<(), $errty> {
303                $($crate::table_update_check!(self, old.$pk, $itype, $name, new, $crate::table_prop!(old, $prop), $crate::table_prop!(new, $prop), $err);)*
304                Ok(())
305            }
306        }
307    }
308}
309
310#[doc(hidden)]
311#[macro_export]
312macro_rules! table_delete_check {
313    ($self:expr, $pk:expr, reverse, $name:ident, $prop:expr, $err:expr) => {
314        match $self.$name.get(&$pk) {
315            None => {}
316            Some(items) if items.is_empty() => {
317                panic!(concat!(stringify!($name), " has empty index"))
318            }
319            Some(_items) => return Err($err),
320        }
321    };
322    ($self:expr, $pk:expr, $other:ident, $name:ident, $prop:expr, $err:expr) => {};
323}
324
325#[doc(hidden)]
326#[macro_export]
327macro_rules! table_delete_checks {
328    ($table:ident: $type:ty, $pk:ident: $pkty:ty, $errty:ty, $missing:expr, $($itype:ident $name:ident $prop:tt => $err:expr),*) => {
329        $crate::paste! {
330            fn [<$table _delete_check>](&mut self, id: $pkty) -> Result<$type, $errty> {
331                let row = match self.$table.get(&id) {
332                    Some(row) => row.clone(),
333                    None => return Err($missing),
334                };
335
336                $($crate::table_delete_check!(self, row.$pk, $itype, $name, $crate::table_prop!(row, $prop), $err);)*
337
338                Ok(row)
339            }
340        }
341    };
342}
343
344#[doc(hidden)]
345#[macro_export]
346macro_rules! table_indices {
347    ($table:ident: $type:ty, $pk:ident: $pkty:ty, $errty:ty, $error:expr, $($itype:ident $name:ident $prop:tt => $err:expr),*) => {
348        $crate::table_insert_checks!($table: $type, $errty, $($itype $name $prop => $err),*);
349        $crate::table_update_checks!($table: $type, $pk, $errty, $($itype $name $prop => $err),*);
350        $crate::table_delete_checks!($table: $type, $pk: $pkty, $errty, $error, $($itype $name $prop => $err),*);
351        $crate::table_insert_indices!($table: $type, $pk, $($itype $name $prop => $err),*);
352        $crate::table_delete_indices!($table: $type, $pk, $($itype $name $prop => $err),*);
353        $crate::table_update_indices!($table: $type, $pk, $($itype $name $prop => $err),*);
354    }
355}
356
357#[doc(hidden)]
358#[macro_export]
359macro_rules! table_update {
360    ($table:ident: $type:ty, $pk:ident => $err:expr, $errty:ty) => {
361        $crate::paste! {
362            pub fn [<$table _update>](&mut self, new: $type) -> Result<$type, $errty> {
363                let old = match self.$table.get(&new.$pk) {
364                    Some(value) => value.clone(),
365                    None => return Err($err),
366                };
367                self.[<$table _update_check>](&old, &new)?;
368                self.[<$table _update_indices>](&old, &new);
369                self.$table.insert(new.$pk.clone(), new);
370                Ok(old)
371            }
372        }
373    };
374}
375
376/// # Table Macro
377///
378/// Generate database methods (insert, update and delete) for a table.
379/// This macro takes a definition of the database table and indices and generates helper
380/// methods used to safely insert, update and delete rows in the database.
381///
382/// ## Usage
383///
384/// In order to use the macro, you need an error type for your database,
385/// a row type per table (for example, a User struct for your users table), and a struct
386/// for the database itself.
387///
388/// ### Error Type
389///
390/// The error type is usually a simple enum. For every table, you need an error case for when a
391/// row is missing, as well as when it exists. Additionally, you need one error type per unique
392/// index.
393///
394/// Here is an example of the what the error type might look like:
395///
396/// ```rust
397/// pub enum Error {
398///     UserIdExists,
399///     UserEmailExists,
400///     UserNotFound,
401///     UserNameEmpty,
402///     GroupNameExists,
403///     GroupIdExists,
404///     GroupNotFound,
405///     GroupNotEmpty,
406///     GroupNameEmpty,
407/// }
408/// ```
409///
410/// ### Row Types
411///
412/// For every table, you need a row type. This can just be a regular Rust struct. It needs to
413/// derive [Clone][], and it needs to have some kind of primary key. The primary key uniquely
414/// identifies the row for update and deletion operations. Usually, an integer type is
415/// recommended here. For visual clarity, you can create a type alias for this field. We will
416/// refer to the row type as `RowType` and to the type of the primary key as `RowId`.
417///
418/// Here is an example of what *User* and *Group* row types might look like.
419///
420/// ```rust
421/// type UserId = u64;
422///
423/// #[derive(Clone)]
424/// pub struct User {
425///     id: UserId,
426///     name: String,
427///     email: String,
428///     group: GroupId,
429/// }
430///
431/// type GroupId = u64;
432///
433/// #[derive(Clone)]
434/// pub struct Group {
435///     id: GroupId,
436///     name: String,
437/// }
438/// ```
439///
440///
441/// ### Database Struct
442///
443/// The database struct must contain one map per table and one per index.
444/// 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][std::collections::BTreeMap]
445/// and [HashMap][std::collections::HashMap].
446/// For every unique index, a map of the shape `MapType<IndexType,
447/// RowId>` must be added.
448/// For every index, a map of the shape `MapType<IndexType, Set<RowId>>`
449/// needs to be added.
450/// The set type that is used
451/// does not matter, although common choices are [BTreeSet][std::collections::BTreeSet]
452/// and [HashSet][std::collections::HashSet].
453/// The *IndexType* is the type of the field of the index. For instance, if
454/// the index is on a field of type [String][], that is the IndexType.
455///
456/// | Kind | Type |
457/// | --- | --- |
458/// | Table | `MapType<RowId, RowType>` |
459/// | Index | `MapType<IndexType, Set<RowId>>` |
460/// | Unique index | `MapType<IndexType, RowId>` |
461///
462/// For example, to define a database struct with two tables (*users* and *groups*),
463/// two unique indices (*user_by_email* and *group_by_name*), and one regular index
464/// (*users_by_group*), this struct definition could be used:
465///
466/// ```rust
467/// # type UserId = u64;
468/// # type GroupId = u64;
469/// # struct User;
470/// # struct Group;
471/// use std::collections::{BTreeMap, HashMap, BTreeSet, HashSet};
472///
473/// pub struct Database {
474///     /// Users table
475///     users: BTreeMap<UserId, User>,
476///     /// User by email unique index
477///     user_by_email: HashMap<String, UserId>,
478///
479///     /// Groups table
480///     groups: HashMap<GroupId, Group>,
481///     /// Users by group index
482///     users_by_group: BTreeMap<GroupId, BTreeSet<UserId>>,
483///     /// Group by name unique index
484///     group_by_name: BTreeMap<String, GroupId>,
485/// }
486/// ```
487///
488/// ## Syntax
489///
490/// The basic syntax of the macro looks like this:
491///
492/// ```rust,ignore
493/// table!(
494///     $table_name: RowType,
495///     $id_field: RowId,
496///     missing ErrorType => $missing_error,
497///     primary $table_name $id_field => $exists_error,
498///     <constraints...>
499///     <indices...>
500/// );
501/// ```
502///
503/// Here is an overview of what the various placeholders mean:
504///
505/// | Placeholder | Example | Explanation |
506/// | --- | --- | --- |
507/// | `$table_name` | `users` | Name of the table map in the database struct |
508/// | `RowType` | `User` | Name of the data type of the rows |
509/// | `RowId` | `UserId` | Name of the type of the primary keys for the rows |
510/// | `$id_field` | `id` | Name of the struct field of the Row type that contains the primary key |
511/// | `ErrorType` | `Error` | Name of the error type (enum) |
512/// | `$missing_error` | `Error::UserNotFound` | Error to throw when trying to delete a row that does not exists |
513/// | `$exists_error` | `Error::UserIdExists` | Error to throw when trying to insert a row that already exists |
514/// | `<constraints...>` | Definitions for the constraints (if any). |
515/// | `<indices...>` | | Definitions for the indices (explained in next section) |
516///
517/// ### Constraints
518///
519/// The syntax for constraints is the following:
520///
521/// ```rust,ignore
522/// constraint fn_name _ => (),
523/// ```
524///
525/// In order to define constraints, you need to declare a method on your database struct. This
526/// method should return `Result<(), ErrorType>`. For example, you can create a constraint that
527/// enforces that a user name should not be empty. The constraint method might look like this:
528///
529/// ```rust
530/// # enum Error { UserNameEmpty, }
531/// # struct User { name: String }
532/// # struct Database {}
533/// impl Database {
534///     fn name_not_empty(&self, user: &User) -> Result<(), Error> {
535///         if user.name.is_empty() {
536///             return Err(Error::UserNameEmpty);
537///         }
538///
539///         Ok(())
540///     }
541/// }
542/// ```
543///
544/// Defining the constraint in the macro then looks as follows:
545///
546/// ```text
547/// constraint name_not_empty _ => ()
548/// ```
549///
550/// ### Indices
551///
552/// The macro also needs to be told of the various indices. The syntax for indices looks like
553/// this:
554///
555/// ```text
556/// $type $map $field => $error
557/// ```
558///
559/// Here, `$type` refers to the type of index (can be `index`, `unique`, `foreign` or
560/// `reverse`). `$map` is the name of the map field in the database struct that represents this
561/// index. `$field` is the name of the field of the `RowType` that this index is on. The field can
562/// also be a compound key, by writing it as a tuple (for example `(first_name, last_name)`. Finally,
563/// `$error` is the error that is thrown when this index is violated. Here is an overview of the
564/// index types:
565///
566/// | Type | Example | Explanation |
567/// | --- | --- | --- |
568/// | Index | `index users_by_group group => ()` | Defines a simple index to look up rows based on their group. Does not need an error. |
569/// | 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. |
570/// | 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. |
571/// | 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. |
572///
573/// The result of this is that the macro generates insertion, update and deletion methods for
574/// every table. It uses the table map name as the prefix for those methods. For example,
575/// calling it on a table with the name *users* results in three methods being generated:
576///
577/// ```rust,ignore
578/// impl Database {
579///     /// Insert a User into the database, or return an error.
580///     pub fn users_insert(row: User) -> Result<(), Error>;
581///
582///     /// Update a User row (identified by the primary key), returning the old row, or return
583///     /// an error.
584///     pub fn users_update(row: User) -> Result<User, Error>;
585///
586///     /// Delete a User row (identified by the primary key), returning the row, or return an
587///     /// error.
588///     pub fn users_delete(id: UserId) -> Result<User, Error>;
589/// }
590/// ```
591///
592/// ## Example
593///
594/// Here is an example invocation of the macro on the Database struct with two tables (*users* and
595/// *groups*), including indices and foreign key constraints:
596///
597/// ```rust
598/// # use std::collections::{BTreeMap, HashMap, BTreeSet, HashSet};
599/// use macrodb::table;
600/// # pub enum Error {
601/// #     UserIdExists,
602/// #     UserEmailExists,
603/// #     UserNotFound,
604/// #     UserNameEmpty,
605/// #     GroupNameExists,
606/// #     GroupIdExists,
607/// #     GroupNotFound,
608/// #     GroupNotEmpty,
609/// #     GroupNameEmpty,
610/// # }
611/// # type UserId = u64;
612/// # #[derive(Clone)]
613/// # pub struct User {
614/// #     id: UserId,
615/// #     name: String,
616/// #     email: String,
617/// #     group: GroupId,
618/// # }
619/// # type GroupId = u64;
620/// # #[derive(Clone)]
621/// # pub struct Group {
622/// #     id: GroupId,
623/// #     name: String,
624/// # }
625/// # pub struct Database {
626/// #     users: BTreeMap<UserId, User>,
627/// #     user_by_email: HashMap<String, UserId>,
628/// #     groups: HashMap<GroupId, Group>,
629/// #     users_by_group: BTreeMap<GroupId, BTreeSet<UserId>>,
630/// #     group_by_name: BTreeMap<String, GroupId>,
631/// # }
632/// impl Database {
633///     fn user_name_not_empty(&self, user: &User) -> Result<(), Error> {
634///         if user.name.is_empty() {
635///             return Err(Error::UserNameEmpty);
636///         }
637///
638///         Ok(())
639///     }
640///
641///     fn group_name_not_empty(&self, group: &Group) -> Result<(), Error> {
642///         if group.name.is_empty() {
643///             return Err(Error::GroupNameEmpty);
644///         }
645///
646///         Ok(())
647///     }
648///
649///     table!(
650///         users: User,
651///         id: UserId,
652///         missing Error => Error::UserNotFound,
653///         primary users id => Error::UserIdExists,
654///         foreign groups group => Error::GroupNotFound,
655///         index users_by_group group => (),
656///         constraint user_name_not_empty _ => (),
657///         unique user_by_email email => Error::UserEmailExists
658///     );
659///     table!(
660///         groups: Group,
661///         id: GroupId,
662///         missing Error => Error::GroupNotFound,
663///         primary users id => Error::GroupIdExists,
664///         constraint group_name_not_empty _ => (),
665///         reverse users_by_group id => Error::GroupNotEmpty,
666///         unique group_by_name name => Error::GroupNameExists
667///     );
668/// }
669/// ```
670#[macro_export]
671macro_rules! table {
672    ($table:ident: $type:ty, $pk:ident: $pkty:ty, missing $errty:ty => $missing:expr, $($itype:ident $name:ident $prop:tt => $err:expr),*) => {
673        $crate::table_next_id!($table: $pkty);
674        $crate::table_indices!($table: $type, $pk: $pkty, $errty, $missing, $($itype $name $prop => $err),*);
675        $crate::table_delete!($table: $type, $pkty, $errty);
676        $crate::table_insert!($table: $type, $pk, $errty);
677        $crate::table_update!($table: $type, $pk => $missing, $errty);
678    };
679    ($table:ident: $type:ty, $pk:ident: $pkty:ty, noautokey, missing $errty:ty => $missing:expr, $($itype:ident $name:ident $prop:tt => $err:expr),*) => {
680        $crate::table_indices!($table: $type, $pk: $pkty, $errty, $missing, $($itype $name $prop => $err),*);
681        $crate::table_delete!($table: $type, $pkty, $errty);
682        $crate::table_insert!($table: $type, $pk, $errty);
683        $crate::table_update!($table: $type, $pk => $missing, $errty);
684    }
685}