diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2efb04e..e6c4029 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,6 +4,7 @@ on: [push] jobs: test: runs-on: ubuntu-latest + name: Nextcloud ${{ matrix.nextcloud }} - ${{ matrix.db.name }} strategy: matrix: db: @@ -12,6 +13,11 @@ jobs: username: root database: nextcloud port: 3306 + - name: pgsql + image: postgres:17.5 + username: postgres + database: postgres + port: 5432 nextcloud: [31] services: db: @@ -54,5 +60,13 @@ jobs: run: > php occ -n fulltextsearch:configure '{"search_platform": "OCA\\FullTextSearch_SQL\\Platform\\SQLPlatform"}' + # This can be removed as soon as https://github.com/nextcloud/fulltextsearch/pull/915 is merged. + - name: Fix test harness + run: > + sed -i \ + -e "s/'document is a simple test'/'document is a'/" \ + -e "s/document is a simple -test/document -test/" \ + custom_apps/fulltextsearch/lib/Command/Test.php + - name: Run fulltextsearch test run: php occ -n fulltextsearch:test --platform_delay 1 \ No newline at end of file diff --git a/appinfo/info.xml b/appinfo/info.xml index df2b953..fcadcfd 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -29,7 +29,7 @@ Extension to the _Full text search_ app to communicate with the usual Nextcloud mysql - postgresql + pgsql diff --git a/lib/Db/IndexDocumentMapper.php b/lib/Db/IndexDocumentMapper.php index 23593a0..f647870 100644 --- a/lib/Db/IndexDocumentMapper.php +++ b/lib/Db/IndexDocumentMapper.php @@ -53,31 +53,43 @@ public function search(ISearchRequest $request, string $providerId, IDocumentAcc ); } - $qb->andWhere( - $qb->expr()->orX( - $qb->expr()->eq('owner', $qb->createNamedParameter($access->getViewerId(), IQueryBuilder::PARAM_STR)), - 'JSON_CONTAINS(access_users, ' . $qb->createNamedParameter(json_encode($access->getViewerId())) . ')', - 'JSON_OVERLAPS(access_groups, ' . $qb->createNamedParameter(json_encode($access->getGroups())) . ')', - 'JSON_OVERLAPS(access_circles, ' . $qb->createNamedParameter(json_encode($access->getCircles())) . ')', - ) - ); + $search = $qb->createNamedParameter($request->getSearch(), IQueryBuilder::PARAM_STR); + $viewerId = $qb->createNamedParameter($access->getViewerId(), IQueryBuilder::PARAM_STR); + $jsonViewerId = $qb->createNamedParameter(json_encode($access->getViewerId()), IQueryBuilder::PARAM_STR); + $jsonGroups = $qb->createNamedParameter(json_encode($access->getGroups()), IQueryBuilder::PARAM_STR); + $jsonCircles = $qb->createNamedParameter(json_encode($access->getCircles()), IQueryBuilder::PARAM_STR); + // TODO: Match tags, subtags, whatnot... switch ($this->db->getDatabaseProvider()) { case IDBConnection::PLATFORM_MYSQL: - $q = 'MATCH (content) AGAINST (:search IN BOOLEAN MODE)'; + $qb->andWhere( + $qb->expr()->orX( + $qb->expr()->eq('owner', $viewerId), + "JSON_CONTAINS(access_users, $jsonViewerId)", + "JSON_OVERLAPS(access_groups, $jsonGroups)", + "JSON_OVERLAPS(access_circles, $jsonCircles)", + ) + ); + + $q = "MATCH (content) AGAINST ($search IN BOOLEAN MODE)"; $qb->andWhere($q) ->selectAlias($qb->createFunction($q), 'score'); break; + case IDBConnection::PLATFORM_POSTGRES: - $qb->andWhere('to_tsvector(content) @@ to_tsquery(:search)'); - break; - case IDBConnection::PLATFORM_SQLITE: - break; - case IDBConnection::PLATFORM_ORACLE: + $qb + ->andWhere( + $qb->expr()->orX( + $qb->expr()->eq('owner', $viewerId), + "access_users @> $jsonViewerId::jsonb", + "jsonb_exists_any(access_groups, JSON_QUERY($jsonGroups, '$' RETURNING text[]))", + "jsonb_exists_any(access_circles, JSON_QUERY($jsonCircles, '$' RETURNING text[]))", + ) + ) + ->andWhere("to_tsvector(content) @@ websearch_to_tsquery($search)"); break; } - $qb->setParameter('search', $request->getSearch()); return $this->findEntities($qb); } diff --git a/lib/Migration/Version10000Date20250720000000.php b/lib/Migration/Version10000Date20250720000000.php index dcd839b..17e0580 100644 --- a/lib/Migration/Version10000Date20250720000000.php +++ b/lib/Migration/Version10000Date20250720000000.php @@ -16,6 +16,10 @@ class Version10000Date20250720000000 extends SimpleMigrationStep { public const TABLE = 'fts_documents'; + private $collations = [ + IDBConnection::PLATFORM_MYSQL => 'utf8mb4_unicode_ci', + IDBConnection::PLATFORM_POSTGRES => 'unicode', + ]; public function __construct( private IDBConnection $db @@ -44,28 +48,52 @@ public function changeSchema(IOutput $output, \Closure $schemaClosure, array $op 'notnull' => true ]); $table->addColumn('access_users', Types::JSON, [ - 'notnull' => true + 'notnull' => true, + 'customSchemaOptions' => [ + 'jsonb' => true, + ] ]); $table->addColumn('access_circles', Types::JSON, [ - 'notnull' => true + 'notnull' => true, + 'customSchemaOptions' => [ + 'jsonb' => true, + ] ]); $table->addColumn('access_groups', Types::JSON, [ - 'notnull' => true + 'notnull' => true, + 'customSchemaOptions' => [ + 'jsonb' => true, + ] ]); $table->addColumn('access_links', Types::JSON, [ - 'notnull' => true + 'notnull' => true, + 'customSchemaOptions' => [ + 'jsonb' => true, + ] ]); $table->addColumn('tags', Types::JSON, [ - 'notnull' => true + 'notnull' => true, + 'customSchemaOptions' => [ + 'jsonb' => true, + ] ]); $table->addColumn('metadata', Types::JSON, [ - 'notnull' => true + 'notnull' => true, + 'customSchemaOptions' => [ + 'jsonb' => true, + ] ]); $table->addColumn('subtags', Types::JSON, [ - 'notnull' => true + 'notnull' => true, + 'customSchemaOptions' => [ + 'jsonb' => true, + ] ]); $table->addColumn('parts', Types::JSON, [ - 'notnull' => true + 'notnull' => true, + 'customSchemaOptions' => [ + 'jsonb' => true, + ] ]); $table->addColumn('link', Types::STRING, [ 'notnull' => true @@ -76,7 +104,7 @@ public function changeSchema(IOutput $output, \Closure $schemaClosure, array $op $table->addColumn('content', Types::TEXT, [ 'notnull' => true, 'customSchemaOptions' => [ - 'collation' => 'utf8mb4_unicode_ci' + 'collation' => $this->collations[$this->db->getDatabaseProvider()], ] ]); $table->setPrimaryKey(['id']);