1313use PDPhilip \Elasticsearch \Eloquent \Model ;
1414use PDPhilip \Elasticsearch \Schema \Blueprint ;
1515use PDPhilip \Elasticsearch \Schema \Builder as SchemaBuilder ;
16- use PDPhilip \Elasticsearch \Utils \Helpers ;
16+ use PDPhilip \Elasticsearch \Utils \Sanitizer ;
1717
1818class ReIndexCommand extends Command
1919{
@@ -104,7 +104,7 @@ public function handle(): int
104104 // Confirmation gate before danger zone
105105 if ($ resumeAt !== 'CREATE_ORIGINAL ' ) {
106106 $ this ->omni ->hr ();
107- $ this ->omni ->warning ('About to enter the danger zone — original index will be dropped ' );
107+ $ this ->omni ->warning ('About to enter the danger zone - original index will be dropped ' );
108108 $ this ->omni ->tableRow ('Original ' , $ this ->indexName .' ( ' .$ this ->originalCount .' docs) ' );
109109 $ this ->omni ->tableRow ('Temp ' , $ this ->tempIndexName .' ( ' .$ this ->tempCount .' docs) ' );
110110 $ this ->omni ->hr ();
@@ -120,7 +120,7 @@ public function handle(): int
120120 return self ::FAILURE ;
121121 }
122122 } else {
123- $ this ->omni ->warning ('Resuming in danger zone — original already gone, temp is source of truth ' );
123+ $ this ->omni ->warning ('Resuming in danger zone - original already gone, temp is source of truth ' );
124124 }
125125
126126 if (! $ this ->confirmContinue ('Phase 6: Create Original (New Mapping) ' )) {
@@ -287,7 +287,7 @@ private function validate(): string|false
287287 $ tempExists = $ this ->schema ->hasTable ($ this ->tempIndexName );
288288
289289 if (! $ originalExists && ! $ tempExists ) {
290- $ this ->omni ->error ('Both indices missing — catastrophic state, manual recovery needed ' );
290+ $ this ->omni ->error ('Both indices missing - catastrophic state, manual recovery needed ' );
291291 $ this ->newLine ();
292292
293293 return false ;
@@ -313,20 +313,17 @@ private function validate(): string|false
313313 $ analysis = $ this ->mappingAnalysis ();
314314
315315 if (empty ($ analysis ['mismatches ' ])) {
316- $ this ->omni ->success ('Mapping already matches — nothing to re-index ' );
316+ $ this ->omni ->success ('Mapping already matches - nothing to re-index ' );
317317 $ this ->newLine ();
318318
319319 return false ;
320320 }
321321
322- $ fieldsToUpdate = [];
323- foreach ($ analysis ['mismatches ' ] as $ field => $ info ) {
324- $ fieldsToUpdate [$ field ] = $ info ['current ' ].' → ' .$ info ['desired ' ];
325- }
322+ $ fieldsToUpdate = $ this ->formatMismatchesForDisplay ($ analysis ['mismatches ' ]);
326323 $ this ->omni ->dataList ($ fieldsToUpdate , 'Fields to Update ' , 'text-emerald-500 ' );
327324
328325 if (! empty ($ analysis ['unmapped ' ])) {
329- $ this ->omni ->dataList ($ analysis ['unmapped ' ], 'Unmapped Fields ' , 'text-rose-500 ' );
326+ $ this ->omni ->dataList ($ this -> nestDotNotation ( $ analysis ['unmapped ' ]) , 'Unmapped Fields ' , 'text-rose-500 ' );
330327 }
331328
332329 return 'CREATE_TEMP ' ;
@@ -338,7 +335,7 @@ private function validateWithExistingTemp(bool $originalExists): string|false
338335
339336 if (! $ originalExists ) {
340337 $ this ->omni ->warning ('Original missing, temp has ' .$ this ->tempCount .' docs ' );
341- $ this ->omni ->info ('Resuming from danger zone — temp is source of truth ' );
338+ $ this ->omni ->info ('Resuming from danger zone - temp is source of truth ' );
342339 $ this ->originalCount = $ this ->tempCount ;
343340
344341 return 'CREATE_ORIGINAL ' ;
@@ -347,27 +344,27 @@ private function validateWithExistingTemp(bool $originalExists): string|false
347344 $ this ->originalCount = $ this ->countDocs ($ this ->indexName );
348345
349346 if ($ this ->tempCount === 0 ) {
350- $ this ->omni ->info ('Empty temp from previous run — dropping, starting fresh ' );
347+ $ this ->omni ->info ('Empty temp from previous run - dropping, starting fresh ' );
351348 $ this ->schema ->dropIfExists ($ this ->tempIndexName );
352349
353350 return 'CREATE_TEMP ' ;
354351 }
355352
356353 if ($ this ->countsMatch ($ this ->originalCount , $ this ->tempCount )) {
357- $ this ->omni ->info ('Temp has matching data ( ' .$ this ->tempCount .' docs) — resuming at verify ' );
354+ $ this ->omni ->info ('Temp has matching data ( ' .$ this ->tempCount .' docs) - resuming at verify ' );
358355
359356 return 'VERIFY_TEMP ' ;
360357 }
361358
362- $ this ->omni ->info ('Partial temp ( ' .$ this ->tempCount .'/ ' .$ this ->originalCount .') — dropping, starting fresh ' );
359+ $ this ->omni ->info ('Partial temp ( ' .$ this ->tempCount .'/ ' .$ this ->originalCount .') - dropping, starting fresh ' );
363360 $ this ->schema ->dropIfExists ($ this ->tempIndexName );
364361
365362 return 'CREATE_TEMP ' ;
366363 }
367364
368365 private function handleEmptyIndex (): string |false
369366 {
370- $ this ->omni ->warning ('Index has 0 records — just drop and recreate ' );
367+ $ this ->omni ->warning ('Index has 0 records - just drop and recreate ' );
371368
372369 if (! $ this ->option ('force ' ) && ! $ this ->promptYesNo ('Drop and recreate with new mapping? ' )) {
373370 $ this ->newLine ();
@@ -427,7 +424,7 @@ private function copyToTemp(): bool
427424 }
428425
429426 $ this ->tempCount = (int ) ($ result ['created ' ] ?? 0 );
430- $ this ->omni ->success ('Copy complete — ' .$ this ->tempCount .' docs created ' );
427+ $ this ->omni ->success ('Copy complete - ' .$ this ->tempCount .' docs created ' );
431428
432429 return true ;
433430 } catch (Exception $ e ) {
@@ -480,7 +477,7 @@ private function dropOriginal(): bool
480477 return true ;
481478 } catch (Exception $ e ) {
482479 $ this ->omni ->statusError ('Failed to drop original ' , $ e ->getMessage ());
483- $ this ->omni ->info ('Safe state — original still exists. Drop temp manually if needed. ' );
480+ $ this ->omni ->info ('Safe state - original still exists. Drop temp manually if needed. ' );
484481 $ this ->newLine ();
485482
486483 return false ;
@@ -496,7 +493,7 @@ private function createOriginal(): bool
496493 $ this ->omni ->divider ('Phase 6: Create Original (New Mapping) ' );
497494
498495 if ($ this ->schema ->hasTable ($ this ->indexName )) {
499- $ this ->omni ->info ('Partial original exists — dropping before recreate ' );
496+ $ this ->omni ->info ('Partial original exists - dropping before recreate ' );
500497 try {
501498 $ this ->schema ->drop ($ this ->indexName );
502499 sleep (1 );
@@ -536,7 +533,7 @@ private function copyBack(): bool
536533 $ failures = $ result ['failures ' ] ?? [];
537534 if (empty ($ failures )) {
538535 $ this ->finalCount = (int ) ($ result ['created ' ] ?? 0 );
539- $ this ->omni ->success ('Copy back complete — ' .$ this ->finalCount .' docs ' );
536+ $ this ->omni ->success ('Copy back complete - ' .$ this ->finalCount .' docs ' );
540537
541538 return true ;
542539 }
@@ -576,7 +573,7 @@ private function verifyFinal(): bool
576573 return true ;
577574 }
578575
579- $ this ->omni ->warning ('Final count mismatch ( ' .$ this ->finalCount .' vs ' .$ this ->tempCount .') — running catch-up ' );
576+ $ this ->omni ->warning ('Final count mismatch ( ' .$ this ->finalCount .' vs ' .$ this ->tempCount .') - running catch-up ' );
580577
581578 try {
582579 $ result = $ this ->schema ->reindex ($ this ->tempIndexName , $ this ->indexName );
@@ -591,7 +588,7 @@ private function verifyFinal(): bool
591588 $ this ->omni ->warning ('Catch-up failed: ' .$ e ->getMessage ());
592589 }
593590
594- $ this ->critical ('Final verification failed — ' .$ this ->finalCount .' vs ' .$ this ->tempCount );
591+ $ this ->critical ('Final verification failed - ' .$ this ->finalCount .' vs ' .$ this ->tempCount );
595592
596593 return false ;
597594 }
@@ -651,7 +648,7 @@ private function countDocs(string $index): int
651648
652649 private function rollbackTemp (string $ reason ): void
653650 {
654- $ this ->omni ->warning ('Rolling back — dropping temp index ' );
651+ $ this ->omni ->warning ('Rolling back - dropping temp index ' );
655652 $ this ->omni ->info ('Reason: ' .$ reason );
656653
657654 try {
@@ -667,7 +664,7 @@ private function critical(string $message): void
667664 $ this ->omni ->hrError ();
668665 $ this ->omni ->error ('CRITICAL: ' .$ message );
669666 $ this ->omni ->error ('Data lives in temp index: ' .$ this ->tempIndexName );
670- $ this ->omni ->error ('DO NOT drop ' .$ this ->tempIndexName .' — it is the only complete copy ' );
667+ $ this ->omni ->error ('DO NOT drop ' .$ this ->tempIndexName .' - it is the only complete copy ' );
671668 $ this ->omni ->hrError ();
672669 $ this ->newLine ();
673670 }
@@ -678,51 +675,38 @@ private function showMappings(string $index): void
678675 return ;
679676 }
680677
681- $ mapping = $ this ->schema ->getFieldsMapping ($ index );
678+ $ mapping = $ this ->schema ->getMappings ($ index );
682679 $ this ->omni ->dataList ($ mapping , $ index .' mapping ' );
683680 }
684681
685682 private function mappingAnalysis (): array
686683 {
687- $ currentMapping = $ this ->schema ->getFieldsMapping ($ this ->indexName );
688-
689- $ blueprint = Helpers::getLaravelCompatabilityVersion () >= 12
690- ? new Blueprint ($ this ->connection , $ this ->indexName )
691- : new Blueprint ($ this ->indexName ); // @phpstan-ignore arguments.count
692- ($ this ->mappingDefinition )($ blueprint );
684+ $ currentMapping = $ this ->schema ->getMappings ($ this ->indexName );
685+ $ desiredMapping = Sanitizer::flattenMappingProperties ([
686+ 'properties ' => $ this ->schema ->compileMapping ($ this ->mappingDefinition ),
687+ ]);
693688
694- $ definedFields = [] ;
689+ $ desiredFields = array_keys ( $ desiredMapping ) ;
695690 $ mismatches = [];
696- foreach ($ blueprint ->getAddedColumns () as $ column ) {
697- $ field = $ column ->name ;
698- $ desiredType = $ column ->type ;
699- $ definedFields [] = $ field ;
700- $ currentType = $ currentMapping [$ field ] ?? null ;
701-
702- if ($ currentType !== $ desiredType ) {
703- $ mismatches [$ field ] = [
704- 'current ' => $ currentType ?? 'missing ' ,
705- 'desired ' => $ desiredType ,
706- ];
707691
692+ foreach ($ desiredMapping as $ field => $ desired ) {
693+ $ current = $ currentMapping [$ field ] ?? null ;
694+ if ($ current === $ desired ) {
708695 continue ;
709696 }
710697
711- $ subMismatch = $ this -> detectSubFieldMismatch ( $ column , $ field, $ currentMapping );
712- if ( $ subMismatch ) {
713- $ mismatches [ $ field ] = $ subMismatch ;
714- }
698+ $ mismatches [ $ field] = [
699+ ' current ' => ( $ current [ ' type ' ] ?? null ) ?? ' missing ' ,
700+ ' desired ' => $ desired [ ' type ' ] ?? ' unknown ' ,
701+ ];
715702 }
716703
717704 $ unmapped = [];
718- foreach ($ currentMapping as $ field => $ type ) {
719- if (in_array ( $ field , $ definedFields )) {
705+ foreach ($ currentMapping as $ field => $ details ) {
706+ if (isset ( $ desiredMapping [ $ field ] )) {
720707 continue ;
721708 }
722- if ($ this ->isSubFieldOfDefined ($ field , $ definedFields )) {
723- continue ;
724- }
725- $ unmapped [$ field ] = $ type ;
709+ $ unmapped [$ field ] = $ details ['type ' ] ?? 'object ' ;
726710 }
727711
728712 return [
@@ -731,70 +715,37 @@ private function mappingAnalysis(): array
731715 ];
732716 }
733717
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
718+ private function formatMismatchesForDisplay (array $ mismatches ): array
754719 {
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- }
720+ $ display = [];
721+ foreach ($ mismatches as $ field => $ info ) {
722+ $ label = $ info ['current ' ].' → ' .$ info ['desired ' ];
723+ if (empty ($ info ['details ' ])) {
724+ $ display [$ field ] = $ label ;
771725
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 )) {
778726 continue ;
779727 }
780- $ subName = substr ($ key , strlen ($ prefix ));
781- if (! str_contains ($ subName , '. ' )) {
782- $ subs [$ subName ] = $ type ;
783- }
728+ $ display [$ field ] = [$ label => $ info ['details ' ]];
784729 }
785730
786- return $ subs ;
731+ return $ display ;
787732 }
788733
789- private function isSubFieldOfDefined ( string $ field , array $ definedFields ): bool
734+ private function nestDotNotation ( array $ flat ): array
790735 {
791- foreach ($ definedFields as $ defined ) {
792- if (str_starts_with ($ field , $ defined .'. ' )) {
793- return true ;
736+ $ nested = [];
737+ foreach ($ flat as $ key => $ value ) {
738+ $ parts = explode ('. ' , $ key );
739+ if (count ($ parts ) === 1 ) {
740+ $ nested [$ key ] = $ value ;
741+
742+ continue ;
794743 }
744+ $ parent = array_shift ($ parts );
745+ $ nested [$ parent ][implode ('. ' , $ parts )] = $ value ;
795746 }
796747
797- return false ;
748+ return $ nested ;
798749 }
799750
800751 private function confirmSettings (): bool
@@ -842,7 +793,7 @@ private function editSettings(): void
842793 $ this ->maxRetries = (int ) $ retries ;
843794 }
844795
845- $ this ->omni ->success ('Settings updated — tolerance: ' .($ this ->tolerance * 100 ).'%, retries: ' .$ this ->maxRetries );
796+ $ this ->omni ->success ('Settings updated - tolerance: ' .($ this ->tolerance * 100 ).'%, retries: ' .$ this ->maxRetries );
846797 }
847798
848799 private function confirmContinue (string $ nextPhase ): bool
0 commit comments