Lines 82.66% 62 / 75
Methods 60.00% 3 / 5
Classes 0.00% 0 / 1
Name Lines Methods CRAP
 run 83.09% 59 / 71 0.00% 0 / 1 21.93
 line 100.00% 1 / 1 100.00% 1 / 1 1
 success 100.00% 1 / 1 100.00% 1 / 1 1
 warn 100.00% 1 / 1 100.00% 1 / 1 1
 error 0.00% 0 / 1 0.00% 0 / 1 2
37final class SyncLegacyCommand
38{
39    public static string $command = "sync:legacy";
40
41    /**
42     * Run the legacy table-name migration.
43     *
44     * @param array<string, mixed> $options CLI options (--dir, --filter,
45     *        --exclude-filter, --dry-run)
46     * @return string Always '' — output is echoed progressively
47     */
48    public function run(array $options = []): string
49    {
50        self::warn(
51            "DEPRECATED: sync:legacy is a one-time migration and will be removed in a future release."
52        );
53
54        // ---- Discover models ----
55        $dirs = isset($options['dir'])
56            ? array_map(trim(...), explode(',', (string) $options['dir']))
57            : null;
58
59        $models = (new ModelDiscovery())->discover($dirs);
60
61        // ---- Apply filters (exclude wins) ----
62        try {
63            $filter = new ModelFilter(
64                isset($options['filter']) ? (string) $options['filter'] : null,
65                isset($options['exclude-filter']) ? (string) $options['exclude-filter'] : null,
66            );
67        } catch (\InvalidArgumentException $e) {
68            self::error(ExceptionChain::render($e, ''));
69            return '';
70        }
71
72        ['active' => $models] = $filter->apply($models);
73
74        if ($models === []) {
75            self::warn("No models matched.");
76            return '';
77        }
78
79        // ---- Resolve old and new table names ----
80        $connection = Database::sqlConnection();
81        $liveTables = $connection->schemaInspector->tables();
82
83        // Atomic apply: on dialects with transactional DDL the whole rename
84        // set commits or rolls back together (a mid-loop failure must not
85        // leave half the tables renamed). MySQL DDL auto-commits, so the
86        // apply runs non-transactionally there.
87        $transactional = $connection->supportsTransactionalDdl();
88
89        $renamed = 0;
90        $skipped = 0;
91        $dryRun = isset($options['dry-run']);
92
93        try {
94            $connection->withLock(function () use ($connection, $models, $liveTables, $transactional, $dryRun, &$renamed, &$skipped): void {
95                $synchronizer = new SchemaSynchronizer($connection);
96
97                // ---- Collect every rename first — apply ONCE, atomically ----
98                $changes = [];
99
100                foreach ($models as $model) {
101                    $metadata = MetadataFactory::tryFor($model);
102
103                    if ($metadata === null || $metadata->tableName === null) {
104                        continue; // broken or table-less model — skip
105                    }
106
107                    $newTable = $metadata->tableName;
108                    $oldTable = (new \ReflectionClass($model))->getShortName();
109
110                    if ($oldTable === $newTable) {
111                        continue; // names already match
112                    }
113
114                    if (!in_array($oldTable, $liveTables, true)) {
115                        continue; // no old-style table — nothing to rename
116                    }
117
118                    if (in_array($newTable, $liveTables, true)) {
119                        self::error("Cannot rename [{$oldTable}]: target [{$newTable}] already exists.");
120                        $skipped++;
121                        continue;
122                    }
123
124                    $changes[] = new SchemaChange(
125                        $newTable,
126                        \BlueprintAU\Radiant\Database\Schema\Enums\SchemaOperation::RenameTable,
127                        (new Blueprint($newTable))->renamedFrom($oldTable),
128                        false,
129                        "rename table [{$oldTable}] to [{$newTable}] — data travels with the rename",
130                        false,
131                        $oldTable,
132                    );
133                }
134
135                if ($changes === []) {
136                    return;
137                }
138
139                if ($dryRun) {
140                    foreach ($changes as $change) {
141                        self::line("Would rename [{$change->renameOf}] → [{$change->table}].");
142                    }
143                    return;
144                }
145
146                // Renames are not destructive — no confirm gate. Apply the
147                // whole set in one call so a transactional dialect commits
148                // or rolls back atomically.
149                $applied = $synchronizer->apply($changes, transactional: $transactional);
150                $renamed = count($applied);
151
152                foreach ($applied as $change) {
153                    self::success("Renamed [{$change->renameOf}] → [{$change->table}].");
154                }
155            }, 'radiant:schema');
156        } catch (\Throwable $e) {
157            self::error(ExceptionChain::render($e, "sync:legacy failed: "));
158            return '';
159        }
160
161        if ($dryRun) {
162            self::line(ConsoleColors::FG_CYAN . "Dry run — no changes applied." . ConsoleColors::RESET);
163            return '';
164        }
165
166        if ($renamed === 0 && $skipped === 0) {
167            self::success("Nothing to rename — schema is already in the Radiant naming.");
168        } else {
169            self::line("Renamed {$renamed} table(s), skipped {$skipped}.");
170        }
171
172        return '';
173    }
174
175    private static function line(string $message): void
176    {
177        echo $message . "\n";
178    }
179
180    private static function success(string $message): void
181    {
182        echo ConsoleColors::FG_GREEN . $message . ConsoleColors::RESET . "\n";
183    }
184
185    private static function warn(string $message): void
186    {
187        echo ConsoleColors::FG_YELLOW . $message . ConsoleColors::RESET . "\n";
188    }
189
190    private static function error(string $message): void
191    {
192        echo ConsoleColors::FG_RED . $message . ConsoleColors::RESET . "\n";
193    }
194}