Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 13 additions & 13 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,16 @@
resolver = "3"
members = ["crates/*"]
exclude = [
"examples/basic",
"examples/filtering",
"examples/relations",
"examples/events",
"examples/hooks",
"examples/commands",
"examples/transactions",
"examples/soft-delete",
"examples/streams",
"examples/full-app",
"examples/basic",
"examples/filtering",
"examples/relations",
"examples/events",
"examples/hooks",
"examples/commands",
"examples/transactions",
"examples/soft-delete",
"examples/streams",
"examples/full-app",
]

[workspace.package]
Expand All @@ -26,9 +26,9 @@ license = "MIT"
repository = "https://github.com/RAprogramm/entity-derive"

[workspace.dependencies]
entity-core = { path = "crates/entity-core", version = "0.2.0" }
entity-derive = { path = "crates/entity-derive", version = "0.4.0" }
entity-derive-impl = { path = "crates/entity-derive-impl", version = "0.2.0" }
entity-core = { path = "crates/entity-core", version = "0.3.0" }
entity-derive = { path = "crates/entity-derive", version = "0.5.0" }
entity-derive-impl = { path = "crates/entity-derive-impl", version = "0.3.0" }
syn = { version = "2", features = ["full", "extra-traits", "parsing"] }
quote = "1"
proc-macro2 = "1"
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<a id="top"></a>

<p align="center">
<img src="logo.png" alt="entity-derive logo" width="200"/>
<h1 align="center">entity-derive</h1>
<p align="center">
<strong>One macro to rule them all</strong>
Expand Down
6 changes: 4 additions & 2 deletions crates/entity-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

[package]
name = "entity-core"
version = "0.2.0"
version = "0.3.0"
edition = "2024"
rust-version = "1.92"
authors = ["RAprogramm <andrey.rozanov.vl@gmail.com>"]
Expand All @@ -24,7 +24,9 @@ streams = ["serde", "serde_json", "futures"]

[dependencies]
async-trait = "0.1"
sqlx = { version = "0.8", optional = true, default-features = false, features = ["postgres"] }
sqlx = { version = "0.8", optional = true, default-features = false, features = [
"postgres",
] }
serde = { version = "1", features = ["derive"], optional = true }
serde_json = { version = "1", optional = true }
futures = { version = "0.3", optional = true }
Expand Down
9 changes: 7 additions & 2 deletions crates/entity-derive-impl/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

