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
3 changes: 2 additions & 1 deletion crates/azoth-core/src/types/meta.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ impl CanonicalMeta {
Self {
next_event_id: 0,
sealed_event_id: None,
schema_version: 1,
// schema_version starts at 0 so that migrations starting from version 1 will be applied
schema_version: 0,
created_at: now.clone(),
updated_at: now,
}
Expand Down
3 changes: 2 additions & 1 deletion crates/azoth-lmdb/src/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -177,11 +177,12 @@ impl CanonicalStore for LmdbCanonicalStore {
}

// Initialize schema_version if not present
// schema_version starts at 0 so that migrations starting from version 1 will be applied
if txn.get(meta_db, &meta_keys::SCHEMA_VERSION).is_err() {
txn.put(
meta_db,
&meta_keys::SCHEMA_VERSION,
&"1",
&"0",
WriteFlags::empty(),
)
.map_err(|e| AzothError::Transaction(e.to_string()))?;
Expand Down
3 changes: 2 additions & 1 deletion crates/azoth-sqlite/src/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,10 @@ impl SqliteProjectionStore {
.map_err(|e| AzothError::Projection(e.to_string()))?;

// Insert default row if not exists (-1 means no events processed yet)
// schema_version starts at 0 so that migrations starting from version 1 will be applied
conn.execute(
"INSERT OR IGNORE INTO projection_meta (id, last_applied_event_id, schema_version)
VALUES (0, -1, 1)",
VALUES (0, -1, 0)",
[],
)
.map_err(|e| AzothError::Projection(e.to_string()))?;
Expand Down
16 changes: 8 additions & 8 deletions crates/azoth/src/migration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
//!
//! impl Migration for CreateAccountsTable {
//! fn version(&self) -> u32 {
//! 2
//! 1 // First migration starts at version 1
//! }
//!
//! fn name(&self) -> &str {
Expand Down Expand Up @@ -64,7 +64,7 @@ use std::sync::Arc;
pub trait Migration: Send + Sync {
/// The version number this migration targets
///
/// Versions should be sequential starting from 2 (version 1 is the base schema).
/// Versions should be sequential starting from 1.
fn version(&self) -> u32;

/// Human-readable name for this migration
Expand Down Expand Up @@ -256,9 +256,9 @@ impl MigrationManager {
pub fn rollback_last(&self, projection: &Arc<crate::SqliteProjectionStore>) -> Result<()> {
let current_version = projection.schema_version()?;

if current_version <= 1 {
if current_version == 0 {
return Err(AzothError::InvalidState(
"Cannot rollback base schema".into(),
"Cannot rollback: no migrations have been applied".into(),
));
}

Expand Down Expand Up @@ -364,13 +364,13 @@ impl MigrationManager {
fs::create_dir_all(migrations_dir)
.map_err(|e| AzothError::Projection(format!("Failed to create directory: {}", e)))?;

// Find next version number
// Find next version number (starts at 1 if no migrations exist)
let next_version = self
.migrations
.iter()
.map(|m| m.version())
.max()
.unwrap_or(1)
.unwrap_or(0)
+ 1;

// Create filename
Expand Down Expand Up @@ -582,7 +582,7 @@ mod tests {

impl Migration for TestMigration {
fn version(&self) -> u32 {
2
1 // First migration starts at version 1
}

fn name(&self) -> &str {
Expand All @@ -601,7 +601,7 @@ mod tests {

let migrations = manager.list();
assert_eq!(migrations.len(), 1);
assert_eq!(migrations[0].version, 2);
assert_eq!(migrations[0].version, 1);
assert_eq!(migrations[0].name, "test_migration");
}
}
14 changes: 7 additions & 7 deletions crates/azoth/tests/integration_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -339,14 +339,14 @@ fn test_event_id_monotonicity() {
fn test_schema_version() {
let (db, _temp) = create_test_db();

// Initial version should be 1
assert_eq!(db.projection().schema_version().unwrap(), 1);
// Initial version should be 0 (no migrations applied)
assert_eq!(db.projection().schema_version().unwrap(), 0);

// Migrate to version 2
db.projection().migrate(2).unwrap();
assert_eq!(db.projection().schema_version().unwrap(), 2);
// Migrate to version 1
db.projection().migrate(1).unwrap();
assert_eq!(db.projection().schema_version().unwrap(), 1);

// Migrating to same version is a no-op
db.projection().migrate(2).unwrap();
assert_eq!(db.projection().schema_version().unwrap(), 2);
db.projection().migrate(1).unwrap();
assert_eq!(db.projection().schema_version().unwrap(), 1);
}
28 changes: 14 additions & 14 deletions crates/azoth/tests/migration_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ struct CreateUsersTable;

impl Migration for CreateUsersTable {
fn version(&self) -> u32 {
2
1 // First migration starts at version 1
}

fn name(&self) -> &str {
Expand Down Expand Up @@ -39,7 +39,7 @@ struct AddEmailToUsers;

impl Migration for AddEmailToUsers {
fn version(&self) -> u32 {
3
2 // Second migration is version 2
}

fn name(&self) -> &str {
Expand All @@ -64,9 +64,9 @@ fn test_migration_manager() {
// Should list migrations
let migrations = manager.list();
assert_eq!(migrations.len(), 2);
assert_eq!(migrations[0].version, 2);
assert_eq!(migrations[0].version, 1);
assert_eq!(migrations[0].name, "create_users_table");
assert_eq!(migrations[1].version, 3);
assert_eq!(migrations[1].version, 2);
assert_eq!(migrations[1].name, "add_email_to_users");
}

Expand All @@ -80,8 +80,8 @@ fn test_migration_ordering() {

// Should be sorted by version
let migrations = manager.list();
assert_eq!(migrations[0].version, 2);
assert_eq!(migrations[1].version, 3);
assert_eq!(migrations[0].version, 1);
assert_eq!(migrations[1].version, 2);
}

#[test]
Expand All @@ -93,20 +93,20 @@ fn test_pending_migrations() {
manager.add(Box::new(CreateUsersTable));
manager.add(Box::new(AddEmailToUsers));

// All migrations should be pending
// All migrations should be pending (schema starts at 0)
let pending = manager.pending(db.projection()).unwrap();
assert_eq!(pending.len(), 2);
assert_eq!(pending[0].version, 2);
assert_eq!(pending[1].version, 3);
assert_eq!(pending[0].version, 1);
assert_eq!(pending[1].version, 2);

// After migrating to v2, only v3 should be pending
db.projection().migrate(2).unwrap();
// After migrating to v1, only v2 should be pending
db.projection().migrate(1).unwrap();
let pending = manager.pending(db.projection()).unwrap();
assert_eq!(pending.len(), 1);
assert_eq!(pending[0].version, 3);
assert_eq!(pending[0].version, 2);

// After migrating to v3, none should be pending
db.projection().migrate(3).unwrap();
// After migrating to v2, none should be pending
db.projection().migrate(2).unwrap();
let pending = manager.pending(db.projection()).unwrap();
assert_eq!(pending.len(), 0);
}
Expand Down
33 changes: 16 additions & 17 deletions docs/MIGRATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,17 +51,16 @@ Migration files should follow this naming convention:

For example:
```
0002_create_users_table.sql
0002_create_users_table.down.sql
0003_add_user_roles.sql
0003_add_user_roles.down.sql
0001_create_users_table.sql
0001_create_users_table.down.sql
0002_add_user_roles.sql
0002_add_user_roles.down.sql
```

### Version Numbers

- **Version 1**: Reserved for the base schema (created automatically)
- **Version 2+**: Your migrations, numbered sequentially
- Use 4-digit padding (e.g., `0002`, `0003`) for proper sorting
- **Version 1+**: Your migrations, numbered sequentially starting from 1
- Use 4-digit padding (e.g., `0001`, `0002`) for proper sorting

## Using File-Based Migrations

Expand All @@ -81,16 +80,16 @@ use azoth::prelude::*;
let mut manager = MigrationManager::new();
let path = manager.generate("./migrations", "create_users_table")?;
// Creates:
// ./migrations/0002_create_users_table.sql
// ./migrations/0002_create_users_table.down.sql
// ./migrations/0001_create_users_table.sql
// ./migrations/0001_create_users_table.down.sql
```

### 2. Edit the Generated Files

Add your SQL to the `.sql` file:

```sql
-- migrations/0002_create_users_table.sql
-- migrations/0001_create_users_table.sql
CREATE TABLE users (
id INTEGER PRIMARY KEY,
username TEXT NOT NULL UNIQUE,
Expand All @@ -104,7 +103,7 @@ CREATE INDEX idx_users_email ON users(email);
Optionally add rollback SQL to the `.down.sql` file:

```sql
-- migrations/0002_create_users_table.down.sql
-- migrations/0001_create_users_table.down.sql
DROP INDEX IF EXISTS idx_users_email;
DROP TABLE IF EXISTS users;
```
Expand Down Expand Up @@ -147,7 +146,7 @@ use azoth::prelude::*;

migration!(
CreateUsersTable,
version: 2,
version: 1,
name: "create_users_table",
up: |conn| {
conn.execute(
Expand Down Expand Up @@ -183,7 +182,7 @@ struct CreateUsersTable;

impl Migration for CreateUsersTable {
fn version(&self) -> u32 {
2
1 // First migration starts at version 1
}

fn name(&self) -> &str {
Expand Down Expand Up @@ -316,7 +315,7 @@ fn main() -> Result<()> {
// Option 2: Add code-based migrations
migration!(
AddUserRoles,
version: 3,
version: 2,
name: "add_user_roles",
up: |conn| {
conn.execute(
Expand Down Expand Up @@ -362,9 +361,9 @@ fn main() -> Result<()> {
- Verify that referenced tables/columns exist
- Review the error message for specific details

### "Cannot rollback base schema"
- Version 1 is the base schema and cannot be rolled back
- Only versions 2+ can be rolled back
### "Cannot rollback below version 0"
- Schema version 0 is the initial state before any migrations
- Only versions 1+ can be rolled back

## Advanced Features

Expand Down