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}