[package]
name = "entity-derive-impl"
version = "0.2.0"
version = "0.3.0"
edition = "2024"
rust-version = "1.92"
authors = ["RAprogramm <andrey.rozanov.vl@gmail.com>"]
Expand Down Expand Up @@ -34,7 +34,12 @@ uuid = { version = "1", features = ["v4", "v7", "serde"] }
chrono = { version = "0.4", features = ["serde"] }
serde = { version = "1", features = ["derive"] }
async-trait = "0.1"
sqlx = { version = "0.8", features = ["runtime-tokio", "postgres", "uuid", "chrono"] }
sqlx = { version = "0.8", features = [
"runtime-tokio",
"postgres",
"uuid",
"chrono",
] }
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
utoipa = { version = "5", features = ["chrono", "uuid"] }
validator = { version = "0.20", features = ["derive"] }
Expand Down
3 changes: 3 additions & 0 deletions crates/entity-derive-impl/src/entity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ mod events;
mod hooks;
mod insertable;
mod mappers;
mod migrations;
pub mod parse;
mod policy;
mod projection;
Expand Down Expand Up @@ -110,6 +111,7 @@ fn generate(entity: EntityDef) -> TokenStream {
let insertable = insertable::generate(&entity);
let mappers = mappers::generate(&entity);
let sql = sql::generate(&entity);
let migrations = migrations::generate(&entity);

let expanded = quote! {
#dto
Expand All @@ -127,6 +129,7 @@ fn generate(entity: EntityDef) -> TokenStream {
#insertable
#mappers
#sql
#migrations
};

expanded.into()
Expand Down
118 changes: 118 additions & 0 deletions crates/entity-derive-impl/src/entity/migrations.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// SPDX-FileCopyrightText: 2025-2026 RAprogramm <andrey.rozanov.vl@gmail.com>
// SPDX-License-Identifier: MIT

//! Migration generation for entity-derive.
//!
//! Generates `MIGRATION_UP` and `MIGRATION_DOWN` constants containing
//! SQL DDL statements for creating/dropping tables.
//!
//! # Features
//!
//! - Full type mapping (Rust → PostgreSQL)
//! - Column constraints (UNIQUE, CHECK, DEFAULT)
//! - Indexes (btree, hash, gin, gist, brin)
//! - Foreign keys with ON DELETE actions
//! - Composite indexes
//!
//! # Usage
//!
//! ```rust,ignore
//! #[derive(Entity)]
//! #[entity(table = "users", migrations)]
//! pub struct User {
//! #[id]
//! pub id: Uuid,
//!
//! #[column(unique, index)]
//! pub email: String,
//! }
//!
//! // Apply migration:
//! sqlx::query(User::MIGRATION_UP).execute(&pool).await?;
//! ```

mod postgres;
pub mod types;

use proc_macro2::TokenStream;

use super::parse::{DatabaseDialect, EntityDef};

/// Generate migration constants based on entity configuration.
///
/// Returns empty `TokenStream` if migrations are not enabled.
pub fn generate(entity: &EntityDef) -> TokenStream {
if !entity.migrations {
return TokenStream::new();
}

match entity.dialect {
DatabaseDialect::Postgres => postgres::generate(entity),
DatabaseDialect::ClickHouse => TokenStream::new(), // TODO: future
DatabaseDialect::MongoDB => TokenStream::new() // N/A for document DB
}
}

#[cfg(test)]
mod tests {
use syn::DeriveInput;

use super::*;

fn parse_entity(tokens: proc_macro2::TokenStream) -> EntityDef {
let input: DeriveInput = syn::parse_quote!(#tokens);
EntityDef::from_derive_input(&input).unwrap()
}

#[test]
fn generate_returns_empty_when_migrations_disabled() {
let entity = parse_entity(quote::quote! {
#[entity(table = "users")]
pub struct User {
#[id]
pub id: uuid::Uuid,
}
});
let result = generate(&entity);
assert!(result.is_empty());
}

#[test]
fn generate_returns_tokens_when_migrations_enabled() {
let entity = parse_entity(quote::quote! {
#[entity(table = "users", migrations)]
pub struct User {
#[id]
pub id: uuid::Uuid,
}
});
let result = generate(&entity);
assert!(!result.is_empty());
}

#[test]
fn generate_returns_empty_for_clickhouse() {
let entity = parse_entity(quote::quote! {
#[entity(table = "users", dialect = "clickhouse", migrations)]
pub struct User {
#[id]
pub id: uuid::Uuid,
}
});
let result = generate(&entity);
assert!(result.is_empty());
}

#[test]
fn generate_returns_empty_for_mongodb() {
let entity = parse_entity(quote::quote! {
#[entity(table = "users", dialect = "mongodb", migrations)]
pub struct User {
#[id]
pub id: uuid::Uuid,
}
});
let result = generate(&entity);
assert!(result.is_empty());
}
}
52 changes: 52 additions & 0 deletions crates/entity-derive-impl/src/entity/migrations/postgres.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// SPDX-FileCopyrightText: 2025-2026 RAprogramm <andrey.rozanov.vl@gmail.com>
// SPDX-License-Identifier: MIT

//! PostgreSQL migration generation.
//!
//! Generates `MIGRATION_UP` and `MIGRATION_DOWN` constants for PostgreSQL.

mod ddl;

use proc_macro2::TokenStream;
use quote::quote;

use crate::{entity::parse::EntityDef, utils::marker};

/// Generate migration constants for PostgreSQL.
///
/// # Generated Code
///
/// ```rust,ignore
/// impl User {
/// pub const MIGRATION_UP: &'static str = "CREATE TABLE...";
/// pub const MIGRATION_DOWN: &'static str = "DROP TABLE...";
/// }
/// ```
pub fn generate(entity: &EntityDef) -> TokenStream {
let entity_name = entity.name();
let vis = &entity.vis;

let up_sql = ddl::generate_up(entity);
let down_sql = ddl::generate_down(entity);

let marker = marker::generated();

quote! {
#marker
impl #entity_name {
/// SQL migration to create this entity's table, indexes, and constraints.
///
/// # Usage
///
/// ```rust,ignore
/// sqlx::query(User::MIGRATION_UP).execute(&pool).await?;
/// ```
#vis const MIGRATION_UP: &'static str = #up_sql;

/// SQL migration to drop this entity's table.
///
/// Uses CASCADE to drop dependent objects.
#vis const MIGRATION_DOWN: &'static str = #down_sql;
}
}
}
Loading