diff --git a/app/Console/Commands/InspectManticore.php b/app/Console/Commands/InspectManticore.php index 667469645..1e7f2ad6c 100644 --- a/app/Console/Commands/InspectManticore.php +++ b/app/Console/Commands/InspectManticore.php @@ -37,8 +37,8 @@ final class InspectManticore extends Command } $actual = $client->table($table)->describe(); $comparison = ManticoreSchemaInspector::compareColumns(is_array($actual) ? $actual : [], $definition['columns']); - $actualSettings = $client->sql("SHOW TABLE {$table} SETTINGS", true); - $missingSettings = ManticoreSchemaInspector::missingSettings($actualSettings, $definition['settings']); + $actualSettings = $client->tables()->settings(['table' => $table]); + $missingSettings = ManticoreSchemaInspector::missingSettings($actualSettings, ManticoreIndexRegistry::inspectableSettings()); $needsRebuild = $comparison['missing'] !== [] || $comparison['incompatible'] !== [] || $missingSettings !== []; $report['tables'][$table] = [...$comparison, 'missing_settings' => $missingSettings, 'needs_rebuild' => $needsRebuild]; $report['compatible'] = $report['compatible'] && ! $needsRebuild; diff --git a/app/Services/Search/Support/ManticoreIndexRegistry.php b/app/Services/Search/Support/ManticoreIndexRegistry.php index 9b89f5808..754c123ba 100644 --- a/app/Services/Search/Support/ManticoreIndexRegistry.php +++ b/app/Services/Search/Support/ManticoreIndexRegistry.php @@ -83,6 +83,14 @@ final class ManticoreIndexRegistry }; } + /** @return array */ + public static function inspectableSettings(): array + { + // Manticore 28 omits defaults/no-op settings such as min_prefix_len=0 + // and exact_words from SHOW TABLE ... SETTINGS / SHOW CREATE TABLE. + return ['min_infix_len' => 2, 'index_field_lengths' => 1]; + } + /** @param array $configuredIndexes */ public static function logicalName(string $table, array $configuredIndexes = []): ?string { diff --git a/app/Services/Search/Support/ManticoreSchemaInspector.php b/app/Services/Search/Support/ManticoreSchemaInspector.php index a99657eb1..93ffaadc2 100644 --- a/app/Services/Search/Support/ManticoreSchemaInspector.php +++ b/app/Services/Search/Support/ManticoreSchemaInspector.php @@ -12,24 +12,24 @@ final class ManticoreSchemaInspector */ public static function missingSettings(mixed $actual, array $expected): array { - $encoded = strtolower((string) json_encode($actual)); + $settings = self::normalizeSettings($actual); - return array_values(array_filter(array_keys($expected), static function (string $setting) use ($encoded, $expected): bool { - return ! str_contains($encoded, strtolower($setting)) - || ! str_contains($encoded, strtolower((string) $expected[$setting])); + return array_values(array_filter(array_keys($expected), static function (string $setting) use ($settings, $expected): bool { + return ! array_key_exists(strtolower($setting), $settings) + || $settings[strtolower($setting)] !== strtolower((string) $expected[$setting]); })); } /** - * @param array> $actual + * @param array> $actual * @param array> $expected * @return array{missing: list, incompatible: array} */ public static function compareColumns(array $actual, array $expected): array { $types = []; - foreach ($actual as $column) { - $name = (string) ($column['Field'] ?? $column['field'] ?? ''); + foreach ($actual as $key => $column) { + $name = (string) ($column['Field'] ?? $column['field'] ?? (is_string($key) ? $key : '')); $type = strtolower((string) ($column['Type'] ?? $column['type'] ?? '')); if ($name !== '') { $types[$name] = $type; @@ -42,11 +42,67 @@ final class ManticoreSchemaInspector $expectedType = strtolower((string) $definition['type']); if (! isset($types[$name])) { $missing[] = $name; - } elseif (! str_contains($types[$name], $expectedType)) { + } elseif (! self::typesAreCompatible($types[$name], $expectedType)) { $incompatible[$name] = ['expected' => $expectedType, 'actual' => $types[$name]]; } } return ['missing' => $missing, 'incompatible' => $incompatible]; } + + private static function typesAreCompatible(string $actual, string $expected): bool + { + if ($expected === 'integer') { + return str_contains($actual, 'integer') || str_contains($actual, 'uint'); + } + + return str_contains($actual, $expected); + } + + /** @return array */ + private static function normalizeSettings(mixed $actual): array + { + if (! is_array($actual)) { + return []; + } + + $settings = []; + foreach ($actual as $key => $value) { + if (is_string($key) && ! is_array($value)) { + if (strtolower($key) === 'settings' && is_string($value)) { + foreach (preg_split('/\R/', $value) ?: [] as $line) { + if (preg_match('/^\s*([a-z0-9_]+)\s*=\s*[\'\"]?([^\'\"]+?)[\'\"]?\s*$/i', $line, $match) === 1) { + $settings[strtolower($match[1])] = strtolower(trim($match[2])); + } + } + + continue; + } + $settings[strtolower($key)] = strtolower((string) $value); + + continue; + } + + if (! is_array($value)) { + continue; + } + + $name = $value['Setting_name'] ?? $value['setting_name'] ?? $value['Variable_name'] ?? $value['variable_name'] ?? $value['Name'] ?? $value['name'] ?? null; + $settingValue = $value['Value'] ?? $value['value'] ?? null; + if (is_string($name) && $settingValue !== null) { + $settings[strtolower($name)] = strtolower((string) $settingValue); + + continue; + } + + if (is_string($key)) { + $nestedValue = $value['Value'] ?? $value['value'] ?? reset($value); + if (is_scalar($nestedValue)) { + $settings[strtolower($key)] = strtolower((string) $nestedValue); + } + } + } + + return $settings; + } } diff --git a/tests/Unit/ManticoreIndexRegistryTest.php b/tests/Unit/ManticoreIndexRegistryTest.php index 9006f60ae..8440a71ff 100644 --- a/tests/Unit/ManticoreIndexRegistryTest.php +++ b/tests/Unit/ManticoreIndexRegistryTest.php @@ -56,6 +56,59 @@ final class ManticoreIndexRegistryTest extends TestCase self::assertSame('bigint', $result['incompatible']['passwordstatus']['expected']); } + #[Test] + public function it_accepts_the_php_clients_associative_describe_response(): void + { + $result = ManticoreSchemaInspector::compareColumns( + [ + 'passwordstatus' => ['Type' => 'bigint', 'Properties' => []], + 'haspreview' => ['Type' => 'bigint', 'Properties' => []], + ], + ['passwordstatus' => ['type' => 'bigint'], 'haspreview' => ['type' => 'bigint']] + ); + + self::assertSame([], $result['missing']); + self::assertSame([], $result['incompatible']); + } + + #[Test] + public function it_treats_manticore_uint_as_the_integer_schema_type(): void + { + $result = ManticoreSchemaInspector::compareColumns( + ['categories_id' => ['Type' => 'uint']], + ['categories_id' => ['type' => 'integer']] + ); + + self::assertSame([], $result['incompatible']); + } + + #[Test] + public function it_compares_settings_by_name_and_value(): void + { + $actual = [ + 'min_infix_len' => ['Value' => '2'], + 'exact_words' => ['Value' => '1'], + 'index_field_lengths' => ['Value' => '1'], + ]; + + self::assertSame([], ManticoreSchemaInspector::missingSettings($actual, [ + 'min_infix_len' => 2, + 'exact_words' => 1, + 'index_field_lengths' => 1, + ])); + } + + #[Test] + public function it_parses_manticore_28_bundled_settings_response(): void + { + $actual = ['settings' => "min_infix_len = 2\nindex_field_lengths = 1"]; + + self::assertSame([], ManticoreSchemaInspector::missingSettings( + $actual, + ManticoreIndexRegistry::inspectableSettings() + )); + } + #[Test] public function benchmark_fixture_is_versioned_and_covers_every_table(): void {