Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
78 commits
Select commit Hold shift + click to select a range
c6cf333
Prototype diff functions
pikaju Jan 8, 2026
c90ece8
Add diff context
pikaju Jan 8, 2026
d372890
Add rename hints
pikaju Jan 9, 2026
c4c8576
Add rename aware table diff algorithm
pikaju Jan 9, 2026
d37b3bb
Rewrite other diff algorithms
pikaju Jan 12, 2026
5b9b81a
Move has_diff function
pikaju Jan 12, 2026
1ecef89
Fix indics diff algorithm
pikaju Jan 12, 2026
a60937b
Add column diff tests
pikaju Jan 12, 2026
d3b607f
Add index tests
pikaju Jan 12, 2026
f5598e9
Add tables diff tests
pikaju Jan 12, 2026
95f92a6
Add deref impls
pikaju Jan 12, 2026
759cd26
Start Toasty CLI
pikaju Jan 12, 2026
7e1e933
Add CLI example project
pikaju Jan 12, 2026
264ede5
Add CLI config
pikaju Jan 12, 2026
2030fd5
Add serde for toasty-core
pikaju Jan 12, 2026
0e64195
Create migration specific config
pikaju Jan 12, 2026
7cad32c
Add lock file concept
pikaju Jan 12, 2026
36e3c7c
Add lock file string functions
pikaju Jan 12, 2026
58b8e93
Add lock command
pikaju Jan 12, 2026
1cbfe69
Try toml edit
pikaju Jan 14, 2026
596c4c9
Add primary key
pikaju Jan 14, 2026
e2e8795
Add indices
pikaju Jan 14, 2026
f9d1189
Try post processing approach
pikaju Jan 14, 2026
f0f34e2
Switch to post processing approach
pikaju Jan 14, 2026
9685098
Remove mut
pikaju Jan 14, 2026
52d5ecf
Rename lock file to snapshot file
pikaju Jan 14, 2026
ea4e185
Start generate command
pikaju Jan 20, 2026
96034e2
Add drop command
pikaju Jan 20, 2026
bf7e633
Split up command files
pikaju Jan 20, 2026
2d3325e
Improve design
pikaju Jan 20, 2026
54fa5f7
Fix migration numbering bug
pikaju Jan 20, 2026
daef592
Check for empty diffs
pikaju Jan 20, 2026
e3b4c20
Further improve CLI design
pikaju Jan 20, 2026
635c666
Improve design again
pikaju Jan 20, 2026
2733fcd
Fix example history
pikaju Jan 20, 2026
ff74177
Derive next migration number from previous migration instead of keepi…
pikaju Jan 20, 2026
bfeba3d
Add Toasty.toml loading
pikaju Jan 20, 2026
b97f0dc
Add statement breakpoints
pikaju Jan 20, 2026
c4c6a68
Start SQL
pikaju Jan 21, 2026
9471107
Merge branch 'main' into diff
pikaju Jan 21, 2026
f05c0bf
Table name
pikaju Jan 23, 2026
052f833
Sigh
pikaju Jan 23, 2026
8ee8155
First migration has landed
pikaju Jan 23, 2026
3670b8a
Format tables with newlines
pikaju Jan 23, 2026
44218f7
Split up migration statements into separate strings
pikaju Jan 23, 2026
da0b718
Add basic rename hint evaluation function
pikaju Jan 23, 2026
c178a18
Extract out theme
pikaju Jan 25, 2026
f968869
Fix warning
pikaju Jan 25, 2026
0965717
Add migration IDs
pikaju Jan 25, 2026
883c9a6
Add migration apply command
pikaju Jan 25, 2026
dac945f
Add test database
pikaju Jan 27, 2026
ac83274
Fix migration ID issues
pikaju Jan 27, 2026
43a190b
Add column migrations
pikaju Jan 27, 2026
b84be67
Alter column prototype
pikaju Jan 31, 2026
82844a2
Run format
pikaju Jan 31, 2026
7452f67
Fix compile errors, tests still passing
pikaju Jan 31, 2026
4f9a890
Merge branch 'main' into diff
pikaju Jan 31, 2026
c27f866
Use new error type in migration code
pikaju Jan 31, 2026
fcd1921
Add schema mutation capabilities concept
pikaju Jan 31, 2026
02bcc29
Add alter column serialization
pikaju Jan 31, 2026
0e8a810
Add alter column constructor
pikaju Jan 31, 2026
1a837c0
Add support for altering the auto increment state
pikaju Jan 31, 2026
bd173c9
Rename from and to to previous and next
pikaju Feb 8, 2026
ad6a1d4
Support alter column statements for postgres and mysql
pikaju Feb 8, 2026
052de7c
Fix build errors
pikaju Feb 8, 2026
496d5a1
Add sqlite pragma
pikaju Feb 8, 2026
229e0f5
Add table renaming
pikaju Feb 8, 2026
8c7a1ff
WIP
pikaju Feb 8, 2026
e6cdbed
Add create table tests
pikaju Feb 8, 2026
097a534
Add drop table tests
pikaju Feb 8, 2026
d7a799f
Add more tests
pikaju Feb 8, 2026
011f65f
Add alter table tests
pikaju Feb 8, 2026
8a98523
Add alter column tests
pikaju Feb 8, 2026
5ea6abb
Fix alter column formatting bug so Postgres and MySQL tests pass
pikaju Feb 8, 2026
86acabc
First working sqlite prototype
pikaju Feb 8, 2026
51a809f
Run cargo fmt
pikaju Feb 8, 2026
6d9576a
Refactor file
pikaju Feb 8, 2026
03a79b8
Run cargo fmt
pikaju Feb 8, 2026
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
7 changes: 7 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
resolver = "2"
members = [
"crates/toasty",
"crates/toasty-cli",
"crates/toasty-codegen",
"crates/toasty-core",
"crates/toasty-macros",
Expand All @@ -23,6 +24,7 @@ members = [
# Examples
"examples/composite-key",
"examples/hello-toasty",
"examples/todo-with-cli",
"examples/user-has-one-profile",

# Tests
Expand All @@ -35,6 +37,7 @@ members = [
[workspace.dependencies]
# Toasty crates
toasty = { path = "crates/toasty" }
toasty-cli = { path = "crates/toasty-cli" }
toasty-codegen = { path = "crates/toasty-codegen" }
toasty-core = { path = "crates/toasty-core" }
toasty-macros = { path = "crates/toasty-macros" }
Expand Down Expand Up @@ -62,7 +65,9 @@ bit-set = "0.8.0"
by_address = "1.2.1"
cfg-if = "1.0.0"
clap = { version = "4.5.20", features = ["derive"] }
console = "0.16"
deadpool = { version = "0.12.3", features = ["rt_tokio_1"] }
dialoguer = "0.12"
heck = "0.5.0"
indexmap = "2.6.0"
index_vec = "0.1.4"
Expand All @@ -85,6 +90,8 @@ rusqlite = { version = "0.32", features = ["bundled"] }
serde = { version = "1.0.214", features = ["derive"] }
serde_json = "1.0.132"
syn = { version = "2.0.86", features = ["full", "extra-traits", "visit-mut"] }
toml = "0.9.11"
toml_edit = "0.24.0"
tempfile = "3.8"
tokio = { version = "1.18", features = ["full"] }
tokio-postgres = "0.7.13"
Expand Down
19 changes: 19 additions & 0 deletions crates/toasty-cli/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[package]
name = "toasty-cli"
version = "0.1.0"
edition = "2024"
publish = false

[dependencies]
toasty.workspace = true
toasty-core = { workspace = true, features = ["serde"] }
toasty-sql.workspace = true

anyhow = "1.0.100"
clap.workspace = true
console.workspace = true
dialoguer.workspace = true
rand.workspace = true
serde.workspace = true
toml.workspace = true
toml_edit = { workspace = true, features = ["serde"] }
33 changes: 33 additions & 0 deletions crates/toasty-cli/src/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
use crate::migration::MigrationConfig;
use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::Path;

/// Configuration for Toasty CLI operations
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct Config {
/// Migration-related configuration
pub migration: MigrationConfig,
}

impl Config {
/// Create a new Config with default values
pub fn new() -> Self {
Self::default()
}

/// Load configuration from Toasty.toml in the project root
pub fn load() -> Result<Self> {
let path = Path::new("Toasty.toml");
let contents = fs::read_to_string(path)?;
let config: Config = toml::from_str(&contents)?;
Ok(config)
}

/// Set the migration configuration
pub fn migration(mut self, migration: MigrationConfig) -> Self {
self.migration = migration;
self
}
}
73 changes: 73 additions & 0 deletions crates/toasty-cli/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
mod config;
mod migration;
mod theme;

pub use config::*;
pub use migration::*;

use anyhow::Result;
use clap::Parser;
use toasty::Db;

/// Toasty CLI library for building custom command-line tools
pub struct ToastyCli {
db: Db,
config: Config,
}

impl ToastyCli {
/// Create a new ToastyCli instance with the given database connection
pub fn new(db: Db) -> Self {
Self {
db,
config: Config::default(),
}
}

/// Create a new ToastyCli instance with a custom configuration
pub fn with_config(db: Db, config: Config) -> Self {
Self { db, config }
}

/// Get a reference to the configuration
pub fn config(&self) -> &Config {
&self.config
}

/// Parse and execute CLI commands from command-line arguments
pub async fn parse_and_run(&self) -> Result<()> {
let cli = Cli::parse();
self.run(cli).await
}

/// Parse and execute CLI commands from an iterator of arguments
pub async fn parse_from<I, T>(&self, args: I) -> Result<()>
where
I: IntoIterator<Item = T>,
T: Into<std::ffi::OsString> + Clone,
{
let cli = Cli::parse_from(args);
self.run(cli).await
}

async fn run(&self, cli: Cli) -> Result<()> {
match cli.command {
Command::Migration(cmd) => cmd.run(&self.db, &self.config).await,
}
}
}

#[derive(Parser, Debug)]
#[command(name = "toasty")]
#[command(about = "Toasty CLI - Database migration and management tool")]
#[command(version)]
struct Cli {
#[command(subcommand)]
command: Command,
}

#[derive(Parser, Debug)]
enum Command {
/// Database migration commands
Migration(migration::MigrationCommand),
}
58 changes: 58 additions & 0 deletions crates/toasty-cli/src/migration.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
mod apply;
mod config;
mod drop;
mod generate;
mod history_file;
mod snapshot;
mod snapshot_file;

pub use apply::*;
pub use config::*;
pub use drop::*;
pub use generate::*;
pub use history_file::*;
pub use snapshot::*;
pub use snapshot_file::*;

use crate::Config;
use anyhow::Result;
use clap::Parser;
use toasty::Db;

#[derive(Parser, Debug)]
pub struct MigrationCommand {
#[command(subcommand)]
subcommand: MigrationSubcommand,
}

#[derive(Parser, Debug)]
enum MigrationSubcommand {
/// Apply pending migrations to the database
Apply(ApplyCommand),

/// Generate a new migration based on schema changes
Generate(GenerateCommand),

/// Print the current schema snapshot file
Snapshot(SnapshotCommand),

/// Drop a migration from the history
Drop(DropCommand),
}

impl MigrationCommand {
pub(crate) async fn run(self, db: &Db, config: &Config) -> Result<()> {
self.subcommand.run(db, config).await
}
}

impl MigrationSubcommand {
async fn run(self, db: &Db, config: &Config) -> Result<()> {
match self {
Self::Apply(cmd) => cmd.run(db, config).await,
Self::Generate(cmd) => cmd.run(db, config),
Self::Snapshot(cmd) => cmd.run(db, config),
Self::Drop(cmd) => cmd.run(db, config),
}
}
}
111 changes: 111 additions & 0 deletions crates/toasty-cli/src/migration/apply.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
use super::HistoryFile;
use crate::Config;
use anyhow::Result;
use clap::Parser;
use console::style;
use std::collections::HashSet;
use std::fs;
use toasty::Db;
use toasty::schema::db::Migration;

#[derive(Parser, Debug)]
pub struct ApplyCommand {}

impl ApplyCommand {
pub(crate) async fn run(self, db: &Db, config: &Config) -> Result<()> {
println!();
println!(" {}", style("Apply Migrations").cyan().bold().underlined());
println!();

let history_path = config.migration.get_history_file_path();

// Load migration history
let history = HistoryFile::load_or_default(&history_path)?;

if history.migrations().is_empty() {
println!(
" {}",
style("No migrations found in history file.")
.magenta()
.dim()
);
println!();
return Ok(());
}

// Get a connection to check which migrations have been applied
let mut conn = db.driver().connect().await?;

// Get list of already applied migrations
let applied_migrations = conn.applied_migrations().await?;
let applied_ids: HashSet<u64> = applied_migrations.iter().map(|m| m.id()).collect();

// Find migrations that haven't been applied yet
let pending_migrations: Vec<_> = history
.migrations()
.iter()
.filter(|m| !applied_ids.contains(&m.id))
.collect();

if pending_migrations.is_empty() {
println!(
" {}",
style("All migrations are already applied. Database is up to date.")
.green()
.dim()
);
println!();
return Ok(());
}

let pending_count = pending_migrations.len();
println!(
" {} Found {} pending migration(s) to apply",
style("→").cyan(),
pending_count
);
println!();

// Apply each pending migration
for migration_entry in &pending_migrations {
let migration_path = config
.migration
.get_migrations_dir()
.join(&migration_entry.name);

println!(
" {} Applying migration: {}",
style("→").cyan(),
style(&migration_entry.name).bold()
);

// Load the migration SQL file
let sql = fs::read_to_string(&migration_path)?;
let migration = Migration::new_sql(sql);

// Apply the migration
conn.apply_migration(migration_entry.id, migration_entry.name.clone(), &migration)
.await?;

println!(
" {} {}",
style("✓").green().bold(),
style(format!("Applied: {}", migration_entry.name)).dim()
);
}

println!();
println!(
" {}",
style(format!(
"Successfully applied {} migration(s)",
pending_count
))
.green()
.bold()
);
println!();

Ok(())
}
}
Loading
Loading