Skip to content

Commit f87e9ac

Browse files
Enforce clean-slate v2 migration slate
Enforce clean-slate v2 migrations
1 parent 9df1fd0 commit f87e9ac

6 files changed

Lines changed: 39 additions & 446 deletions

src/migrations/2026_04_14_000157_create_workflow_schedules_table.php

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,6 @@
99
return new class() extends Migration {
1010
public function up(): void
1111
{
12-
if (Schema::hasTable('workflow_schedules')) {
13-
if (Schema::hasColumn('workflow_schedules', 'schedule_id')) {
14-
return;
15-
}
16-
17-
Schema::drop('workflow_schedules');
18-
}
19-
2012
Schema::create('workflow_schedules', static function (Blueprint $table): void {
2113
$table->string('id', 26)
2214
->primary();

src/migrations/2026_04_16_000158_repair_memo_on_workflow_run_summaries_table.php

Lines changed: 0 additions & 30 deletions
This file was deleted.

src/migrations/2026_04_16_000180_create_workflow_schedule_history_events_table.php

Lines changed: 0 additions & 210 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
use Illuminate\Database\Migrations\Migration;
66
use Illuminate\Database\Schema\Blueprint;
7-
use Illuminate\Support\Facades\DB;
87
use Illuminate\Support\Facades\Schema;
98

109
return new class() extends Migration {
@@ -28,26 +27,6 @@
2827

2928
public function up(): void
3029
{
31-
if (Schema::hasTable(self::TABLE)) {
32-
if (Schema::hasColumns(self::TABLE, [
33-
'id',
34-
'workflow_schedule_id',
35-
'schedule_id',
36-
'sequence',
37-
'event_type',
38-
'payload',
39-
'recorded_at',
40-
])) {
41-
$this->ensureExpectedIndexes();
42-
43-
return;
44-
}
45-
46-
throw new RuntimeException(
47-
self::TABLE . ' already exists but is missing expected schedule-history columns.'
48-
);
49-
}
50-
5130
Schema::create(self::TABLE, static function (Blueprint $table): void {
5231
$table->string('id', 26)
5332
->primary();
@@ -82,193 +61,4 @@ public function down(): void
8261
{
8362
Schema::dropIfExists(self::TABLE);
8463
}
85-
86-
private function ensureExpectedIndexes(): void
87-
{
88-
$this->ensureIndex(['workflow_schedule_id'], self::WORKFLOW_SCHEDULE_INDEX);
89-
$this->ensureIndex(['schedule_id'], self::SCHEDULE_INDEX);
90-
$this->ensureIndex(['namespace'], self::NAMESPACE_INDEX);
91-
$this->ensureIndex(['workflow_instance_id'], self::WORKFLOW_INSTANCE_INDEX);
92-
$this->ensureIndex(['workflow_run_id'], self::WORKFLOW_RUN_INDEX);
93-
$this->ensureIndex(['workflow_schedule_id', 'sequence'], self::SCHEDULE_SEQUENCE_UNIQUE, unique: true);
94-
$this->ensureIndex(['namespace', 'schedule_id'], self::NAMESPACE_SCHEDULE_INDEX);
95-
$this->ensureIndex(['event_type', 'recorded_at'], self::EVENT_RECORDED_INDEX);
96-
}
97-
98-
/**
99-
* @param array<int, string> $columns
100-
*/
101-
private function ensureIndex(array $columns, string $name, bool $unique = false): void
102-
{
103-
if ($this->hasExpectedIndex($columns, $unique)) {
104-
return;
105-
}
106-
107-
Schema::table(self::TABLE, static function (Blueprint $table) use ($columns, $name, $unique): void {
108-
if ($unique) {
109-
$table->unique($columns, $name);
110-
111-
return;
112-
}
113-
114-
$table->index($columns, $name);
115-
});
116-
}
117-
118-
/**
119-
* @param array<int, string> $columns
120-
*/
121-
private function hasExpectedIndex(array $columns, bool $unique): bool
122-
{
123-
$schema = Schema::getFacadeRoot();
124-
125-
if (is_object($schema) && method_exists($schema, 'hasIndex')) {
126-
return Schema::hasIndex(self::TABLE, $columns, $unique ? 'unique' : null);
127-
}
128-
129-
return match (DB::connection()->getDriverName()) {
130-
'mysql', 'mariadb' => $this->hasMysqlIndex($columns, $unique),
131-
'pgsql' => $this->hasPostgresIndex($columns, $unique),
132-
'sqlite' => $this->hasSqliteIndex($columns, $unique),
133-
'sqlsrv' => $this->hasSqlServerIndex($columns, $unique),
134-
default => false,
135-
};
136-
}
137-
138-
/**
139-
* @param array<int, string> $columns
140-
*/
141-
private function hasMysqlIndex(array $columns, bool $unique): bool
142-
{
143-
return $this->indexRowsMatch(
144-
DB::select(
145-
<<<'SQL'
146-
select index_name, (non_unique = 0) as is_unique, column_name
147-
from information_schema.statistics
148-
where table_schema = ? and table_name = ?
149-
order by index_name, seq_in_index
150-
SQL
151-
,
152-
[DB::connection()->getDatabaseName(), self::TABLE]
153-
),
154-
$columns,
155-
$unique
156-
);
157-
}
158-
159-
/**
160-
* @param array<int, string> $columns
161-
*/
162-
private function hasPostgresIndex(array $columns, bool $unique): bool
163-
{
164-
return $this->indexRowsMatch(
165-
DB::select(
166-
<<<'SQL'
167-
select i.relname as index_name, ix.indisunique as is_unique, a.attname as column_name
168-
from pg_class t
169-
join pg_index ix on t.oid = ix.indrelid
170-
join pg_class i on i.oid = ix.indexrelid
171-
join unnest(ix.indkey) with ordinality as ord(attnum, ordinality) on true
172-
join pg_attribute a on a.attrelid = t.oid and a.attnum = ord.attnum
173-
where t.relname = ?
174-
order by i.relname, ord.ordinality
175-
SQL
176-
,
177-
[self::TABLE]
178-
),
179-
$columns,
180-
$unique
181-
);
182-
}
183-
184-
/**
185-
* @param array<int, string> $columns
186-
*/
187-
private function hasSqliteIndex(array $columns, bool $unique): bool
188-
{
189-
$indexes = [];
190-
191-
foreach (DB::select('pragma index_list(' . $this->sqliteIdentifier(self::TABLE) . ')') as $index) {
192-
$indexName = (string) $index->name;
193-
194-
$indexes[] = [
195-
'unique' => $this->truthy($index->unique ?? false),
196-
'columns' => array_map(
197-
static fn (object $column): string => (string) $column->name,
198-
DB::select('pragma index_info(' . $this->sqliteIdentifier($indexName) . ')')
199-
),
200-
];
201-
}
202-
203-
return $this->indexesMatch($indexes, $columns, $unique);
204-
}
205-
206-
/**
207-
* @param array<int, string> $columns
208-
*/
209-
private function hasSqlServerIndex(array $columns, bool $unique): bool
210-
{
211-
return $this->indexRowsMatch(
212-
DB::select(
213-
<<<'SQL'
214-
select i.name as index_name, i.is_unique as is_unique, c.name as column_name
215-
from sys.indexes i
216-
join sys.index_columns ic on i.object_id = ic.object_id and i.index_id = ic.index_id
217-
join sys.columns c on ic.object_id = c.object_id and ic.column_id = c.column_id
218-
where i.object_id = object_id(?)
219-
order by i.name, ic.key_ordinal
220-
SQL
221-
,
222-
[self::TABLE]
223-
),
224-
$columns,
225-
$unique
226-
);
227-
}
228-
229-
/**
230-
* @param array<int, object> $rows
231-
* @param array<int, string> $columns
232-
*/
233-
private function indexRowsMatch(array $rows, array $columns, bool $unique): bool
234-
{
235-
$indexes = [];
236-
237-
foreach ($rows as $row) {
238-
$indexName = (string) $row->index_name;
239-
$indexes[$indexName]['unique'] ??= $this->truthy($row->is_unique ?? false);
240-
$indexes[$indexName]['columns'][] = (string) $row->column_name;
241-
}
242-
243-
return $this->indexesMatch(array_values($indexes), $columns, $unique);
244-
}
245-
246-
/**
247-
* @param array<int, array{unique: bool, columns: array<int, string>}> $indexes
248-
* @param array<int, string> $columns
249-
*/
250-
private function indexesMatch(array $indexes, array $columns, bool $unique): bool
251-
{
252-
foreach ($indexes as $index) {
253-
if ($index['columns'] === $columns && (! $unique || $index['unique'])) {
254-
return true;
255-
}
256-
}
257-
258-
return false;
259-
}
260-
261-
private function sqliteIdentifier(string $identifier): string
262-
{
263-
return '"' . str_replace('"', '""', $identifier) . '"';
264-
}
265-
266-
private function truthy(mixed $value): bool
267-
{
268-
return $value === true
269-
|| $value === 1
270-
|| $value === '1'
271-
|| $value === 't'
272-
|| $value === 'true';
273-
}
27464
};

tests/Feature/MigrationTest.php

Lines changed: 0 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
namespace Tests\Feature;
66

7-
use Illuminate\Database\Schema\Blueprint;
87
use Illuminate\Support\Facades\DB;
98
use Illuminate\Support\Facades\Schema;
109
use Tests\TestCase;
@@ -50,91 +49,6 @@ public function testItRunsV1MigrationsWithoutErrors()
5049
]));
5150
}
5251

53-
public function testScheduleHistoryMigrationCanResumeAfterPartialMysqlIndexFailure()
54-
{
55-
$path = __DIR__ . '/../../src/migrations/2026_04_16_000180_create_workflow_schedule_history_events_table.php';
56-
57-
Schema::create('workflow_schedule_history_events', static function (Blueprint $table): void {
58-
$table->string('id', 26)
59-
->primary();
60-
$table->string('workflow_schedule_id', 26);
61-
$table->string('schedule_id', 255);
62-
$table->string('namespace', 255)
63-
->nullable();
64-
$table->unsignedInteger('sequence');
65-
$table->string('event_type');
66-
$table->json('payload')
67-
->nullable();
68-
$table->string('workflow_instance_id', 191)
69-
->nullable();
70-
$table->string('workflow_run_id', 26)
71-
->nullable();
72-
$table->timestamp('recorded_at', 6)
73-
->nullable();
74-
$table->timestamps(6);
75-
});
76-
77-
$this->assertTrue(Schema::hasTable('workflow_schedule_history_events'));
78-
$this->assertFalse(
79-
DB::table('migrations')
80-
->where('migration', '2026_04_16_000180_create_workflow_schedule_history_events_table')
81-
->exists()
82-
);
83-
84-
$migration = include $path;
85-
$migration->up();
86-
87-
$this->assertTrue(Schema::hasColumns('workflow_schedule_history_events', [
88-
'id',
89-
'workflow_schedule_id',
90-
'schedule_id',
91-
'namespace',
92-
'sequence',
93-
'event_type',
94-
'payload',
95-
'workflow_instance_id',
96-
'workflow_run_id',
97-
'recorded_at',
98-
]));
99-
$this->assertTrue(Schema::hasIndex('workflow_schedule_history_events', ['workflow_schedule_id']));
100-
$this->assertTrue(Schema::hasIndex('workflow_schedule_history_events', ['schedule_id']));
101-
$this->assertTrue(Schema::hasIndex('workflow_schedule_history_events', ['namespace']));
102-
$this->assertTrue(Schema::hasIndex('workflow_schedule_history_events', ['workflow_instance_id']));
103-
$this->assertTrue(Schema::hasIndex('workflow_schedule_history_events', ['workflow_run_id']));
104-
$this->assertTrue(Schema::hasIndex(
105-
'workflow_schedule_history_events',
106-
['workflow_schedule_id', 'sequence'],
107-
'unique'
108-
));
109-
$this->assertTrue(Schema::hasIndex('workflow_schedule_history_events', ['namespace', 'schedule_id']));
110-
$this->assertTrue(Schema::hasIndex('workflow_schedule_history_events', ['event_type', 'recorded_at']));
111-
}
112-
113-
public function testRunSummaryMemoRepairMigrationAddsMissingMemoColumn(): void
114-
{
115-
$path = __DIR__ . '/../../src/migrations/2026_04_16_000158_repair_memo_on_workflow_run_summaries_table.php';
116-
117-
Schema::create('workflow_run_summaries', static function (Blueprint $table): void {
118-
$table->string('id', 26)
119-
->primary();
120-
$table->json('visibility_labels')
121-
->nullable();
122-
$table->timestamps(6);
123-
});
124-
125-
$this->assertTrue(Schema::hasTable('workflow_run_summaries'));
126-
$this->assertFalse(Schema::hasColumn('workflow_run_summaries', 'memo'));
127-
128-
$migration = include $path;
129-
$migration->up();
130-
131-
$this->assertTrue(Schema::hasColumn('workflow_run_summaries', 'memo'));
132-
133-
$migration->up();
134-
135-
$this->assertTrue(Schema::hasColumn('workflow_run_summaries', 'memo'));
136-
}
137-
13852
public function testItPreservesV1WorkflowDataAfterV2Migration()
13953
{
14054
// Set up v1 schema and data

0 commit comments

Comments
 (0)