Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
147 changes: 147 additions & 0 deletions lib/Migration/Version1Date20260901000000.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
<?php

/**
* Re-run of the PostgreSQL `pg_trgm` extension bootstrap.
*
* Version1Date20260706110000 already installs `pg_trgm` — but that file's
* `CREATE EXTENSION IF NOT EXISTS pg_trgm` call was added on 2026-07-21
* (openregister commit 15d93450a3c), *after* the migration had already
* shipped and been recorded as run on existing installations. Nextcloud's
* migrator only executes each `app+version` row once, so instances that
* ran the older empty version of the file never install the extension —
* and `MagicSearchHandler::hasPgTrgmExtension()` correctly reports false,
* so `_fuzzy=true` silently degrades to unindexed ILIKE and never sets
* `@self.relevance`. Symptom: fuzzy typo-tolerance and relevance-scoring
* broken on any instance upgraded through the intermediate window.
*
* This migration is a NEW `app+version` row that re-runs the same
* idempotent `CREATE EXTENSION IF NOT EXISTS pg_trgm` on every instance.
* Same tolerant-failure contract as Version1Date20260706110000 —
* privilege denial is logged, never fatal.
*
* SPDX-License-Identifier: EUPL-1.2
* SPDX-FileCopyrightText: 2026 Conduction B.V.
*
* @category Migration
* @package OCA\OpenRegister\Migration
*
* @author Conduction Development Team <dev@conduction.nl>
* @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