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
1 change: 1 addition & 0 deletions .changepacks/changepack_log_CDjntNpuuGDyO1BES5WxE.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"changes":{"crates/vespertide-macro/Cargo.toml":"Patch","crates/vespertide-exporter/Cargo.toml":"Patch","crates/vespertide-planner/Cargo.toml":"Patch","crates/vespertide-query/Cargo.toml":"Patch","crates/vespertide-cli/Cargo.toml":"Patch","crates/vespertide-core/Cargo.toml":"Patch","crates/vespertide-config/Cargo.toml":"Patch","crates/vespertide/Cargo.toml":"Patch"},"note":"Optimize query","date":"2025-12-15T06:52:49.007765800Z"}
34 changes: 30 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/vespertide-cli/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
src/models
14 changes: 14 additions & 0 deletions crates/vespertide-cli/migrations/0001_test_message.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"$schema": "https://raw.githubusercontent.com/dev-five-git/vespertide/refs/heads/main/schemas/migration.schema.json",
"actions": [
{
"columns": [],
"constraints": [],
"table": "test_table",
"type": "create_table"
}
],
"comment": "test message",
"created_at": "2025-12-15T06:12:57Z",
"version": 1
}
7 changes: 7 additions & 0 deletions crates/vespertide-cli/models/test_table.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"$schema": "https://raw.githubusercontent.com/dev-five-git/vespertide/refs/heads/main/schemas/model.schema.json",
"columns": [],
"constraints": [],
"indexes": [],
"name": "test_table"
}
18 changes: 10 additions & 8 deletions crates/vespertide-cli/src/commands/diff.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,11 +169,7 @@ fn format_constraint_type(constraint: &vespertide_core::TableConstraint) -> Stri
}
}
vespertide_core::TableConstraint::Check { name, expr } => {
if let Some(n) = name {
format!("{} CHECK ({})", n, expr)
} else {
format!("CHECK ({})", expr)
}
format!("{} CHECK ({})", name, expr)
}
}
}
Expand Down Expand Up @@ -345,7 +341,7 @@ mod tests {
MigrationAction::AddConstraint {
table: "users".into(),
constraint: vespertide_core::TableConstraint::Check {
name: Some("check_age".into()),
name: "check_age".into(),
expr: "age > 0".into(),
},
},
Expand Down Expand Up @@ -389,11 +385,17 @@ mod tests {
MigrationAction::RemoveConstraint {
table: "users".into(),
constraint: vespertide_core::TableConstraint::Check {
name: None,
name: "check_age".into(),
expr: "age > 0".into(),
},
},
format!("{} {} {} {}", "Remove constraint:".bright_red(), "CHECK (age > 0)".bright_cyan().bold(), "from".bright_white(), "users".bright_cyan())
format!(
"{} {} {} {}",
"Remove constraint:".bright_red(),
"check_age CHECK (age > 0)".bright_cyan().bold(),
"from".bright_white(),
"users".bright_cyan()
)
)]
#[serial]
fn format_action_cases(#[case] action: MigrationAction, #[case] expected: String) {
Expand Down
8 changes: 3 additions & 5 deletions crates/vespertide-cli/src/commands/log.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use anyhow::Result;
use colored::Colorize;
use vespertide_query::build_plan_queries;
use vespertide_query::{DatabaseBackend, build_plan_queries};

use crate::utils::load_migrations;

Expand Down Expand Up @@ -54,11 +54,9 @@ pub fn cmd_log() -> Result<()> {
println!(
" {}. {}",
(i + 1).to_string().bright_magenta().bold(),
q.sql.trim().bright_white()
q.build(DatabaseBackend::Postgres).trim().bright_white()
);
if !q.binds.is_empty() {
println!(" {} {:?}", "binds:".bright_cyan(), q.binds);
}
println!(" {} {:?}", "binds:".bright_cyan(), q.binds());
}
println!();
}
Expand Down
56 changes: 36 additions & 20 deletions crates/vespertide-cli/src/commands/sql.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use anyhow::Result;
use colored::Colorize;
use vespertide_planner::plan_next_migration;
use vespertide_query::build_plan_queries;
use vespertide_query::{DatabaseBackend, build_plan_queries};

