Skip to main content

wasm_dbms_macros/
lib.rs

1#![crate_name = "wasm_dbms_macros"]
2#![crate_type = "lib"]
3#![cfg_attr(docsrs, feature(doc_cfg))]
4#![deny(clippy::print_stdout)]
5#![deny(clippy::print_stderr)]
6
7//! Runtime-agnostic procedural macros for the wasm-dbms DBMS engine.
8//!
9//! This crate provides procedural macros to automatically implement traits
10//! required by the `wasm-dbms` engine.
11//!
12//! ## Provided Derive Macros
13//!
14//! - `Encode`: Automatically implements the `Encode` trait for structs.
15//! - `Table`: Automatically implements the `TableSchema` trait and associated types.
16//! - `DatabaseSchema`: Generates `DatabaseSchema<M>` trait dispatch and `register_tables`.
17//! - `CustomDataType`: Bridge user-defined types into the `Value` system.
18
19#![doc(html_playground_url = "https://play.rust-lang.org")]
20#![doc(
21    html_favicon_url = "https://raw.githubusercontent.com/veeso/wasm-dbms/main/assets/images/cargo/logo-128.png"
22)]
23#![doc(
24    html_logo_url = "https://raw.githubusercontent.com/veeso/wasm-dbms/main/assets/images/cargo/logo-512.png"
25)]
26
27use proc_macro::TokenStream;
28use syn::{DeriveInput, parse_macro_input};
29
30mod custom_data_type;
31mod database_schema;
32mod encode;
33mod table;
34mod utils;
35
36/// Automatically implements the `Encode` trait for a struct.
37///
38/// This derive macro generates two methods required by the `Encode` trait:
39///
40/// - `fn data_size() -> DataSize`
41///   Computes the static size of the encoded type.
42///   If all fields implement `Encode::data_size()` returning
43///   `DataSize::Fixed(n)`, then the type is also considered fixed-size.
44///   Otherwise, the type is `DataSize::Dynamic`.
45///
46/// - `fn size(&self) -> MSize`
47///   Computes the runtime-encoding size of the value by summing the
48///   sizes of all fields, saturating at `MSize::MAX` when the sum does not
49///   fit.
50///
51/// # What the macro generates
52///
53/// Given a struct like:
54///
55/// ```rust,ignore
56/// #[derive(Encode)]
57/// struct User {
58///     id: Uint32,
59///     name: Text,
60/// }
61/// ```
62///
63/// The macro expands into:
64///
65/// ```rust,ignore
66/// impl Encode for User {
67///     const DATA_SIZE: DataSize = DataSize::Dynamic; // or DataSize::Fixed(n) if applicable
68///
69///     fn size(&self) -> MSize {
70///         MSize::MIN.saturating_add(self.id.size()).saturating_add(self.name.size())
71///     }
72///
73///     fn encode(&'_ self) -> std::borrow::Cow<'_, [u8]> {
74///         let mut __wasm_dbms_encoded = Vec::with_capacity(self.size() as usize);
75///         __wasm_dbms_encoded.extend_from_slice(&self.id.encode());
76///         __wasm_dbms_encoded.extend_from_slice(&self.name.encode());
77///         std::borrow::Cow::Owned(__wasm_dbms_encoded)
78///     }
79///
80///     fn decode(__wasm_dbms_data: std::borrow::Cow<[u8]>) -> ::wasm_dbms_api::prelude::MemoryResult<Self> {
81///         let mut __wasm_dbms_offset = 0;
82///         let id = Uint32::decode(std::borrow::Borrowed(&__wasm_dbms_data[__wasm_dbms_offset..]))?;
83///         __wasm_dbms_offset += id.size() as usize;
84///         let name = Text::decode(std::borrow::Borrowed(&__wasm_dbms_data[__wasm_dbms_offset..]))?;
85///         __wasm_dbms_offset += name.size() as usize;
86///         Ok(Self { id, name })
87///     }
88/// }
89/// ```
90/// # Zero-size structs
91///
92/// A struct without fields (`struct Empty {}` or `struct Empty;`) encodes to zero bytes:
93/// `SIZE` is `DataSize::Fixed(0)` and `ALIGNMENT` is `1`. A fixed-size struct is otherwise
94/// aligned to its size; whenever that size is `0` (e.g. only zero-size fields) the alignment
95/// is `1`, so storage never divides by a zero alignment.
96///
97/// # Requirements
98///
99/// - Each field type must implement `Encode`.
100/// - Only works on `struct`s; enums and unions are not supported.
101/// - All field identifiers must be valid Rust identifiers (no tuple structs).
102///
103/// # Notes
104///
105/// - It is intended for internal use within the `wasm-dbms` DBMS memory
106///   system.
107///
108/// # Errors
109///
110/// The macro will fail to expand if:
111///
112/// - The struct has unnamed fields (tuple struct)
113/// - A field type does not implement `Encode`
114/// - The macro is applied to a non-struct item.
115///
116/// # Example
117///
118/// ```rust,ignore
119/// #[derive(Encode, Debug, PartialEq, Eq)]
120/// struct Position {
121///     x: Int32,
122///     y: Int32,
123/// }
124///
125/// let pos = Position { x: 10.into(), y: 20.into() };
126/// assert_eq!(Position::data_size(), DataSize::Fixed(8));
127/// assert_eq!(pos.size(), 8);
128/// let encoded = pos.encode();
129/// let decoded = Position::decode(encoded).unwrap();
130/// assert_eq!(pos, decoded);
131/// ```
132#[proc_macro_derive(Encode)]
133pub fn derive_encode(input: TokenStream) -> TokenStream {
134    let input = parse_macro_input!(input as DeriveInput);
135    self::encode::encode(input, None)
136        .expect("Failed to derive `Encode`")
137        .into()
138}
139
140/// Given a struct representing a database table, automatically implements
141/// the `TableSchema` trait with all the necessary types to work with the wasm-dbms engine.
142/// So given this struct:
143///
144/// ```rust,ignore
145/// #[derive(Table, Encode)]
146/// #[table = "posts"]
147/// struct Post {
148///     #[primary_key]
149///     id: Uint32,
150///     title: Text,
151///     content: Text,
152///     #[foreign_key(entity = "User", table = "users", column = "id")]
153///     author_id: Uint32,
154/// }
155/// ```
156///
157/// What we expect as output is:
158///
159/// - To implement the `TableSchema` trait for the struct as follows:
160///
161///     ```rust,ignore
162///     impl TableSchema for Post {
163///         type Insert = PostInsertRequest;
164///         type Record = PostRecord;
165///         type Update = PostUpdateRequest;
166///         type ForeignFetcher = PostForeignFetcher;
167///
168///         fn columns() -> &'static [ColumnDef] {
169///             &[
170///                 ColumnDef {
171///                     name: "id",
172///                     data_type: DataTypeKind::Uint32,
173///                     auto_increment: false,
174///                     nullable: false,
175///                     primary_key: true,
176///                     unique: true,
177///                     foreign_key: None,
178///                 },
179///                 ColumnDef {
180///                     name: "title",
181///                     data_type: DataTypeKind::Text,
182///                     auto_increment: false,
183///                     nullable: false,
184///                     primary_key: false,
185///                     unique: false,
186///                     foreign_key: None,
187///                 },
188///                 ColumnDef {
189///                     name: "content",
190///                     data_type: DataTypeKind::Text,
191///                     auto_increment: false,
192///                     nullable: false,
193///                     primary_key: false,
194///                     unique: false,
195///                     foreign_key: None,
196///                 },
197///                 ColumnDef {
198///                     name: "user_id",
199///                     data_type: DataTypeKind::Uint32,
200///                     auto_increment: false,
201///                     nullable: false,
202///                     primary_key: false,
203///                     unique: false,
204///                     foreign_key: Some(ForeignKeyDef {
205///                         local_column: "user_id",
206///                         foreign_table: "users",
207///                         foreign_column: "id",
208///                     }),
209///                 },
210///             ]
211///         }
212///
213///         fn table_name() -> &'static str {
214///             "posts"
215///         }
216///
217///         fn primary_key() -> &'static str {
218///             "id"
219///         }
220///
221///         fn to_values(self) -> Vec<(ColumnDef, Value)> {
222///             vec![
223///                 (Self::columns()[0], Value::Uint32(self.id)),
224///                 (Self::columns()[1], Value::Text(self.title)),
225///                 (Self::columns()[2], Value::Text(self.content)),
226///                 (Self::columns()[3], Value::Uint32(self.user_id)),
227///             ]
228///         }
229///     }
230///     ```
231///
232/// - Implement the associated `Record` type
233/// - Implement the associated `InsertRecord` type
234/// - Implement the associated `UpdateRecord` type
235/// - If has foreign keys, implement the associated `ForeignFetcher`
236///
237/// So for each struct deriving `Table`, we will generate the following type. Given `${StructName}`, we will generate:
238///
239/// - `${StructName}Record` - implementing `TableRecord`
240/// - `${StructName}InsertRequest` - implementing `InsertRecord`
241/// - `${StructName}UpdateRequest` - implementing `UpdateRecord`
242/// - `${StructName}ForeignFetcher` (only if foreign keys are present)
243///
244/// Every generated type is declared with the visibility of the annotated struct.
245///
246/// ## Generics
247///
248/// Type parameters and the `where` clause of the struct are carried over to every
249/// generated type and impl, so a table can be generic over a custom data type:
250///
251/// ```rust,ignore
252/// #[derive(Debug, Clone, PartialEq, Eq, Table)]
253/// #[table = "tagged"]
254/// pub struct Tagged<T>
255/// where
256///     T: CustomDataType,
257/// {
258///     #[primary_key]
259///     id: Uint32,
260///     #[custom_type]
261///     tag: T,
262/// }
263/// ```
264///
265/// Type parameters must be `'static`; the generated impls add that bound. Lifetime
266/// and const generic parameters are rejected with a compile error.
267///
268/// Also, we will implement the `TableSchema` trait for the struct itself and derive `Encode` for `${StructName}`.
269///
270/// ## Attributes
271///
272/// The `Table` derive macro supports the following attributes:
273///
274/// - `#[alignment = N]`: (optional) Specifies the alignment for the table records. Use only if you know what you are doing.
275/// - `#[autoincrement]`: Marks a field as auto-incrementing. The macro will generate code to automatically fill in values for this field during inserts. Auto-increment fields must be non-nullable and cannot be marked as `#[unique]`.
276/// - `#[candid]`: Marks the table as compatible with Candid serialization.
277/// - `#[custom_type = "TypeName"]`: Specifies a custom data type for the field.
278/// - `#[default = <expr>]`: Field-level default value used by the migration planner when adding a non-nullable column. The expression must convert into the column's `Value` variant via `From`/`Into` (e.g. `#[default = 0]` on a `Uint32` column).
279/// - `#[foreign_key(entity = "EntityName", table = "table_name", column = "column_name")]`: Defines a foreign key relationship.
280/// - `#[index]`: Marks a field to be indexed for faster queries.
281/// - `#[migrate]`: Struct-level attribute that suppresses the macro's default `impl Migrate for T {}` so the user can provide a hand-written impl with custom `default_value` / `transform_column` overrides.
282/// - `#[primary_key]`: Marks a field as the primary key of the table.
283/// - `#[renamed_from("old1", "old2", ...)]`: Field-level list of previous column names. The migration planner uses these to detect rename ops when matching a stored column against the compiled column.
284/// - `#[sanitizer(SanitizerType)]`: Specifies a sanitize for the field.
285/// - `#[table = "table_name"]`: Specifies the name of the table in the database.
286/// - `#[unique]`: Marks a field to have a unique constraint.
287/// - `#[validate(ValidatorType)]`: Specifies a validator for the field.
288///
289/// A column cannot be named `where_clause`: the generated `${StructName}UpdateRequest` uses that
290/// name for its filter field, so the derive rejects it with a compile error.
291///
292#[proc_macro_derive(
293    Table,
294    attributes(
295        alignment,
296        autoincrement,
297        candid,
298        custom_type,
299        default,
300        foreign_key,
301        index,
302        migrate,
303        primary_key,
304        renamed_from,
305        sanitizer,
306        table,
307        unique,
308        validate
309    )
310)]
311pub fn derive_table(input: TokenStream) -> TokenStream {
312    let input = parse_macro_input!(input as DeriveInput);
313    self::table::table(input)
314        .unwrap_or_else(|e| e.to_compile_error())
315        .into()
316}
317
318/// Derives the [`CustomDataType`] trait and an `impl From<T> for Value` conversion
319/// for a user-defined enum or struct.
320///
321/// The type must also derive [`Encode`] (for binary serialization) and implement
322/// [`Display`](std::fmt::Display) (for the cached display string in
323/// `CustomValue`).
324///
325/// # Required attribute
326///
327/// - `#[type_tag = "..."]`: A unique string identifier for this custom data type.
328///
329/// # What the macro generates
330///
331/// Given a type like:
332///
333/// ```rust,ignore
334/// #[derive(Encode, CustomDataType)]
335/// #[type_tag = "status"]
336/// enum Status { Active, Inactive }
337/// ```
338///
339/// The macro expands into:
340///
341/// ```rust,ignore
342/// impl CustomDataType for Status {
343///     const TYPE_TAG: &'static str = "status";
344/// }
345///
346/// impl From<Status> for Value {
347///     fn from(val: Status) -> Value {
348///         Value::Custom(CustomValue {
349///             type_tag: "status".to_string(),
350///             encoded: Encode::encode(&val).into_owned(),
351///             display: val.to_string(),
352///         })
353///     }
354/// }
355/// ```
356///
357/// # Note
358///
359/// The user must also provide `Display`, `Default`, and `DataType` implementations
360/// for the type. This macro only bridges the custom type to the `Value` system.
361#[proc_macro_derive(CustomDataType, attributes(type_tag))]
362pub fn derive_custom_data_type(input: TokenStream) -> TokenStream {
363    let input = parse_macro_input!(input as DeriveInput);
364    custom_data_type::custom_data_type(&input)
365        .unwrap_or_else(|e| e.to_compile_error())
366        .into()
367}
368
369/// Generates a [`DatabaseSchema`] implementation that dispatches generic
370/// DBMS operations to the correct concrete table types.
371///
372/// Given a struct annotated with `#[tables(User = "users", Post = "posts")]`,
373/// this macro produces:
374///
375/// - `impl<M: MemoryProvider> DatabaseSchema<M>` with match-arm dispatch
376///   for `select`, `insert`, `delete`, `update`, `validate_insert`,
377///   `validate_update`, and `referenced_tables`.
378/// - An inherent `register_tables` method that registers all tables in a
379///   `DbmsContext`.
380///
381/// # Example
382///
383/// ```rust,ignore
384/// #[derive(DatabaseSchema)]
385/// #[tables(User = "users", Post = "posts")]
386/// pub struct MySchema;
387///
388/// // Register tables during initialization:
389/// MySchema::register_tables(&ctx)?;
390/// ```
391///
392/// # Generics
393///
394/// The schema struct may declare type parameters and a `where` clause; they are
395/// carried over to the generated impls. Table types in `#[tables(...)]` may carry
396/// generic arguments, e.g. `#[tables(Demo<T> = "demo")]`. Lifetime and const
397/// generic parameters are rejected with a compile error.
398///
399/// # Requirements
400///
401/// - Each type in the `#[tables(...)]` attribute must implement
402///   `TableSchema`.
403#[proc_macro_derive(DatabaseSchema, attributes(tables))]
404pub fn derive_database_schema(input: TokenStream) -> TokenStream {
405    let input = parse_macro_input!(input as DeriveInput);
406    self::database_schema::database_schema(input)
407        .unwrap_or_else(|e| e.to_compile_error())
408        .into()
409}