@@ -77,12 +77,21 @@ public function handle(): int
7777
7878 // Safe zone: create temp, copy, verify
7979 if ($ resumeAt === 'CREATE_TEMP ' ) {
80+ if (! $ this ->confirmContinue ('Phase 2: Create Temp Index ' )) {
81+ return self ::SUCCESS ;
82+ }
8083 if (! $ this ->createTempIndex ()) {
8184 return self ::FAILURE ;
8285 }
86+ if (! $ this ->confirmContinue ('Phase 3: Copy to Temp ' )) {
87+ return self ::SUCCESS ;
88+ }
8389 if (! $ this ->copyToTemp ()) {
8490 return self ::FAILURE ;
8591 }
92+ if (! $ this ->confirmContinue ('Phase 4: Verify Temp ' )) {
93+ return self ::SUCCESS ;
94+ }
8695 if (! $ this ->verifyTemp ()) {
8796 return self ::FAILURE ;
8897 }
@@ -114,18 +123,30 @@ public function handle(): int
114123 $ this ->omni ->warning ('Resuming in danger zone — original already gone, temp is source of truth ' );
115124 }
116125
126+ if (! $ this ->confirmContinue ('Phase 6: Create Original (New Mapping) ' )) {
127+ return self ::SUCCESS ;
128+ }
117129 if (! $ this ->createOriginal ()) {
118130 return self ::FAILURE ;
119131 }
120132
133+ if (! $ this ->confirmContinue ('Phase 7: Copy Back ' )) {
134+ return self ::SUCCESS ;
135+ }
121136 if (! $ this ->copyBack ()) {
122137 return self ::FAILURE ;
123138 }
124139
140+ if (! $ this ->confirmContinue ('Phase 8: Verify Final ' )) {
141+ return self ::SUCCESS ;
142+ }
125143 if (! $ this ->verifyFinal ()) {
126144 return self ::FAILURE ;
127145 }
128146
147+ if (! $ this ->confirmContinue ('Phase 9: Cleanup ' )) {
148+ return self ::SUCCESS ;
149+ }
129150 $ this ->cleanup ();
130151 $ this ->summary ();
131152
@@ -177,15 +198,18 @@ private function resolveModel(): ?Model
177198 $ this ->newLine ();
178199 $ this ->omni ->statusError ('Missing mapping definition ' , $ class , [
179200 'Your model must override mappingDefinition(): ' ,
180- '' ,
181- 'use PDPhilip\Elasticsearch\Schema\Blueprint; ' ,
182- '' ,
183- 'public static function mappingDefinition(Blueprint $index): void ' ,
184- '{ ' ,
185- ' $index->keyword( \'status \'); ' ,
186- ' $index->geoPoint( \'location \'); ' ,
187- '} ' ,
188201 ]);
202+ $ this ->omni ->render (<<<'HTML'
203+ <code line="3" start-line="1">
204+ use PDPhilip\Elasticsearch\Schema\Blueprint;
205+
206+ public static function mappingDefinition(Blueprint $index): void
207+ {
208+ $index->keyword('status');
209+ $index->geoPoint('location');
210+ }
211+ </code>
212+ HTML);
189213 $ this ->newLine ();
190214
191215 return null ;
@@ -286,24 +310,23 @@ private function validate(): string|false
286310 return $ this ->handleEmptyIndex ();
287311 }
288312
289- $ this ->omni ->tableHeader ('Check ' , 'Status ' );
290- $ this ->omni ->tableRowSuccess ('Index exists ' , $ this ->indexName );
291- $ this ->omni ->tableRowSuccess ('Record count ' , number_format ($ this ->originalCount ));
292- $ this ->omni ->tableRowSuccess ('No leftover temp ' );
313+ $ analysis = $ this ->mappingAnalysis ();
293314
294- $ this ->showMappings ($ this ->indexName );
295-
296- $ mismatches = $ this ->mappingMismatches ();
297- if (empty ($ mismatches )) {
315+ if (empty ($ analysis ['mismatches ' ])) {
298316 $ this ->omni ->success ('Mapping already matches — nothing to re-index ' );
299317 $ this ->newLine ();
300318
301319 return false ;
302320 }
303321
304- $ this ->omni ->divider ('Fields to update ' );
305- foreach ($ mismatches as $ field => $ info ) {
306- $ this ->omni ->tableRowWarning ($ field , $ info ['current ' ].' → ' .$ info ['desired ' ]);
322+ $ fieldsToUpdate = [];
323+ foreach ($ analysis ['mismatches ' ] as $ field => $ info ) {
324+ $ fieldsToUpdate [$ field ] = $ info ['current ' ].' → ' .$ info ['desired ' ];
325+ }
326+ $ this ->omni ->dataList ($ fieldsToUpdate , 'Fields to Update ' , 'text-emerald-500 ' );
327+
328+ if (! empty ($ analysis ['unmapped ' ])) {
329+ $ this ->omni ->dataList ($ analysis ['unmapped ' ], 'Unmapped Fields ' , 'text-rose-500 ' );
307330 }
308331
309332 return 'CREATE_TEMP ' ;
@@ -659,7 +682,7 @@ private function showMappings(string $index): void
659682 $ this ->omni ->dataList ($ mapping , $ index .' mapping ' );
660683 }
661684
662- private function mappingMismatches (): array
685+ private function mappingAnalysis (): array
663686 {
664687 $ currentMapping = $ this ->schema ->getFieldsMapping ($ this ->indexName );
665688
@@ -668,23 +691,110 @@ private function mappingMismatches(): array
668691 : new Blueprint ($ this ->indexName ); // @phpstan-ignore arguments.count
669692 ($ this ->mappingDefinition )($ blueprint );
670693
694+ $ definedFields = [];
671695 $ mismatches = [];
672696 foreach ($ blueprint ->getAddedColumns () as $ column ) {
673697 $ field = $ column ->name ;
674698 $ desiredType = $ column ->type ;
699+ $ definedFields [] = $ field ;
675700 $ currentType = $ currentMapping [$ field ] ?? null ;
676701
677- if ($ currentType === $ desiredType ) {
702+ if ($ currentType !== $ desiredType ) {
703+ $ mismatches [$ field ] = [
704+ 'current ' => $ currentType ?? 'missing ' ,
705+ 'desired ' => $ desiredType ,
706+ ];
707+
678708 continue ;
679709 }
680710
681- $ mismatches [$ field ] = [
682- 'current ' => $ currentType ?? 'missing ' ,
683- 'desired ' => $ desiredType ,
684- ];
711+ $ subMismatch = $ this ->detectSubFieldMismatch ($ column , $ field , $ currentMapping );
712+ if ($ subMismatch ) {
713+ $ mismatches [$ field ] = $ subMismatch ;
714+ }
715+ }
716+
717+ $ unmapped = [];
718+ foreach ($ currentMapping as $ field => $ type ) {
719+ if (in_array ($ field , $ definedFields )) {
720+ continue ;
721+ }
722+ if ($ this ->isSubFieldOfDefined ($ field , $ definedFields )) {
723+ continue ;
724+ }
725+ $ unmapped [$ field ] = $ type ;
685726 }
686727
687- return $ mismatches ;
728+ return [
729+ 'mismatches ' => $ mismatches ,
730+ 'unmapped ' => $ unmapped ,
731+ ];
732+ }
733+
734+ private function detectSubFieldMismatch ($ column , string $ field , array $ currentMapping ): ?array
735+ {
736+ $ expectedSubs = $ this ->getExpectedSubFields ($ column );
737+ $ currentSubs = $ this ->getCurrentSubFields ($ field , $ currentMapping );
738+
739+ if ($ expectedSubs === $ currentSubs ) {
740+ return null ;
741+ }
742+
743+ $ format = fn (array $ subs ) => empty ($ subs )
744+ ? $ column ->type
745+ : $ column ->type .' [+ ' .implode (', ' , array_keys ($ subs )).'] ' ;
746+
747+ return [
748+ 'current ' => $ format ($ currentSubs ),
749+ 'desired ' => $ format ($ expectedSubs ),
750+ ];
751+ }
752+
753+ private function getExpectedSubFields ($ column ): array
754+ {
755+ if (! ($ column ->fields instanceof Closure)) {
756+ return [];
757+ }
758+
759+ $ subBlueprint = Helpers::getLaravelCompatabilityVersion () >= 12
760+ ? new Blueprint ($ this ->connection , '_sub ' )
761+ : new Blueprint ('_sub ' ); // @phpstan-ignore arguments.count
762+ ($ column ->fields )($ subBlueprint );
763+
764+ $ subs = [];
765+ foreach ($ subBlueprint ->getAddedColumns () as $ subCol ) {
766+ $ subs [$ subCol ->name ] = $ subCol ->type ;
767+ }
768+
769+ return $ subs ;
770+ }
771+
772+ private function getCurrentSubFields (string $ field , array $ mapping ): array
773+ {
774+ $ prefix = $ field .'. ' ;
775+ $ subs = [];
776+ foreach ($ mapping as $ key => $ type ) {
777+ if (! str_starts_with ($ key , $ prefix )) {
778+ continue ;
779+ }
780+ $ subName = substr ($ key , strlen ($ prefix ));
781+ if (! str_contains ($ subName , '. ' )) {
782+ $ subs [$ subName ] = $ type ;
783+ }
784+ }
785+
786+ return $ subs ;
787+ }
788+
789+ private function isSubFieldOfDefined (string $ field , array $ definedFields ): bool
790+ {
791+ foreach ($ definedFields as $ defined ) {
792+ if (str_starts_with ($ field , $ defined .'. ' )) {
793+ return true ;
794+ }
795+ }
796+
797+ return false ;
688798 }
689799
690800 private function confirmSettings (): bool
@@ -701,7 +811,7 @@ private function confirmSettings(): bool
701811 $ valid = ['yes ' , 'y ' , 'edit ' , 'e ' , 'cancel ' , 'c ' , 'no ' , 'n ' ];
702812
703813 while (! in_array (strtolower ($ answer ), $ valid )) {
704- $ answer = $ this ->omni ->ask ('Continue with these settings? ' , ['Yes ' , 'Edit ' , 'Cancel ' ]);
814+ $ answer = $ this ->omni ->ask ('Continue with these settings? ' , ['yes ' , 'edit ' , 'cancel ' ]);
705815 }
706816
707817 $ answer = strtolower ($ answer );
@@ -735,13 +845,22 @@ private function editSettings(): void
735845 $ this ->omni ->success ('Settings updated — tolerance: ' .($ this ->tolerance * 100 ).'%, retries: ' .$ this ->maxRetries );
736846 }
737847
848+ private function confirmContinue (string $ nextPhase ): bool
849+ {
850+ if ($ this ->option ('force ' )) {
851+ return true ;
852+ }
853+
854+ return $ this ->promptYesNo ('Continue to ' .$ nextPhase .'? ' );
855+ }
856+
738857 private function promptYesNo (string $ question ): bool
739858 {
740859 $ answer = '' ;
741860 $ valid = ['yes ' , 'y ' , 'n ' , 'no ' ];
742861
743862 while (! in_array (strtolower ($ answer ), $ valid )) {
744- $ answer = $ this ->omni ->ask ($ question , ['Yes ' , 'No ' ]);
863+ $ answer = $ this ->omni ->ask ($ question , ['yes ' , 'no ' ]);
745864 }
746865
747866 return in_array (strtolower ($ answer ), ['yes ' , 'y ' ]);
0 commit comments