use crate::utils::{load_config, load_migrations, load_models};

Expand Down Expand Up @@ -60,11 +60,9 @@ fn emit_sql(plan: &vespertide_core::MigrationPlan) -> Result<()> {
println!(
"{}. {}",
(i + 1).to_string().bright_magenta().bold(),
q.sql.trim().bright_white()
q.build(DatabaseBackend::Postgres).trim().bright_white()
);
if !q.binds.is_empty() {
println!(" {} {:?}", "binds:".bright_cyan(), q.binds);
}
println!(" {} {:?}", "binds:".bright_cyan(), q.binds());
}

Ok(())
Expand Down Expand Up @@ -137,31 +135,28 @@ mod tests {
let tmp = tempdir().unwrap();
let _guard = CwdGuard::new(&tmp.path().to_path_buf());

let cfg = write_config();
let _cfg = write_config();
write_model("users");
fs::create_dir_all(cfg.migrations_dir()).unwrap();

// No migrations yet -> plan will create table
let result = cmd_sql();
assert!(result.is_ok());
}

#[test]
fn emit_sql_no_actions_early_return() {
#[serial]
fn cmd_sql_no_changes() {
let tmp = tempdir().unwrap();
let _guard = CwdGuard::new(&tmp.path().to_path_buf());

let cfg = write_config();
write_model("users");

// Create initial migration to establish baseline
let plan = MigrationPlan {
comment: None,
created_at: None,
version: 1,
actions: vec![],
};
assert!(emit_sql(&plan).is_ok());
}

#[test]
fn emit_sql_with_metadata() {
let plan = MigrationPlan {
comment: Some("init".into()),
created_at: Some("2024-01-01T00:00:00Z".into()),
version: 1,
actions: vec![MigrationAction::CreateTable {
table: "users".into(),
columns: vec![ColumnDef {
Expand All @@ -181,6 +176,27 @@ mod tests {
}],
}],
};
assert!(emit_sql(&plan).is_ok());
fs::create_dir_all(cfg.migrations_dir()).unwrap();
let path = cfg.migrations_dir().join("0001_init.json");
fs::write(path, serde_json::to_string_pretty(&plan).unwrap()).unwrap();

let result = cmd_sql();
assert!(result.is_ok());
}

#[test]
#[serial]
fn emit_sql_prints_created_at_and_comment() {
let plan = MigrationPlan {
comment: Some("with comment".into()),
created_at: Some("2024-01-02T00:00:00Z".into()),
version: 1,
actions: vec![MigrationAction::RawSql {
sql: "SELECT 1;".into(),
}],
};

let result = emit_sql(&plan);
assert!(result.is_ok());
}
}
10 changes: 10 additions & 0 deletions crates/vespertide-cli/vespertide.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"modelsDir": "models",
"migrationsDir": "migrations",
"tableNamingCase": "snake",
"columnNamingCase": "snake",
"modelFormat": "json",
"migrationFormat": "json",
"migrationFilenamePattern": "%04v_%m",
"modelExportDir": "src/models"
}
2 changes: 1 addition & 1 deletion crates/vespertide-core/src/schema/constraint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ pub enum TableConstraint {
on_update: Option<ReferenceAction>,
},
Check {
name: Option<String>,
name: String,
expr: String,
},
}
7 changes: 4 additions & 3 deletions crates/vespertide-core/src/schema/table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,12 +89,13 @@ impl TableDef {
}
}

