diff --git a/lib/Migration/Version1Date20260901000000.php b/lib/Migration/Version1Date20260901000000.php new file mode 100644 index 0000000000..4ab9525711 --- /dev/null +++ b/lib/Migration/Version1Date20260901000000.php @@ -0,0 +1,147 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Migration; + +use Closure; +use Exception; +use OCP\IDBConnection; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +/** + * Re-runs `CREATE EXTENSION IF NOT EXISTS pg_trgm` on PostgreSQL. + * + * @package OCA\OpenRegister\Migration + * + * @spec openspec/changes/searchable-property-index/tasks.md#1.1 + */ +class Version1Date20260901000000 extends SimpleMigrationStep { + /** + * Constructor. + * + * @param IDBConnection $connection Database connection + */ + public function __construct( + private readonly IDBConnection $connection, + ) { + }//end __construct() + + /** + * Ensure the pg_trgm extension exists after schema changes (PostgreSQL only). + * + * @param IOutput $output Migration output + * @param Closure $schemaClosure Schema closure + * @param array $options Migration options + * + * @return void + * + * @spec openspec/changes/searchable-property-index/tasks.md#1.1 + */ + public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void { + $platform = $this->connection->getDatabasePlatform(); + + if (str_contains(get_class($platform), 'PostgreSQL') === false) { + $output->info('Skipping pg_trgm extension re-bootstrap: unsupported database platform (PostgreSQL only)'); + return; + } + + $this->ensurePgTrgmExtension(output: $output); + }//end postSchemaChange() + + /** + * Create the pg_trgm extension when missing, tolerating privilege failures. + * + * Same contract as Version1Date20260706110000::ensurePgTrgmExtension — + * `CREATE EXTENSION IF NOT EXISTS` is idempotent, and a privilege + * failure is logged rather than fatal so the migration is safe on + * instances where the extension is already present or where the + * connecting role lacks the CREATE EXTENSION privilege. + * + * Probes `pg_extension` before the CREATE EXTENSION call so the log + * distinguishes the no-op re-run (extension was already installed by the + * 2026-07 bootstrap or an operator) from the actual install that this + * re-run migration ships to perform on instances that missed the first run. + * + * @param IOutput $output Migration output + * + * @return void + */ + private function ensurePgTrgmExtension(IOutput $output): void { + try { + $wasAlreadyInstalled = false; + try { + $result = $this->connection->executeQuery( + "SELECT 1 FROM pg_extension WHERE extname = 'pg_trgm'" + ); + $wasAlreadyInstalled = ($result->fetchOne() !== false); + } catch (Exception $probeException) { + // Probe failed (e.g. permissions on pg_extension); fall through to + // the CREATE EXTENSION attempt, which is idempotent regardless. + } + + $this->connection->executeStatement('CREATE EXTENSION IF NOT EXISTS pg_trgm'); + if ($wasAlreadyInstalled === true) { + $output->info('pg_trgm extension already installed (no-op re-run)'); + } else { + $output->info('pg_trgm extension installed by re-run (fuzzy/substring search indexes can now be created)'); + } + + return; + } catch (Exception $e) { + // Creation failed (usually privileges); check whether it already exists. + try { + $result = $this->connection->executeQuery( + "SELECT 1 FROM pg_extension WHERE extname = 'pg_trgm'" + ); + if ($result->fetchOne() !== false) { + $output->info('pg_trgm extension already installed'); + return; + } + } catch (Exception $inner) { + // Fall through to the warning below. + } + + $output->warning( + 'pg_trgm extension is not installed and could not be created (' + . $e->getMessage() + . '). Fuzzy/substring search (_fuzzy=true, @self.relevance) stays on ' + . 'the unindexed ILIKE path. ' + . 'Run CREATE EXTENSION pg_trgm; as a superuser and re-run this migration to enable it.' + ); + }//end try + }//end ensurePgTrgmExtension() +}//end class