Skip to content

Commit 65b85d7

Browse files
committed
wip
1 parent e0608b6 commit 65b85d7

File tree

1 file changed

+92
-0
lines changed

1 file changed

+92
-0
lines changed
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
---
2+
title: No more down migrations
3+
description: Database migrations have had a serious refactor in the newest Tempest release
4+
author: brent
5+
tag: thoughts
6+
---
7+
8+
With Tempest 2 comes a pretty significant change to how database migrations work. Luckily, the [upgrade process is automated](/blog/tempest-2). I thought it would be interesting to explain _why_ we made this change, though.
9+
10+
Previously, the `DatabaseMigration` interface looked like this:
11+
12+
```php
13+
interface DatabaseMigration
14+
{
15+
public string $name { get; }
16+
17+
public function up(): ?QueryStatement;
18+
19+
public function down(): ?QueryStatement;
20+
}
21+
```
22+
23+
Each migration had to implement both an `up()` and `down()` method. If your migration didn't need `up()` or `down()` functionality, you'd have to return `null`. This design was originally inspired by Laravel, and was one of the very early parts of Tempest that had never really changed. However, Freek recently wrote [a good blog post](https://freek.dev/2900-why-i-dont-use-down-migrations) on why he doesn't write down migrations anymore:
24+
25+
> At Spatie, we've embraced forward-only migrations for many years now.
26+
>
27+
> When something needs to be reversed, we will first think carefully about the appropriate solution for the particular situation we’re in. If necessary, we’ll handcraft a new migration that moves us forward rather than trying to reverse history.
28+
29+
Freek makes the point that "trying to reverse history with down migrations" is pretty tricky, especially if the migrations you're trying to roll back are already in production. I have to agree with him: up-migrations can already be tricky; trying to have consistent down-migrations as well is a whole new level of tricky-ness.
30+
31+
After reading Freek's blog post, I remembered: Tempest is a clean slate. Nothing is stopping us from using a different approach. That's why we removed the `DatabaseMigration` interface in Tempest 2. Instead there are now both the {b`Tempest\Database\MigratesUp`} and {b`Tempest\Database\MigratesDown`} interfaces. Yes, we kept the `MigratesDown` interface for now, and I'll elaborate a bit more on why later. First, let me show you what migrations now look like:
32+
33+
```php
34+
use Tempest\Database\MigratesUp;
35+
use Tempest\Database\QueryStatement;
36+
use Tempest\Database\QueryStatements\CreateTableStatement;
37+
38+
final class CreateStoredEventTable implements MigratesUp
39+
{
40+
public string $name = '2025-01-01-create_stored_events_table';
41+
42+
public function up(): QueryStatement
43+
{
44+
return CreateTableStatement::forModel(StoredEvent::class)
45+
->primary()
46+
->text('uuid')
47+
->text('eventClass')
48+
->text('payload')
49+
->datetime('createdAt');
50+
}
51+
}
52+
```
53+
54+
This is our recommended way of writing migrations: to only implement the {b`Tempest\Database\MigratesUp`} interface. Thanks to this refactor, we don't have to worry about nullable return statements on the interfaces as well, which I'd say is a nice bonus. Of course, you can still implement both interfaces in the same class if you really want to:
55+
56+
```php
57+
use Tempest\Database\MigratesUp;
58+
use Tempest\Database\MigratesDown;
59+
use Tempest\Database\QueryStatement;
60+
use Tempest\Database\QueryStatements\CreateTableStatement;
61+
use Tempest\Database\QueryStatements\DropTableStatement;
62+
63+
final class CreateStoredEventTable implements MigratesUp, MigratedDown
64+
{
65+
public string $name = '2025-01-01-stored_events_table';
66+
67+
public function up(): QueryStatement
68+
{
69+
return new CreateTableStatement('stored_events')
70+
->primary()
71+
->text('uuid')
72+
->text('eventClass')
73+
->text('payload')
74+
->datetime('createdAt');
75+
}
76+
77+
public function down(): QueryStatement
78+
{
79+
return new DropTableStatement('stored_events');
80+
}
81+
}
82+
```
83+
84+
So why did we keep the `MigratesDown` interface? Some developers told me they like to use down migrations during development where they partially roll back the database while working on a feature. Personally, I prefer to always start from a fresh database and use [database seeders](/2.x/essentials/database#multiple-seeders) to bring it to a specific state. This way you'll always end up with the same database across developer machines, and can develop in a much more consistent way. You could, for example, make a seeder per feature you're working on, and so rollback the database to the right state during testing much more consistently:
85+
86+
```
87+
./tempest migrate:fresh --seeder="Tests\Tempest\Fixtures\MailingSeeder"
88+
{:hl-comment:# Or:}
89+
./tempest migrate:fresh --seeder="Tests\Tempest\Fixtures\InvoiceSeeder"
90+
```
91+
92+
Either way, we decided to keep `MigrateDown` in for now, and see the community's reaction to this new approach. We might get rid of down migrations altogether in the future, or we might keep them. Our recommended approach won't change, though: don't try to reverse the past, focus on moving forward.

0 commit comments

Comments
 (0)