// Add primary key constraint if any columns have inline pk and no existing pk constraint
// Add primary key constraint if any columns have inline pk and no existing pk constraint.
if !pk_columns.is_empty() {
let has_pk = constraints
let has_pk_constraint = constraints
.iter()
.any(|c| matches!(c, TableConstraint::PrimaryKey { .. }));
if !has_pk {

if !has_pk_constraint {
constraints.push(TableConstraint::PrimaryKey {
auto_increment: pk_auto_increment,
columns: pk_columns,
Expand Down
44 changes: 23 additions & 21 deletions crates/vespertide-macro/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use proc_macro::TokenStream;
use quote::quote;
use syn::parse::{Parse, ParseStream};
use syn::{Expr, Ident, Token, parse_macro_input};
use vespertide_query::build_plan_queries;
use vespertide_query::{DatabaseBackend, build_plan_queries};

struct MacroInput {
pool: Expr,
Expand Down Expand Up @@ -85,16 +85,21 @@ pub fn vespertide_migration(input: TokenStream) -> TokenStream {
}
};

// Statically embed SQL text and bind parameters (as values)
// Pre-generate SQL for all backends at compile time
let sql_statements: Vec<_> = queries
.iter()
.map(|q| {
let sql = &q.sql;
let binds = &q.binds;
let value_tokens = binds.iter().map(|b| {
quote! { sea_orm::Value::String(Some(#b.to_string())) }
});
quote! { (#sql, vec![#(#value_tokens),*]) }
let pg_sql = q.build(DatabaseBackend::Postgres);
let mysql_sql = q.build(DatabaseBackend::MySql);
let sqlite_sql = q.build(DatabaseBackend::Sqlite);
quote! {
match backend {
sea_orm::DatabaseBackend::Postgres => #pg_sql,
sea_orm::DatabaseBackend::MySql => #mysql_sql,
sea_orm::DatabaseBackend::Sqlite => #sqlite_sql,
_ => #pg_sql, // Fallback to PostgreSQL syntax for unknown backends
}
}
})
.collect();

Expand All @@ -109,20 +114,18 @@ pub fn vespertide_migration(input: TokenStream) -> TokenStream {
// Execute SQL statements
#(
{
let (sql, values) = #sql_statements;
let stmt = sea_orm::Statement::from_sql_and_values(backend, sql, values);
let sql: &str = #sql_statements;
let stmt = sea_orm::Statement::from_string(backend, sql);
txn.execute_raw(stmt).await.map_err(|e| {
::vespertide::MigrationError::DatabaseError(format!("Failed to execute SQL: {}", e))
::vespertide::MigrationError::DatabaseError(format!("Failed to execute SQL '{}': {}", sql, e))
})?;
}
)*

// Insert version record for this migration
let stmt = sea_orm::Statement::from_sql_and_values(
backend,
&format!("INSERT INTO {} (version) VALUES (?)", version_table),
vec![sea_orm::Value::Int(Some(#version as i32))],
);
let q = if matches!(backend, sea_orm::DatabaseBackend::MySql) { '`' } else { '"' };
let insert_sql = format!("INSERT INTO {q}{}{q} (version) VALUES ({})", version_table, #version);
let stmt = sea_orm::Statement::from_string(backend, insert_sql);
txn.execute_raw(stmt).await.map_err(|e| {
::vespertide::MigrationError::DatabaseError(format!("Failed to insert version: {}", e))
})?;
Expand All @@ -147,8 +150,9 @@ pub fn vespertide_migration(input: TokenStream) -> TokenStream {

// Create version table if it does not exist
// Table structure: version (INTEGER PRIMARY KEY), created_at (timestamp)
let q = if matches!(backend, sea_orm::DatabaseBackend::MySql) { '`' } else { '"' };
let create_table_sql = format!(
"CREATE TABLE IF NOT EXISTS {} (version INTEGER PRIMARY KEY, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)",
"CREATE TABLE IF NOT EXISTS {q}{}{q} (version INTEGER PRIMARY KEY, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)",
version_table
);
let stmt = sea_orm::Statement::from_string(backend, create_table_sql);
Expand All @@ -157,10 +161,8 @@ pub fn vespertide_migration(input: TokenStream) -> TokenStream {
})?;

// Read current maximum version (latest applied migration)
let stmt = sea_orm::Statement::from_string(
backend,
format!("SELECT MAX(version) as version FROM {}", version_table),
);
let select_sql = format!("SELECT MAX(version) as version FROM {q}{}{q}", version_table);
let stmt = sea_orm::Statement::from_string(backend, select_sql);
let version_result = __pool.query_one_raw(stmt).await.map_err(|e| {
::vespertide::MigrationError::DatabaseError(format!("Failed to read version: {}", e))
})?;
Expand Down
Loading