From 635a0ec28a2bb381ecb28267a8302d6647e4f5de Mon Sep 17 00:00:00 2001 From: Stefan Zwischenbrugger Date: Sun, 29 Mar 2026 21:06:53 +0200 Subject: [PATCH] Migration shopping_items: idempotent (Duplikat-Spalten, MySQL-Check, Repair) Made-with: Cursor --- ...grate_shopping_items_to_shopping_lists.php | 159 +++++++++++++++--- ...air_shopping_items_created_by_nullable.php | 50 ++++++ 2 files changed, 183 insertions(+), 26 deletions(-) create mode 100644 database/migrations/2026_03_30_100000_repair_shopping_items_created_by_nullable.php diff --git a/database/migrations/2026_03_29_100100_migrate_shopping_items_to_shopping_lists.php b/database/migrations/2026_03_29_100100_migrate_shopping_items_to_shopping_lists.php index 38e29a2..b4526bd 100644 --- a/database/migrations/2026_03_29_100100_migrate_shopping_items_to_shopping_lists.php +++ b/database/migrations/2026_03_29_100100_migrate_shopping_items_to_shopping_lists.php @@ -1,6 +1,7 @@ unsignedBigInteger('shopping_list_id')->nullable(); - $table->unsignedBigInteger('created_by')->nullable(); - }); + if (! Schema::hasTable('shopping_items')) { + return; + } + + $this->addColumnUnlessExists('shopping_list_id'); + $this->addColumnUnlessExists('created_by'); $userIds = DB::table('users')->pluck('id'); $map = []; foreach ($userIds as $userId) { + $pivot = DB::table('shopping_list_user') + ->where('user_id', $userId) + ->orderBy('shopping_list_id') + ->first(); + + if ($pivot !== null) { + $map[$userId] = $pivot->shopping_list_id; + + continue; + } + + $owned = DB::table('shopping_lists')->where('owner_id', $userId)->orderBy('id')->first(); + if ($owned !== null) { + $map[$userId] = $owned->id; + if (! DB::table('shopping_list_user')->where('shopping_list_id', $owned->id)->where('user_id', $userId)->exists()) { + DB::table('shopping_list_user')->insert([ + 'shopping_list_id' => $owned->id, + 'user_id' => $userId, + 'created_at' => now(), + 'updated_at' => now(), + ]); + } + + continue; + } + $map[$userId] = DB::table('shopping_lists')->insertGetId([ 'owner_id' => $userId, 'name' => 'Einkaufsliste', @@ -32,33 +61,111 @@ return new class extends Migration ]); } - $items = DB::table('shopping_items')->select('id', 'user_id')->get(); - foreach ($items as $item) { - if (! isset($map[$item->user_id])) { - continue; + if (Schema::hasColumn('shopping_items', 'user_id')) { + $items = DB::table('shopping_items')->select('id', 'user_id')->get(); + foreach ($items as $item) { + if (! isset($map[$item->user_id])) { + continue; + } + DB::table('shopping_items')->where('id', $item->id)->update([ + 'shopping_list_id' => $map[$item->user_id], + 'created_by' => $item->user_id, + ]); } - DB::table('shopping_items')->where('id', $item->id)->update([ - 'shopping_list_id' => $map[$item->user_id], - 'created_by' => $item->user_id, - ]); + + Schema::table('shopping_items', function (Blueprint $table) { + $table->dropForeign(['user_id']); + $table->dropColumn('user_id'); + }); } - Schema::table('shopping_items', function (Blueprint $table) { - $table->dropForeign(['user_id']); - $table->dropColumn('user_id'); - }); + if (Schema::getConnection()->getDriverName() === 'mysql') { + $col = DB::select("SHOW COLUMNS FROM shopping_items WHERE Field = 'shopping_list_id'"); + if (! empty($col) && ($col[0]->Null ?? '') === 'YES') { + Schema::table('shopping_items', function (Blueprint $table) { + $table->unsignedBigInteger('shopping_list_id')->nullable(false)->change(); + }); + } + } else { + Schema::table('shopping_items', function (Blueprint $table) { + $table->unsignedBigInteger('shopping_list_id')->nullable(false)->change(); + }); + } - Schema::table('shopping_items', function (Blueprint $table) { - $table->unsignedBigInteger('shopping_list_id')->nullable(false)->change(); - $table->unsignedBigInteger('created_by')->nullable(false)->change(); - }); + try { + Schema::table('shopping_items', function (Blueprint $table) { + $table->foreign('shopping_list_id')->references('id')->on('shopping_lists')->cascadeOnDelete(); + }); + } catch (\Throwable) { + } - Schema::table('shopping_items', function (Blueprint $table) { - $table->foreign('shopping_list_id')->references('id')->on('shopping_lists')->cascadeOnDelete(); - $table->foreign('created_by')->references('id')->on('users')->nullOnDelete(); - $table->index(['shopping_list_id', 'is_done']); - $table->index(['shopping_list_id', 'store_id']); - }); + try { + Schema::table('shopping_items', function (Blueprint $table) { + $table->foreign('created_by')->references('id')->on('users')->nullOnDelete(); + }); + } catch (\Throwable) { + } + + try { + Schema::table('shopping_items', function (Blueprint $table) { + $table->index(['shopping_list_id', 'is_done']); + }); + } catch (\Throwable) { + } + + try { + Schema::table('shopping_items', function (Blueprint $table) { + $table->index(['shopping_list_id', 'store_id']); + }); + } catch (\Throwable) { + } + } + + private function addColumnUnlessExists(string $column): void + { + if ($this->columnExistsMySql('shopping_items', $column)) { + return; + } + + if (Schema::hasColumn('shopping_items', $column)) { + return; + } + + try { + Schema::table('shopping_items', function (Blueprint $table) use ($column) { + $table->unsignedBigInteger($column)->nullable(); + }); + } catch (QueryException $e) { + if ($this->isDuplicateColumnError($e)) { + return; + } + throw $e; + } + } + + private function columnExistsMySql(string $table, string $column): bool + { + if (Schema::getConnection()->getDriverName() !== 'mysql') { + return false; + } + + $db = Schema::getConnection()->getDatabaseName(); + $n = DB::selectOne( + 'SELECT COUNT(*) AS c FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? AND COLUMN_NAME = ?', + [$db, $table, $column] + ); + + return $n !== null && (int) $n->c > 0; + } + + private function isDuplicateColumnError(QueryException $e): bool + { + if ($e->getCode() === '42S21') { + return true; + } + + return str_contains($e->getMessage(), 'Duplicate column'); } public function down(): void diff --git a/database/migrations/2026_03_30_100000_repair_shopping_items_created_by_nullable.php b/database/migrations/2026_03_30_100000_repair_shopping_items_created_by_nullable.php new file mode 100644 index 0000000..815b170 --- /dev/null +++ b/database/migrations/2026_03_30_100000_repair_shopping_items_created_by_nullable.php @@ -0,0 +1,50 @@ +getDriverName() !== 'mysql') { + return; + } + + $col = DB::select("SHOW COLUMNS FROM shopping_items WHERE Field = 'created_by'"); + if (empty($col) || ($col[0]->Null ?? '') === 'YES') { + return; + } + + try { + Schema::table('shopping_items', function (Blueprint $table) { + $table->dropForeign(['created_by']); + }); + } catch (\Throwable) { + } + + DB::statement('ALTER TABLE shopping_items MODIFY created_by BIGINT UNSIGNED NULL'); + + try { + Schema::table('shopping_items', function (Blueprint $table) { + $table->foreign('created_by')->references('id')->on('users')->nullOnDelete(); + }); + } catch (\Throwable) { + } + } + + public function down(): void + { + // + } +};