From 1b5d477837c0d6432801a27a08dcfe790f8e006c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Szo=C5=82tysek?= Date: Thu, 2 Oct 2025 15:06:59 +0200 Subject: [PATCH 01/19] IBX-10494: Included Postgres 18 on CI --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 6e152f3d77..bb4bd4953b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -78,7 +78,7 @@ jobs: needs: tests services: postgres: - image: postgres:14 + image: postgres:18 ports: - 5432 env: From 01bbfc66dcb3eea474855adb590f831f3a3570bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Niedzielski?= Date: Mon, 6 Oct 2025 11:30:19 +0200 Subject: [PATCH 02/19] Fixed inconsistent test --- .../Repository/Filtering/BaseRepositoryFilteringTestCase.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/Core/Repository/Filtering/BaseRepositoryFilteringTestCase.php b/tests/integration/Core/Repository/Filtering/BaseRepositoryFilteringTestCase.php index 6118035a9a..ed3129cdcd 100644 --- a/tests/integration/Core/Repository/Filtering/BaseRepositoryFilteringTestCase.php +++ b/tests/integration/Core/Repository/Filtering/BaseRepositoryFilteringTestCase.php @@ -268,7 +268,7 @@ public function getCriteriaForInitialData(): iterable yield 'Sibling IN 2, 1]' => new Criterion\Sibling(2, 1); yield 'Subtree=/1/2/' => new Criterion\Subtree('/1/2/'); yield 'UserEmail=admin@link.invalid' => new Criterion\UserEmail('admin@link.invalid'); - yield 'UserEmail=admin@*' => new Criterion\UserEmail('*@link.invalid', Criterion\Operator::LIKE); + yield 'UserEmail=admin@*' => new Criterion\UserEmail('admin@*', Criterion\Operator::LIKE); yield 'UserId=14' => new Criterion\UserId(14); yield 'UserLogin=admin' => new Criterion\UserLogin('admin'); yield 'UserLogin=a*' => new Criterion\UserLogin('a*', Criterion\Operator::LIKE); From 30915f950109c5a5b0488a2ffbe6bd179b91343b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Szo=C5=82tysek?= Date: Mon, 6 Oct 2025 16:38:39 +0200 Subject: [PATCH 03/19] Postgres 14 & 18 setup --- .github/workflows/ci.yaml | 57 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 55 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index bb4bd4953b..1bd324c34a 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -73,8 +73,61 @@ jobs: - name: Run integration test suite run: composer run-script integration - integration-tests-postgres: - name: PostgreSQL integration tests + integration-tests-postgres-14: + name: PostgreSQL integration tests (14) + needs: tests + services: + postgres: + image: postgres:14 + ports: + - 5432 + env: + POSTGRES_PASSWORD: postgres + POSTGRES_DB: testdb + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + --tmpfs /var/lib/postgres + runs-on: "ubuntu-24.04" + timeout-minutes: 60 + + strategy: + fail-fast: false + matrix: + php: + - '7.4' + - '8.0' + - '8.1' + + steps: + - uses: actions/checkout@v2 + + - name: Setup PHP Action + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: none + extensions: pdo_pgsql, gd + tools: cs2pr + + - uses: "ramsey/composer-install@v1" + with: + dependency-versions: "highest" + + - name: Setup problem matchers for PHPUnit + run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" + + - name: Run integration test suite vs Postgresql + run: composer run-script integration + env: + DATABASE_URL: "pgsql://postgres:postgres@localhost:${{ job.services.postgres.ports[5432] }}/testdb?server_version=10" + # Required by old repository tests + DATABASE: "pgsql://postgres:postgres@localhost:${{ job.services.postgres.ports[5432] }}/testdb" + + integration-tests-postgres-18: + name: PostgreSQL integration tests (18) needs: tests services: postgres: From d6ead1c6965e490da9e0bfc68ffac3af23a18e4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Szo=C5=82tysek?= Date: Tue, 14 Oct 2025 11:09:21 +0200 Subject: [PATCH 04/19] Image in matrix for Postgres int tests --- .github/workflows/ci.yaml | 62 ++++----------------------------------- 1 file changed, 6 insertions(+), 56 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 1bd324c34a..1e994a6fed 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -73,12 +73,12 @@ jobs: - name: Run integration test suite run: composer run-script integration - integration-tests-postgres-14: - name: PostgreSQL integration tests (14) + integration-tests-postgres: + name: PostgreSQL integration tests needs: tests services: postgres: - image: postgres:14 + image: ${{ matrix.image }} ports: - 5432 env: @@ -100,6 +100,9 @@ jobs: - '7.4' - '8.0' - '8.1' + image: + - 'postgres:14' + - 'postgres:18' steps: - uses: actions/checkout@v2 @@ -126,59 +129,6 @@ jobs: # Required by old repository tests DATABASE: "pgsql://postgres:postgres@localhost:${{ job.services.postgres.ports[5432] }}/testdb" - integration-tests-postgres-18: - name: PostgreSQL integration tests (18) - needs: tests - services: - postgres: - image: postgres:18 - ports: - - 5432 - env: - POSTGRES_PASSWORD: postgres - POSTGRES_DB: testdb - options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 - --tmpfs /var/lib/postgres - runs-on: "ubuntu-24.04" - timeout-minutes: 60 - - strategy: - fail-fast: false - matrix: - php: - - '7.4' - - '8.0' - - '8.1' - - steps: - - uses: actions/checkout@v5 - - - name: Setup PHP Action - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php }} - coverage: none - extensions: pdo_pgsql, gd - tools: cs2pr - - - uses: ramsey/composer-install@v3 - with: - dependency-versions: "highest" - - - name: Setup problem matchers for PHPUnit - run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" - - - name: Run integration test suite vs Postgresql - run: composer run-script integration - env: - DATABASE_URL: "pgsql://postgres:postgres@localhost:${{ job.services.postgres.ports[5432] }}/testdb?server_version=10" - # Required by old repository tests - DATABASE: "pgsql://postgres:postgres@localhost:${{ job.services.postgres.ports[5432] }}/testdb" - integration-tests-mysql-80: name: MySQL integration tests (8.0) needs: tests From babf6e918b27147caea85b2d10dc8bb6453db68c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Szo=C5=82tysek?= Date: Tue, 14 Oct 2025 11:36:04 +0200 Subject: [PATCH 05/19] Image in matrix for MySQL int tests --- .github/workflows/ci.yaml | 63 ++++----------------------------------- 1 file changed, 6 insertions(+), 57 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 1e994a6fed..2af4bcf0d9 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -129,66 +129,12 @@ jobs: # Required by old repository tests DATABASE: "pgsql://postgres:postgres@localhost:${{ job.services.postgres.ports[5432] }}/testdb" - integration-tests-mysql-80: - name: MySQL integration tests (8.0) + integration-tests-mysql: + name: MySQL integration tests needs: tests services: mysql: - image: mysql:8.0 - ports: - - 3306/tcp - env: - MYSQL_RANDOM_ROOT_PASSWORD: true - MYSQL_USER: mysql - MYSQL_PASSWORD: mysql - MYSQL_DATABASE: testdb - options: >- - --health-cmd="mysqladmin ping" - --health-interval=10s - --health-timeout=5s - --health-retries=5 - --tmpfs=/var/lib/mysql - runs-on: "ubuntu-24.04" - timeout-minutes: 60 - - strategy: - fail-fast: false - matrix: - php: - - '7.4' - - '8.0' - - '8.1' - - steps: - - uses: actions/checkout@v5 - - - name: Setup PHP Action - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php }} - coverage: none - extensions: pdo_mysql, gd, redis - tools: cs2pr - - - uses: ramsey/composer-install@v3 - with: - dependency-versions: "highest" - - - name: Setup problem matchers for PHPUnit - run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" - - - name: Run integration test suite vs MySQL - run: composer run-script integration - env: - DATABASE_URL: "mysql://mysql:mysql@127.0.0.1:${{ job.services.mysql.ports[3306] }}/testdb" - DATABASE: "mysql://mysql:mysql@127.0.0.1:${{ job.services.mysql.ports[3306] }}/testdb" - - integration-tests-mysql-84: - name: MySQL integration tests (8.4) - needs: tests - services: - mysql: - image: mysql:8.4 + image: ${{ matrix.image }} ports: - 3306/tcp env: @@ -212,6 +158,9 @@ jobs: - '7.4' - '8.0' - '8.1' + image: + - 'mysql:8.0' + - 'mysql:8.4' steps: - uses: actions/checkout@v5 From 52c10b38edc75c6e7244e881943b44720306dffe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Szo=C5=82tysek?= Date: Tue, 14 Oct 2025 12:00:29 +0200 Subject: [PATCH 06/19] Dedicated action for installation of composer packages --- .github/workflows/ci.yaml | 98 ++++++++++++++++----------------------- 1 file changed, 39 insertions(+), 59 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 2af4bcf0d9..4b92058d09 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -18,17 +18,12 @@ jobs: steps: - uses: actions/checkout@v5 - - name: Setup PHP Action - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php }} - coverage: none - extensions: 'pdo_sqlite, gd' - tools: cs2pr - - - uses: ramsey/composer-install@v3 + - uses: ibexa/gh-workflows/actions/composer-install@main with: - dependency-versions: "highest" + gh-client-id: ${{ secrets.AUTOMATION_CLIENT_ID }} + gh-client-secret: ${{ secrets.AUTOMATION_CLIENT_SECRET }} + satis-network-key: ${{ secrets.SATIS_NETWORK_KEY }} + satis-network-token: ${{ secrets.SATIS_NETWORK_TOKEN }} - name: Run code style check run: composer run-script check-cs -- --format=checkstyle | cs2pr @@ -49,17 +44,12 @@ jobs: steps: - uses: actions/checkout@v5 - - name: Setup PHP Action - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php }} - coverage: none - extensions: pdo_sqlite, gd - tools: cs2pr - - - uses: ramsey/composer-install@v3 + - uses: ibexa/gh-workflows/actions/composer-install@main with: - dependency-versions: "highest" + gh-client-id: ${{ secrets.AUTOMATION_CLIENT_ID }} + gh-client-secret: ${{ secrets.AUTOMATION_CLIENT_SECRET }} + satis-network-key: ${{ secrets.SATIS_NETWORK_KEY }} + satis-network-token: ${{ secrets.SATIS_NETWORK_TOKEN }} - name: Setup problem matchers for PHPUnit run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" @@ -105,29 +95,24 @@ jobs: - 'postgres:18' steps: - - uses: actions/checkout@v2 - - - name: Setup PHP Action - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php }} - coverage: none - extensions: pdo_pgsql, gd - tools: cs2pr + - uses: actions/checkout@v5 - - uses: "ramsey/composer-install@v1" - with: - dependency-versions: "highest" + - uses: ibexa/gh-workflows/actions/composer-install@main + with: + gh-client-id: ${{ secrets.AUTOMATION_CLIENT_ID }} + gh-client-secret: ${{ secrets.AUTOMATION_CLIENT_SECRET }} + satis-network-key: ${{ secrets.SATIS_NETWORK_KEY }} + satis-network-token: ${{ secrets.SATIS_NETWORK_TOKEN }} - - name: Setup problem matchers for PHPUnit - run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" + - name: Setup problem matchers for PHPUnit + run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" - - name: Run integration test suite vs Postgresql - run: composer run-script integration - env: - DATABASE_URL: "pgsql://postgres:postgres@localhost:${{ job.services.postgres.ports[5432] }}/testdb?server_version=10" - # Required by old repository tests - DATABASE: "pgsql://postgres:postgres@localhost:${{ job.services.postgres.ports[5432] }}/testdb" + - name: Run integration test suite vs Postgresql + run: composer run-script integration + env: + DATABASE_URL: "pgsql://postgres:postgres@localhost:${{ job.services.postgres.ports[5432] }}/testdb?server_version=10" + # Required by old repository tests + DATABASE: "pgsql://postgres:postgres@localhost:${{ job.services.postgres.ports[5432] }}/testdb" integration-tests-mysql: name: MySQL integration tests @@ -163,28 +148,23 @@ jobs: - 'mysql:8.4' steps: - - uses: actions/checkout@v5 - - - name: Setup PHP Action - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php }} - coverage: none - extensions: pdo_mysql, gd, redis - tools: cs2pr + - uses: actions/checkout@v5 - - uses: ramsey/composer-install@v3 - with: - dependency-versions: "highest" + - uses: ibexa/gh-workflows/actions/composer-install@main + with: + gh-client-id: ${{ secrets.AUTOMATION_CLIENT_ID }} + gh-client-secret: ${{ secrets.AUTOMATION_CLIENT_SECRET }} + satis-network-key: ${{ secrets.SATIS_NETWORK_KEY }} + satis-network-token: ${{ secrets.SATIS_NETWORK_TOKEN }} - - name: Setup problem matchers for PHPUnit - run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" + - name: Setup problem matchers for PHPUnit + run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" - - name: Run integration test suite vs MySQL - run: composer run-script integration - env: - DATABASE_URL: "mysql://mysql:mysql@127.0.0.1:${{ job.services.mysql.ports[3306] }}/testdb" - DATABASE: "mysql://mysql:mysql@127.0.0.1:${{ job.services.mysql.ports[3306] }}/testdb" + - name: Run integration test suite vs MySQL + run: composer run-script integration + env: + DATABASE_URL: "mysql://mysql:mysql@127.0.0.1:${{ job.services.mysql.ports[3306] }}/testdb" + DATABASE: "mysql://mysql:mysql@127.0.0.1:${{ job.services.mysql.ports[3306] }}/testdb" solr-integration: name: "Solr integration tests" From 468a9aebc7a9d9fe715a22b09569bef968d7cdc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Szo=C5=82tysek?= Date: Tue, 14 Oct 2025 12:10:22 +0200 Subject: [PATCH 07/19] reformat code --- .github/workflows/ci.yaml | 438 +++++++++++++++++++------------------- 1 file changed, 219 insertions(+), 219 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 4b92058d09..4df1214d79 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1,224 +1,224 @@ name: CI on: - push: - branches: - - main - - '[0-9]+.[0-9]+' - pull_request: ~ + push: + branches: + - main + - '[0-9]+.[0-9]+' + pull_request: ~ jobs: - cs-fix: - name: Run code style check - runs-on: "ubuntu-24.04" - strategy: - matrix: - php: - - '8.1' - steps: - - uses: actions/checkout@v5 - - - uses: ibexa/gh-workflows/actions/composer-install@main - with: - gh-client-id: ${{ secrets.AUTOMATION_CLIENT_ID }} - gh-client-secret: ${{ secrets.AUTOMATION_CLIENT_SECRET }} - satis-network-key: ${{ secrets.SATIS_NETWORK_KEY }} - satis-network-token: ${{ secrets.SATIS_NETWORK_TOKEN }} - - - name: Run code style check - run: composer run-script check-cs -- --format=checkstyle | cs2pr - - tests: - name: Unit tests & SQLite integration tests - runs-on: "ubuntu-24.04" - timeout-minutes: 15 - - strategy: - fail-fast: false - matrix: - php: - - '7.4' - - '8.0' - - '8.1' - - steps: - - uses: actions/checkout@v5 - - - uses: ibexa/gh-workflows/actions/composer-install@main - with: - gh-client-id: ${{ secrets.AUTOMATION_CLIENT_ID }} - gh-client-secret: ${{ secrets.AUTOMATION_CLIENT_SECRET }} - satis-network-key: ${{ secrets.SATIS_NETWORK_KEY }} - satis-network-token: ${{ secrets.SATIS_NETWORK_TOKEN }} - - - name: Setup problem matchers for PHPUnit - run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" - - - name: Run PHPStan analysis - run: composer run-script phpstan - - - name: Run unit test suite - run: composer run-script unit - - - name: Run integration test suite - run: composer run-script integration - - integration-tests-postgres: - name: PostgreSQL integration tests - needs: tests - services: - postgres: - image: ${{ matrix.image }} - ports: - - 5432 - env: - POSTGRES_PASSWORD: postgres - POSTGRES_DB: testdb - options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 - --tmpfs /var/lib/postgres - runs-on: "ubuntu-24.04" - timeout-minutes: 60 - - strategy: - fail-fast: false - matrix: - php: - - '7.4' - - '8.0' - - '8.1' - image: - - 'postgres:14' - - 'postgres:18' - - steps: - - uses: actions/checkout@v5 - - - uses: ibexa/gh-workflows/actions/composer-install@main - with: - gh-client-id: ${{ secrets.AUTOMATION_CLIENT_ID }} - gh-client-secret: ${{ secrets.AUTOMATION_CLIENT_SECRET }} - satis-network-key: ${{ secrets.SATIS_NETWORK_KEY }} - satis-network-token: ${{ secrets.SATIS_NETWORK_TOKEN }} - - - name: Setup problem matchers for PHPUnit - run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" - - - name: Run integration test suite vs Postgresql - run: composer run-script integration - env: - DATABASE_URL: "pgsql://postgres:postgres@localhost:${{ job.services.postgres.ports[5432] }}/testdb?server_version=10" - # Required by old repository tests - DATABASE: "pgsql://postgres:postgres@localhost:${{ job.services.postgres.ports[5432] }}/testdb" - - integration-tests-mysql: - name: MySQL integration tests - needs: tests - services: - mysql: - image: ${{ matrix.image }} - ports: - - 3306/tcp - env: - MYSQL_RANDOM_ROOT_PASSWORD: true - MYSQL_USER: mysql - MYSQL_PASSWORD: mysql - MYSQL_DATABASE: testdb - options: >- - --health-cmd="mysqladmin ping" - --health-interval=10s - --health-timeout=5s - --health-retries=5 - --tmpfs=/var/lib/mysql - runs-on: "ubuntu-24.04" - timeout-minutes: 60 - - strategy: - fail-fast: false - matrix: - php: - - '7.4' - - '8.0' - - '8.1' - image: - - 'mysql:8.0' - - 'mysql:8.4' - - steps: - - uses: actions/checkout@v5 - - - uses: ibexa/gh-workflows/actions/composer-install@main - with: - gh-client-id: ${{ secrets.AUTOMATION_CLIENT_ID }} - gh-client-secret: ${{ secrets.AUTOMATION_CLIENT_SECRET }} - satis-network-key: ${{ secrets.SATIS_NETWORK_KEY }} - satis-network-token: ${{ secrets.SATIS_NETWORK_TOKEN }} - - - name: Setup problem matchers for PHPUnit - run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" - - - name: Run integration test suite vs MySQL - run: composer run-script integration - env: - DATABASE_URL: "mysql://mysql:mysql@127.0.0.1:${{ job.services.mysql.ports[3306] }}/testdb" - DATABASE: "mysql://mysql:mysql@127.0.0.1:${{ job.services.mysql.ports[3306] }}/testdb" - - solr-integration: - name: "Solr integration tests" - runs-on: "ubuntu-24.04" - timeout-minutes: 30 - permissions: - packages: read - contents: read - services: - redis: - image: redis - ports: - - 6379:6379 - options: - --memory=60m - solr: - image: ghcr.io/ibexa/core/solr - ports: - - 8983:8983 - options: >- - --health-cmd "solr status" - --health-interval 10s - --health-timeout 5s - --health-retries 10 - strategy: - fail-fast: false - matrix: - php: - - '7.4' - - '8.0' - - '8.1' - steps: - - uses: actions/checkout@v5 - with: - fetch-depth: 0 - - - name: Setup PHP Action - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php }} - coverage: none - - - name: Add solr dependency - run: | - VERSION=$(jq -r '.extra | ."branch-alias" | ."dev-main"' < composer.json) - composer require --no-update "ibexa/solr:$VERSION" - - - uses: ramsey/composer-install@v3 - with: - dependency-versions: "highest" - - - name: Run integration test suite - run: composer test-integration-solr - env: - CUSTOM_CACHE_POOL: singleredis - CACHE_HOST: 127.0.0.1 - CORES_SETUP: single + cs-fix: + name: Run code style check + runs-on: "ubuntu-24.04" + strategy: + matrix: + php: + - '8.1' + steps: + - uses: actions/checkout@v5 + + - uses: ibexa/gh-workflows/actions/composer-install@main + with: + gh-client-id: ${{ secrets.AUTOMATION_CLIENT_ID }} + gh-client-secret: ${{ secrets.AUTOMATION_CLIENT_SECRET }} + satis-network-key: ${{ secrets.SATIS_NETWORK_KEY }} + satis-network-token: ${{ secrets.SATIS_NETWORK_TOKEN }} + + - name: Run code style check + run: composer run-script check-cs -- --format=checkstyle | cs2pr + + tests: + name: Unit tests & SQLite integration tests + runs-on: "ubuntu-24.04" + timeout-minutes: 15 + + strategy: + fail-fast: false + matrix: + php: + - '7.4' + - '8.0' + - '8.1' + + steps: + - uses: actions/checkout@v5 + + - uses: ibexa/gh-workflows/actions/composer-install@main + with: + gh-client-id: ${{ secrets.AUTOMATION_CLIENT_ID }} + gh-client-secret: ${{ secrets.AUTOMATION_CLIENT_SECRET }} + satis-network-key: ${{ secrets.SATIS_NETWORK_KEY }} + satis-network-token: ${{ secrets.SATIS_NETWORK_TOKEN }} + + - name: Setup problem matchers for PHPUnit + run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" + + - name: Run PHPStan analysis + run: composer run-script phpstan + + - name: Run unit test suite + run: composer run-script unit + + - name: Run integration test suite + run: composer run-script integration + + integration-tests-postgres: + name: PostgreSQL integration tests + needs: tests + services: + postgres: + image: ${{ matrix.image }} + ports: + - 5432 + env: + POSTGRES_PASSWORD: postgres + POSTGRES_DB: testdb + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + --tmpfs /var/lib/postgres + runs-on: "ubuntu-24.04" + timeout-minutes: 60 + + strategy: + fail-fast: false + matrix: + php: + - '7.4' + - '8.0' + - '8.1' + image: + - 'postgres:14' + - 'postgres:18' + + steps: + - uses: actions/checkout@v5 + + - uses: ibexa/gh-workflows/actions/composer-install@main + with: + gh-client-id: ${{ secrets.AUTOMATION_CLIENT_ID }} + gh-client-secret: ${{ secrets.AUTOMATION_CLIENT_SECRET }} + satis-network-key: ${{ secrets.SATIS_NETWORK_KEY }} + satis-network-token: ${{ secrets.SATIS_NETWORK_TOKEN }} + + - name: Setup problem matchers for PHPUnit + run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" + + - name: Run integration test suite vs Postgresql + run: composer run-script integration + env: + DATABASE_URL: "pgsql://postgres:postgres@localhost:${{ job.services.postgres.ports[5432] }}/testdb?server_version=10" + # Required by old repository tests + DATABASE: "pgsql://postgres:postgres@localhost:${{ job.services.postgres.ports[5432] }}/testdb" + + integration-tests-mysql: + name: MySQL integration tests + needs: tests + services: + mysql: + image: ${{ matrix.image }} + ports: + - 3306/tcp + env: + MYSQL_RANDOM_ROOT_PASSWORD: true + MYSQL_USER: mysql + MYSQL_PASSWORD: mysql + MYSQL_DATABASE: testdb + options: >- + --health-cmd="mysqladmin ping" + --health-interval=10s + --health-timeout=5s + --health-retries=5 + --tmpfs=/var/lib/mysql + runs-on: "ubuntu-24.04" + timeout-minutes: 60 + + strategy: + fail-fast: false + matrix: + php: + - '7.4' + - '8.0' + - '8.1' + image: + - 'mysql:8.0' + - 'mysql:8.4' + + steps: + - uses: actions/checkout@v5 + + - uses: ibexa/gh-workflows/actions/composer-install@main + with: + gh-client-id: ${{ secrets.AUTOMATION_CLIENT_ID }} + gh-client-secret: ${{ secrets.AUTOMATION_CLIENT_SECRET }} + satis-network-key: ${{ secrets.SATIS_NETWORK_KEY }} + satis-network-token: ${{ secrets.SATIS_NETWORK_TOKEN }} + + - name: Setup problem matchers for PHPUnit + run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" + + - name: Run integration test suite vs MySQL + run: composer run-script integration + env: + DATABASE_URL: "mysql://mysql:mysql@127.0.0.1:${{ job.services.mysql.ports[3306] }}/testdb" + DATABASE: "mysql://mysql:mysql@127.0.0.1:${{ job.services.mysql.ports[3306] }}/testdb" + + solr-integration: + name: "Solr integration tests" + runs-on: "ubuntu-24.04" + timeout-minutes: 30 + permissions: + packages: read + contents: read + services: + redis: + image: redis + ports: + - 6379:6379 + options: + --memory=60m + solr: + image: ghcr.io/ibexa/core/solr + ports: + - 8983:8983 + options: >- + --health-cmd "solr status" + --health-interval 10s + --health-timeout 5s + --health-retries 10 + strategy: + fail-fast: false + matrix: + php: + - '7.4' + - '8.0' + - '8.1' + steps: + - uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - name: Setup PHP Action + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: none + + - name: Add solr dependency + run: | + VERSION=$(jq -r '.extra | ."branch-alias" | ."dev-main"' < composer.json) + composer require --no-update "ibexa/solr:$VERSION" + + - uses: ramsey/composer-install@v3 + with: + dependency-versions: "highest" + + - name: Run integration test suite + run: composer test-integration-solr + env: + CUSTOM_CACHE_POOL: singleredis + CACHE_HOST: 127.0.0.1 + CORES_SETUP: single From 6fe61677357741b9185fc2754d8ab3fbc4a7b884 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Szo=C5=82tysek?= Date: Tue, 14 Oct 2025 12:20:35 +0200 Subject: [PATCH 08/19] fixup! reformat code --- .github/workflows/ci.yaml | 438 +++++++++++++++++++------------------- 1 file changed, 219 insertions(+), 219 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 4df1214d79..98ab313e7a 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1,224 +1,224 @@ name: CI on: - push: - branches: - - main - - '[0-9]+.[0-9]+' - pull_request: ~ + push: + branches: + - main + - '[0-9]+.[0-9]+' + pull_request: ~ jobs: - cs-fix: - name: Run code style check - runs-on: "ubuntu-24.04" - strategy: - matrix: - php: - - '8.1' - steps: - - uses: actions/checkout@v5 - - - uses: ibexa/gh-workflows/actions/composer-install@main - with: - gh-client-id: ${{ secrets.AUTOMATION_CLIENT_ID }} - gh-client-secret: ${{ secrets.AUTOMATION_CLIENT_SECRET }} - satis-network-key: ${{ secrets.SATIS_NETWORK_KEY }} - satis-network-token: ${{ secrets.SATIS_NETWORK_TOKEN }} - - - name: Run code style check - run: composer run-script check-cs -- --format=checkstyle | cs2pr - - tests: - name: Unit tests & SQLite integration tests - runs-on: "ubuntu-24.04" - timeout-minutes: 15 - - strategy: - fail-fast: false - matrix: - php: - - '7.4' - - '8.0' - - '8.1' - - steps: - - uses: actions/checkout@v5 - - - uses: ibexa/gh-workflows/actions/composer-install@main - with: - gh-client-id: ${{ secrets.AUTOMATION_CLIENT_ID }} - gh-client-secret: ${{ secrets.AUTOMATION_CLIENT_SECRET }} - satis-network-key: ${{ secrets.SATIS_NETWORK_KEY }} - satis-network-token: ${{ secrets.SATIS_NETWORK_TOKEN }} - - - name: Setup problem matchers for PHPUnit - run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" - - - name: Run PHPStan analysis - run: composer run-script phpstan - - - name: Run unit test suite - run: composer run-script unit - - - name: Run integration test suite - run: composer run-script integration - - integration-tests-postgres: - name: PostgreSQL integration tests - needs: tests - services: - postgres: - image: ${{ matrix.image }} - ports: - - 5432 - env: - POSTGRES_PASSWORD: postgres - POSTGRES_DB: testdb - options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 - --tmpfs /var/lib/postgres - runs-on: "ubuntu-24.04" - timeout-minutes: 60 - - strategy: - fail-fast: false - matrix: - php: - - '7.4' - - '8.0' - - '8.1' - image: - - 'postgres:14' - - 'postgres:18' - - steps: - - uses: actions/checkout@v5 - - - uses: ibexa/gh-workflows/actions/composer-install@main - with: - gh-client-id: ${{ secrets.AUTOMATION_CLIENT_ID }} - gh-client-secret: ${{ secrets.AUTOMATION_CLIENT_SECRET }} - satis-network-key: ${{ secrets.SATIS_NETWORK_KEY }} - satis-network-token: ${{ secrets.SATIS_NETWORK_TOKEN }} - - - name: Setup problem matchers for PHPUnit - run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" - - - name: Run integration test suite vs Postgresql - run: composer run-script integration - env: - DATABASE_URL: "pgsql://postgres:postgres@localhost:${{ job.services.postgres.ports[5432] }}/testdb?server_version=10" - # Required by old repository tests - DATABASE: "pgsql://postgres:postgres@localhost:${{ job.services.postgres.ports[5432] }}/testdb" - - integration-tests-mysql: - name: MySQL integration tests - needs: tests - services: - mysql: - image: ${{ matrix.image }} - ports: - - 3306/tcp - env: - MYSQL_RANDOM_ROOT_PASSWORD: true - MYSQL_USER: mysql - MYSQL_PASSWORD: mysql - MYSQL_DATABASE: testdb - options: >- - --health-cmd="mysqladmin ping" - --health-interval=10s - --health-timeout=5s - --health-retries=5 - --tmpfs=/var/lib/mysql - runs-on: "ubuntu-24.04" - timeout-minutes: 60 - - strategy: - fail-fast: false - matrix: - php: - - '7.4' - - '8.0' - - '8.1' - image: - - 'mysql:8.0' - - 'mysql:8.4' - - steps: - - uses: actions/checkout@v5 - - - uses: ibexa/gh-workflows/actions/composer-install@main - with: - gh-client-id: ${{ secrets.AUTOMATION_CLIENT_ID }} - gh-client-secret: ${{ secrets.AUTOMATION_CLIENT_SECRET }} - satis-network-key: ${{ secrets.SATIS_NETWORK_KEY }} - satis-network-token: ${{ secrets.SATIS_NETWORK_TOKEN }} - - - name: Setup problem matchers for PHPUnit - run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" - - - name: Run integration test suite vs MySQL - run: composer run-script integration - env: - DATABASE_URL: "mysql://mysql:mysql@127.0.0.1:${{ job.services.mysql.ports[3306] }}/testdb" - DATABASE: "mysql://mysql:mysql@127.0.0.1:${{ job.services.mysql.ports[3306] }}/testdb" - - solr-integration: - name: "Solr integration tests" - runs-on: "ubuntu-24.04" - timeout-minutes: 30 - permissions: - packages: read - contents: read - services: - redis: - image: redis - ports: - - 6379:6379 - options: - --memory=60m - solr: - image: ghcr.io/ibexa/core/solr - ports: - - 8983:8983 - options: >- - --health-cmd "solr status" - --health-interval 10s - --health-timeout 5s - --health-retries 10 - strategy: - fail-fast: false - matrix: - php: - - '7.4' - - '8.0' - - '8.1' - steps: - - uses: actions/checkout@v5 - with: - fetch-depth: 0 - - - name: Setup PHP Action - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php }} - coverage: none - - - name: Add solr dependency - run: | - VERSION=$(jq -r '.extra | ."branch-alias" | ."dev-main"' < composer.json) - composer require --no-update "ibexa/solr:$VERSION" - - - uses: ramsey/composer-install@v3 - with: - dependency-versions: "highest" - - - name: Run integration test suite - run: composer test-integration-solr - env: - CUSTOM_CACHE_POOL: singleredis - CACHE_HOST: 127.0.0.1 - CORES_SETUP: single + cs-fix: + name: Run code style check + runs-on: "ubuntu-24.04" + strategy: + matrix: + php: + - '8.1' + steps: + - uses: actions/checkout@v5 + + - uses: ibexa/gh-workflows/actions/composer-install@main + with: + gh-client-id: ${{ secrets.AUTOMATION_CLIENT_ID }} + gh-client-secret: ${{ secrets.AUTOMATION_CLIENT_SECRET }} + satis-network-key: ${{ secrets.SATIS_NETWORK_KEY }} + satis-network-token: ${{ secrets.SATIS_NETWORK_TOKEN }} + + - name: Run code style check + run: composer run-script check-cs -- --format=checkstyle | cs2pr + + tests: + name: Unit tests & SQLite integration tests + runs-on: "ubuntu-24.04" + timeout-minutes: 15 + + strategy: + fail-fast: false + matrix: + php: + - '7.4' + - '8.0' + - '8.1' + + steps: + - uses: actions/checkout@v5 + + - uses: ibexa/gh-workflows/actions/composer-install@main + with: + gh-client-id: ${{ secrets.AUTOMATION_CLIENT_ID }} + gh-client-secret: ${{ secrets.AUTOMATION_CLIENT_SECRET }} + satis-network-key: ${{ secrets.SATIS_NETWORK_KEY }} + satis-network-token: ${{ secrets.SATIS_NETWORK_TOKEN }} + + - name: Setup problem matchers for PHPUnit + run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" + + - name: Run PHPStan analysis + run: composer run-script phpstan + + - name: Run unit test suite + run: composer run-script unit + + - name: Run integration test suite + run: composer run-script integration + + integration-tests-postgres: + name: PostgreSQL integration tests + needs: tests + services: + postgres: + image: ${{ matrix.image }} + ports: + - 5432 + env: + POSTGRES_PASSWORD: postgres + POSTGRES_DB: testdb + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + --tmpfs /var/lib/postgres + runs-on: "ubuntu-24.04" + timeout-minutes: 60 + + strategy: + fail-fast: false + matrix: + php: + - '7.4' + - '8.0' + - '8.1' + image: + - 'postgres:14' + - 'postgres:18' + + steps: + - uses: actions/checkout@v5 + + - uses: ibexa/gh-workflows/actions/composer-install@main + with: + gh-client-id: ${{ secrets.AUTOMATION_CLIENT_ID }} + gh-client-secret: ${{ secrets.AUTOMATION_CLIENT_SECRET }} + satis-network-key: ${{ secrets.SATIS_NETWORK_KEY }} + satis-network-token: ${{ secrets.SATIS_NETWORK_TOKEN }} + + - name: Setup problem matchers for PHPUnit + run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" + + - name: Run integration test suite vs Postgresql + run: composer run-script integration + env: + DATABASE_URL: "pgsql://postgres:postgres@localhost:${{ job.services.postgres.ports[5432] }}/testdb?server_version=10" + # Required by old repository tests + DATABASE: "pgsql://postgres:postgres@localhost:${{ job.services.postgres.ports[5432] }}/testdb" + + integration-tests-mysql: + name: MySQL integration tests + needs: tests + services: + mysql: + image: ${{ matrix.image }} + ports: + - 3306/tcp + env: + MYSQL_RANDOM_ROOT_PASSWORD: true + MYSQL_USER: mysql + MYSQL_PASSWORD: mysql + MYSQL_DATABASE: testdb + options: >- + --health-cmd="mysqladmin ping" + --health-interval=10s + --health-timeout=5s + --health-retries=5 + --tmpfs=/var/lib/mysql + runs-on: "ubuntu-24.04" + timeout-minutes: 60 + + strategy: + fail-fast: false + matrix: + php: + - '7.4' + - '8.0' + - '8.1' + image: + - 'mysql:8.0' + - 'mysql:8.4' + + steps: + - uses: actions/checkout@v5 + + - uses: ibexa/gh-workflows/actions/composer-install@main + with: + gh-client-id: ${{ secrets.AUTOMATION_CLIENT_ID }} + gh-client-secret: ${{ secrets.AUTOMATION_CLIENT_SECRET }} + satis-network-key: ${{ secrets.SATIS_NETWORK_KEY }} + satis-network-token: ${{ secrets.SATIS_NETWORK_TOKEN }} + + - name: Setup problem matchers for PHPUnit + run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" + + - name: Run integration test suite vs MySQL + run: composer run-script integration + env: + DATABASE_URL: "mysql://mysql:mysql@127.0.0.1:${{ job.services.mysql.ports[3306] }}/testdb" + DATABASE: "mysql://mysql:mysql@127.0.0.1:${{ job.services.mysql.ports[3306] }}/testdb" + + solr-integration: + name: "Solr integration tests" + runs-on: "ubuntu-24.04" + timeout-minutes: 30 + permissions: + packages: read + contents: read + services: + redis: + image: redis + ports: + - 6379:6379 + options: + --memory=60m + solr: + image: ghcr.io/ibexa/core/solr + ports: + - 8983:8983 + options: >- + --health-cmd "solr status" + --health-interval 10s + --health-timeout 5s + --health-retries 10 + strategy: + fail-fast: false + matrix: + php: + - '7.4' + - '8.0' + - '8.1' + steps: + - uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - name: Setup PHP Action + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: none + + - name: Add solr dependency + run: | + VERSION=$(jq -r '.extra | ."branch-alias" | ."dev-main"' < composer.json) + composer require --no-update "ibexa/solr:$VERSION" + + - uses: ramsey/composer-install@v3 + with: + dependency-versions: "highest" + + - name: Run integration test suite + run: composer test-integration-solr + env: + CUSTOM_CACHE_POOL: singleredis + CACHE_HOST: 127.0.0.1 + CORES_SETUP: single From 8df9df85c434d65c2486bbe8dfc82b5d2ae693cd Mon Sep 17 00:00:00 2001 From: mikolaj Date: Thu, 20 Nov 2025 15:39:23 +0100 Subject: [PATCH 09/19] Refactored location sorting logic in query builders and enhance filtering tests --- .../BaseLocationSortClauseQueryBuilder.php | 60 ++++++- .../Location/DepthQueryBuilder.php | 4 +- .../Location/IdQueryBuilder.php | 4 +- .../Location/PathQueryBuilder.php | 4 +- .../Location/PriorityQueryBuilder.php | 4 +- .../Location/VisibilityQueryBuilder.php | 4 +- .../BaseRepositoryFilteringTestCase.php | 2 +- .../Filtering/ContentFilteringTest.php | 155 ++++++++++++++++++ 8 files changed, 221 insertions(+), 16 deletions(-) diff --git a/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Location/BaseLocationSortClauseQueryBuilder.php b/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Location/BaseLocationSortClauseQueryBuilder.php index 25f09c0596..7795a62c9c 100644 --- a/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Location/BaseLocationSortClauseQueryBuilder.php +++ b/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Location/BaseLocationSortClauseQueryBuilder.php @@ -11,26 +11,76 @@ use Ibexa\Contracts\Core\Persistence\Filter\Doctrine\FilteringQueryBuilder; use Ibexa\Contracts\Core\Repository\Values\Filter\FilteringSortClause; use Ibexa\Contracts\Core\Repository\Values\Filter\SortClauseQueryBuilder; +use Ibexa\Core\Persistence\Legacy\Content\Location\Gateway as LocationGateway; /** * @internal */ abstract class BaseLocationSortClauseQueryBuilder implements SortClauseQueryBuilder { + private const CONTENT_SORT_LOCATION_ALIAS = 'ibexa_sort_location'; + + private string $locationAlias = self::CONTENT_SORT_LOCATION_ALIAS; + + private bool $needsMainLocationJoin = true; + public function buildQuery( FilteringQueryBuilder $queryBuilder, FilteringSortClause $sortClause ): void { - $sort = $this->getSortingExpression(); - $queryBuilder - ->addSelect($this->getSortingExpression()) - ->joinAllLocations(); + $this->prepareLocationAlias($queryBuilder); + + $sort = $this->getSortingExpression($this->locationAlias); + $queryBuilder->addSelect($sort); + + if ($this->needsMainLocationJoin) { + $this->joinMainLocationOnly($queryBuilder, $this->locationAlias); + } /** @var \Ibexa\Contracts\Core\Repository\Values\Content\Query\SortClause $sortClause */ $queryBuilder->addOrderBy($sort, $sortClause->direction); } - abstract protected function getSortingExpression(): string; + private function prepareLocationAlias(FilteringQueryBuilder $queryBuilder): void + { + if ($this->isLocationFilteringContext($queryBuilder)) { + $queryBuilder->joinAllLocations(); + $this->locationAlias = 'location'; + $this->needsMainLocationJoin = false; + + return; + } + + $this->locationAlias = self::CONTENT_SORT_LOCATION_ALIAS; + $this->needsMainLocationJoin = true; + } + + private function isLocationFilteringContext(FilteringQueryBuilder $queryBuilder): bool + { + $fromParts = $queryBuilder->getQueryPart('from'); + foreach ($fromParts as $fromPart) { + if (($fromPart['alias'] ?? null) === 'location') { + return true; + } + } + + return false; + } + + private function joinMainLocationOnly(FilteringQueryBuilder $queryBuilder, string $alias): void + { + $queryBuilder->joinOnce( + 'content', + LocationGateway::CONTENT_TREE_TABLE, + $alias, + (string)$queryBuilder->expr()->andX( + sprintf('content.id = %s.contentobject_id', $alias), + sprintf('%s.node_id = %s.main_node_id', $alias, $alias) + ) + ); + } + + abstract protected function getSortingExpression(string $locationAlias): string; } class_alias(BaseLocationSortClauseQueryBuilder::class, 'eZ\Publish\Core\Persistence\Legacy\Filter\SortClauseQueryBuilder\Location\BaseLocationSortClauseQueryBuilder'); diff --git a/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Location/DepthQueryBuilder.php b/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Location/DepthQueryBuilder.php index 2f862abc37..c7e98bad1d 100644 --- a/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Location/DepthQueryBuilder.php +++ b/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Location/DepthQueryBuilder.php @@ -21,9 +21,9 @@ public function accepts(FilteringSortClause $sortClause): bool return $sortClause instanceof Location\Depth; } - protected function getSortingExpression(): string + protected function getSortingExpression(string $locationAlias): string { - return 'location.depth'; + return sprintf('%s.depth', $locationAlias); } } diff --git a/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Location/IdQueryBuilder.php b/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Location/IdQueryBuilder.php index a9651bddb5..c946498abe 100644 --- a/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Location/IdQueryBuilder.php +++ b/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Location/IdQueryBuilder.php @@ -21,9 +21,9 @@ public function accepts(FilteringSortClause $sortClause): bool return $sortClause instanceof Location\Id; } - protected function getSortingExpression(): string + protected function getSortingExpression(string $locationAlias): string { - return 'location.node_id'; + return sprintf('%s.node_id', $locationAlias); } } diff --git a/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Location/PathQueryBuilder.php b/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Location/PathQueryBuilder.php index 039951e0c0..0dac771d79 100644 --- a/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Location/PathQueryBuilder.php +++ b/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Location/PathQueryBuilder.php @@ -18,9 +18,9 @@ public function accepts(FilteringSortClause $sortClause): bool return $sortClause instanceof Location\Path; } - protected function getSortingExpression(): string + protected function getSortingExpression(string $locationAlias): string { - return 'location.path_string'; + return sprintf('%s.path_string', $locationAlias); } } diff --git a/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Location/PriorityQueryBuilder.php b/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Location/PriorityQueryBuilder.php index 543f64d816..0d1e19164b 100644 --- a/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Location/PriorityQueryBuilder.php +++ b/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Location/PriorityQueryBuilder.php @@ -18,9 +18,9 @@ public function accepts(FilteringSortClause $sortClause): bool return $sortClause instanceof Location\Priority; } - protected function getSortingExpression(): string + protected function getSortingExpression(string $locationAlias): string { - return 'location.priority'; + return sprintf('%s.priority', $locationAlias); } } diff --git a/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Location/VisibilityQueryBuilder.php b/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Location/VisibilityQueryBuilder.php index 3877761668..a8659e75de 100644 --- a/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Location/VisibilityQueryBuilder.php +++ b/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Location/VisibilityQueryBuilder.php @@ -18,9 +18,9 @@ public function accepts(FilteringSortClause $sortClause): bool return $sortClause instanceof Location\Visibility; } - protected function getSortingExpression(): string + protected function getSortingExpression(string $locationAlias): string { - return 'location.is_invisible'; + return sprintf('%s.is_invisible', $locationAlias); } } diff --git a/tests/integration/Core/Repository/Filtering/BaseRepositoryFilteringTestCase.php b/tests/integration/Core/Repository/Filtering/BaseRepositoryFilteringTestCase.php index ed3129cdcd..9c6f929641 100644 --- a/tests/integration/Core/Repository/Filtering/BaseRepositoryFilteringTestCase.php +++ b/tests/integration/Core/Repository/Filtering/BaseRepositoryFilteringTestCase.php @@ -75,7 +75,7 @@ public function testFind(callable $filterFactory): void protected function setUp(): void { parent::setUp(); - $this->contentProvider = new TestContentProvider($this->getRepository(false), $this); + $this->contentProvider = new TestContentProvider($this->getRepository(true), $this); } /** diff --git a/tests/integration/Core/Repository/Filtering/ContentFilteringTest.php b/tests/integration/Core/Repository/Filtering/ContentFilteringTest.php index 5a26a80163..df921f25c2 100644 --- a/tests/integration/Core/Repository/Filtering/ContentFilteringTest.php +++ b/tests/integration/Core/Repository/Filtering/ContentFilteringTest.php @@ -10,8 +10,10 @@ use function array_map; use function count; +use Ibexa\Contracts\Core\Repository\LocationService; use Ibexa\Contracts\Core\Repository\Values\Content\Content; use Ibexa\Contracts\Core\Repository\Values\Content\ContentList; +use Ibexa\Contracts\Core\Repository\Values\Content\Location; use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; use Ibexa\Contracts\Core\Repository\Values\Content\Query\SortClause; @@ -22,6 +24,7 @@ use Ibexa\Contracts\Core\Repository\Values\ObjectState\ObjectStateGroupCreateStruct; use Ibexa\Core\FieldType\Keyword; use Ibexa\Tests\Core\Repository\Filtering\TestContentProvider; +use function iterator_to_array; use IteratorAggregate; use function sprintf; @@ -81,6 +84,150 @@ public function testFindWithLocationSortClauses(): void ); } + public function testLocationSortClausesUseMainLocationDuringContentFiltering(): void + { + $repository = $this->getRepository(false); + $locationService = $repository->getLocationService(); + $contentService = $repository->getContentService(); + + $shallowParent = $this->createFolder( + ['eng-GB' => 'Shallow Parent'], + 2 + ); + $referenceContent = $this->createFolder( + ['eng-GB' => 'Reference folder'], + $shallowParent->contentInfo->mainLocationId + ); + $deepParent = $this->createFolder( + ['eng-GB' => 'Deep Parent'], + $referenceContent->contentInfo->mainLocationId + ); + $contentWithAdditionalLocation = $this->createFolder( + ['eng-GB' => 'Folder with extra location'], + $deepParent->contentInfo->mainLocationId + ); + $locationService->createLocation( + $contentWithAdditionalLocation->contentInfo, + $locationService->newLocationCreateStruct(2) + ); + + $mainLocation = $this->loadMainLocation($locationService, $contentWithAdditionalLocation); + $nonMainLocations = []; + foreach ($locationService->loadLocations($contentWithAdditionalLocation->contentInfo) as $location) { + if ($location->id !== $contentWithAdditionalLocation->contentInfo->mainLocationId) { + $nonMainLocations[] = $location; + } + } + self::assertNotEmpty($nonMainLocations); + $nonMainLocation = $nonMainLocations[0]; + $referenceLocation = $this->loadMainLocation($locationService, $referenceContent); + + self::assertLessThan($referenceLocation->depth, $nonMainLocation->depth); + self::assertLessThan($mainLocation->depth, $referenceLocation->depth); + + $filter = (new Filter()) + ->withCriterion( + new Criterion\ContentId( + [ + $contentWithAdditionalLocation->id, + $referenceContent->id, + ] + ) + ) + ->withSortClause(new SortClause\Location\Depth(Query::SORT_ASC)); + + $contentList = $contentService->find($filter); + + self::assertSame( + [$referenceContent->id, $contentWithAdditionalLocation->id], + array_map( + static function (Content $content): int { + return $content->id; + }, + iterator_to_array($contentList) + ) + ); + } + + public function testLocationSortClausesStayDeterministicWithComplexCriteria(): void + { + $repository = $this->getRepository(false); + $locationService = $repository->getLocationService(); + $contentService = $repository->getContentService(); + + $shallowParent = $this->createFolder( + ['eng-GB' => 'Complex Root'], + 2 + ); + $referenceContent = $this->createFolder( + ['eng-GB' => 'Ref folder'], + $shallowParent->contentInfo->mainLocationId + ); + $middleContent = $this->createFolder( + ['eng-GB' => 'Middle folder'], + $referenceContent->contentInfo->mainLocationId + ); + $deepParent = $this->createFolder( + ['eng-GB' => 'Deep intermediate'], + $middleContent->contentInfo->mainLocationId + ); + $contentWithAdditionalLocation = $this->createFolder( + ['eng-GB' => 'Folder with randomizing location'], + $deepParent->contentInfo->mainLocationId + ); + $locationService->createLocation( + $contentWithAdditionalLocation->contentInfo, + $locationService->newLocationCreateStruct(2) + ); + + $referenceLocation = $this->loadMainLocation($locationService, $referenceContent); + $middleLocation = $this->loadMainLocation($locationService, $middleContent); + $mainLocation = $this->loadMainLocation($locationService, $contentWithAdditionalLocation); + $nonMainLocations = []; + foreach ($locationService->loadLocations($contentWithAdditionalLocation->contentInfo) as $location) { + if ($location->id !== $contentWithAdditionalLocation->contentInfo->mainLocationId) { + $nonMainLocations[] = $location; + } + } + self::assertNotEmpty($nonMainLocations); + $nonMainLocation = $nonMainLocations[0]; + self::assertNotEquals($mainLocation->depth, $nonMainLocation->depth); + + $shallowParentLocation = $this->loadMainLocation($locationService, $shallowParent); + + $filter = (new Filter()) + ->withCriterion(new Criterion\Subtree($shallowParentLocation->pathString)) + ->andWithCriterion(new Criterion\ContentTypeIdentifier('folder')) + ->andWithCriterion( + new Criterion\ContentId( + [ + $referenceContent->id, + $middleContent->id, + $contentWithAdditionalLocation->id, + ] + ) + ) + ->withSortClause(new SortClause\Location\Depth(Query::SORT_ASC)) + ->withSortClause(new SortClause\ContentId(Query::SORT_ASC)) + ->withLimit(10); + + $contentList = $contentService->find($filter); + + self::assertSame( + [ + $referenceContent->id, + $middleContent->id, + $contentWithAdditionalLocation->id, + ], + array_map( + static function (Content $content): int { + return $content->id; + }, + iterator_to_array($contentList) + ) + ); + } + /** * @throws \Ibexa\Contracts\Core\Repository\Exceptions\InvalidArgumentException * @throws \Exception @@ -447,6 +594,14 @@ private function buildSearchQueryFromFilter(Filter $filter): Query ] ); } + + private function loadMainLocation(LocationService $locationService, Content $content): Location + { + $mainLocationId = $content->contentInfo->mainLocationId; + self::assertNotNull($mainLocationId); + + return $locationService->loadLocation($mainLocationId); + } } class_alias(ContentFilteringTest::class, 'eZ\Publish\API\Repository\Tests\Filtering\ContentFilteringTest'); From c289838f024100a32f1e1a16bd10bf1709be8923 Mon Sep 17 00:00:00 2001 From: mikolaj Date: Mon, 24 Nov 2025 13:55:49 +0100 Subject: [PATCH 10/19] Refactored sorting field names in query builders for consistency --- .../Gateway/Content/Doctrine/DoctrineGateway.php | 5 +++++ .../Content/DateModifiedSortClauseQueryBuilder.php | 2 +- .../Content/DatePublishedSortClauseQueryBuilder.php | 2 +- .../Content/IdSortClauseQueryBuilder.php | 2 +- .../Content/NameSortClauseQueryBuilder.php | 2 +- .../SectionIdentifierSortClauseQueryBuilder.php | 5 +++-- .../Content/SectionNameSortClauseQueryBuilder.php | 5 +++-- .../Location/BaseLocationSortClauseQueryBuilder.php | 13 +++++++++++-- .../Location/DepthQueryBuilder.php | 5 +++++ .../Location/IdQueryBuilder.php | 5 +++++ .../Location/PathQueryBuilder.php | 5 +++++ .../Location/PriorityQueryBuilder.php | 5 +++++ .../Location/VisibilityQueryBuilder.php | 5 +++++ 13 files changed, 51 insertions(+), 10 deletions(-) diff --git a/src/lib/Persistence/Legacy/Filter/Gateway/Content/Doctrine/DoctrineGateway.php b/src/lib/Persistence/Legacy/Filter/Gateway/Content/Doctrine/DoctrineGateway.php index 18a2424afc..25e321ae93 100644 --- a/src/lib/Persistence/Legacy/Filter/Gateway/Content/Doctrine/DoctrineGateway.php +++ b/src/lib/Persistence/Legacy/Filter/Gateway/Content/Doctrine/DoctrineGateway.php @@ -287,6 +287,11 @@ private function wrapMainQuery(FilteringQueryBuilder $query): QueryBuilder ->from(sprintf('(%s)', $query->getSQL()), 'wrapped') ->setParameters($query->getParameters(), $query->getParameterTypes()); + $orderByParts = $query->getQueryPart('orderBy'); + if (!empty($orderByParts)) { + $wrappedQuery->add('orderBy', $orderByParts); + } + return $wrappedQuery; } } diff --git a/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Content/DateModifiedSortClauseQueryBuilder.php b/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Content/DateModifiedSortClauseQueryBuilder.php index 0ebd294116..2ffb92d9ea 100644 --- a/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Content/DateModifiedSortClauseQueryBuilder.php +++ b/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Content/DateModifiedSortClauseQueryBuilder.php @@ -25,7 +25,7 @@ public function buildQuery( FilteringSortClause $sortClause ): void { /** @var \Ibexa\Contracts\Core\Repository\Values\Content\Query\SortClause $sortClause */ - $queryBuilder->addOrderBy('content.modified', $sortClause->direction); + $queryBuilder->addOrderBy('content_modified', $sortClause->direction); } } diff --git a/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Content/DatePublishedSortClauseQueryBuilder.php b/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Content/DatePublishedSortClauseQueryBuilder.php index e6ae81cea4..1f95446350 100644 --- a/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Content/DatePublishedSortClauseQueryBuilder.php +++ b/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Content/DatePublishedSortClauseQueryBuilder.php @@ -25,7 +25,7 @@ public function buildQuery( FilteringSortClause $sortClause ): void { /** @var \Ibexa\Contracts\Core\Repository\Values\Content\Query\SortClause $sortClause */ - $queryBuilder->addOrderBy('content.published', $sortClause->direction); + $queryBuilder->addOrderBy('content_published', $sortClause->direction); } } diff --git a/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Content/IdSortClauseQueryBuilder.php b/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Content/IdSortClauseQueryBuilder.php index 2beae9b636..ab3325bcbd 100644 --- a/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Content/IdSortClauseQueryBuilder.php +++ b/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Content/IdSortClauseQueryBuilder.php @@ -25,7 +25,7 @@ public function buildQuery( FilteringSortClause $sortClause ): void { /** @var \Ibexa\Contracts\Core\Repository\Values\Content\Query\SortClause $sortClause */ - $queryBuilder->addOrderBy('content.id', $sortClause->direction); + $queryBuilder->addOrderBy('content_id', $sortClause->direction); } } diff --git a/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Content/NameSortClauseQueryBuilder.php b/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Content/NameSortClauseQueryBuilder.php index 51946da0bc..a165d0b7a3 100644 --- a/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Content/NameSortClauseQueryBuilder.php +++ b/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Content/NameSortClauseQueryBuilder.php @@ -25,7 +25,7 @@ public function buildQuery( FilteringSortClause $sortClause ): void { /** @var \Ibexa\Contracts\Core\Repository\Values\Content\Query\SortClause $sortClause */ - $queryBuilder->addOrderBy('content.name', $sortClause->direction); + $queryBuilder->addOrderBy('content_name', $sortClause->direction); } } diff --git a/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Content/SectionIdentifierSortClauseQueryBuilder.php b/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Content/SectionIdentifierSortClauseQueryBuilder.php index c9bfd8a6ab..6a219b0d98 100644 --- a/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Content/SectionIdentifierSortClauseQueryBuilder.php +++ b/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Content/SectionIdentifierSortClauseQueryBuilder.php @@ -25,8 +25,9 @@ public function buildQuery( FilteringQueryBuilder $queryBuilder, FilteringSortClause $sortClause ): void { + $sortAlias = 'ibexa_filter_sort_section_identifier'; $queryBuilder - ->addSelect('section.identifier') + ->addSelect(sprintf('section.identifier AS %s', $sortAlias)) ->joinOnce( 'content', SectionGateway::CONTENT_SECTION_TABLE, @@ -35,7 +36,7 @@ public function buildQuery( ); /** @var \Ibexa\Contracts\Core\Repository\Values\Content\Query\SortClause $sortClause */ - $queryBuilder->addOrderBy('section.identifier', $sortClause->direction); + $queryBuilder->addOrderBy($sortAlias, $sortClause->direction); } } diff --git a/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Content/SectionNameSortClauseQueryBuilder.php b/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Content/SectionNameSortClauseQueryBuilder.php index a2c95562d8..9c1dbc0e68 100644 --- a/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Content/SectionNameSortClauseQueryBuilder.php +++ b/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Content/SectionNameSortClauseQueryBuilder.php @@ -25,8 +25,9 @@ public function buildQuery( FilteringQueryBuilder $queryBuilder, FilteringSortClause $sortClause ): void { + $sortAlias = 'ibexa_filter_sort_section_name'; $queryBuilder - ->addSelect('section.name') + ->addSelect(sprintf('section.name AS %s', $sortAlias)) ->joinOnce( 'content', SectionGateway::CONTENT_SECTION_TABLE, @@ -35,7 +36,7 @@ public function buildQuery( ); /** @var \Ibexa\Contracts\Core\Repository\Values\Content\Query\SortClause $sortClause */ - $queryBuilder->addOrderBy('section.name', $sortClause->direction); + $queryBuilder->addOrderBy($sortAlias, $sortClause->direction); } } diff --git a/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Location/BaseLocationSortClauseQueryBuilder.php b/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Location/BaseLocationSortClauseQueryBuilder.php index 7795a62c9c..b855f2a168 100644 --- a/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Location/BaseLocationSortClauseQueryBuilder.php +++ b/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Location/BaseLocationSortClauseQueryBuilder.php @@ -19,6 +19,7 @@ abstract class BaseLocationSortClauseQueryBuilder implements SortClauseQueryBuilder { private const CONTENT_SORT_LOCATION_ALIAS = 'ibexa_sort_location'; + private const SORT_FIELD_ALIAS_PREFIX = 'ibexa_filter_sort_'; private string $locationAlias = self::CONTENT_SORT_LOCATION_ALIAS; @@ -31,14 +32,15 @@ public function buildQuery( $this->prepareLocationAlias($queryBuilder); $sort = $this->getSortingExpression($this->locationAlias); - $queryBuilder->addSelect($sort); + $sortAlias = $this->getSortFieldAlias(); + $queryBuilder->addSelect(sprintf('%s AS %s', $sort, $sortAlias)); if ($this->needsMainLocationJoin) { $this->joinMainLocationOnly($queryBuilder, $this->locationAlias); } /** @var \Ibexa\Contracts\Core\Repository\Values\Content\Query\SortClause $sortClause */ - $queryBuilder->addOrderBy($sort, $sortClause->direction); + $queryBuilder->addOrderBy($sortAlias, $sortClause->direction); } private function prepareLocationAlias(FilteringQueryBuilder $queryBuilder): void @@ -81,6 +83,13 @@ private function joinMainLocationOnly(FilteringQueryBuilder $queryBuilder, strin } abstract protected function getSortingExpression(string $locationAlias): string; + + protected function getSortFieldAlias(): string + { + return self::SORT_FIELD_ALIAS_PREFIX . $this->getSortFieldName(); + } + + abstract protected function getSortFieldName(): string; } class_alias(BaseLocationSortClauseQueryBuilder::class, 'eZ\Publish\Core\Persistence\Legacy\Filter\SortClauseQueryBuilder\Location\BaseLocationSortClauseQueryBuilder'); diff --git a/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Location/DepthQueryBuilder.php b/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Location/DepthQueryBuilder.php index c7e98bad1d..596ad32640 100644 --- a/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Location/DepthQueryBuilder.php +++ b/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Location/DepthQueryBuilder.php @@ -25,6 +25,11 @@ protected function getSortingExpression(string $locationAlias): string { return sprintf('%s.depth', $locationAlias); } + + protected function getSortFieldName(): string + { + return 'location_depth'; + } } class_alias(DepthQueryBuilder::class, 'eZ\Publish\Core\Persistence\Legacy\Filter\SortClauseQueryBuilder\Location\DepthQueryBuilder'); diff --git a/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Location/IdQueryBuilder.php b/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Location/IdQueryBuilder.php index c946498abe..8c5e748a04 100644 --- a/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Location/IdQueryBuilder.php +++ b/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Location/IdQueryBuilder.php @@ -25,6 +25,11 @@ protected function getSortingExpression(string $locationAlias): string { return sprintf('%s.node_id', $locationAlias); } + + protected function getSortFieldName(): string + { + return 'location_id'; + } } class_alias(IdQueryBuilder::class, 'eZ\Publish\Core\Persistence\Legacy\Filter\SortClauseQueryBuilder\Location\IdQueryBuilder'); diff --git a/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Location/PathQueryBuilder.php b/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Location/PathQueryBuilder.php index 0dac771d79..321cf823ab 100644 --- a/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Location/PathQueryBuilder.php +++ b/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Location/PathQueryBuilder.php @@ -22,6 +22,11 @@ protected function getSortingExpression(string $locationAlias): string { return sprintf('%s.path_string', $locationAlias); } + + protected function getSortFieldName(): string + { + return 'location_path'; + } } class_alias(PathQueryBuilder::class, 'eZ\Publish\Core\Persistence\Legacy\Filter\SortClauseQueryBuilder\Location\PathQueryBuilder'); diff --git a/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Location/PriorityQueryBuilder.php b/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Location/PriorityQueryBuilder.php index 0d1e19164b..b3bc1341e5 100644 --- a/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Location/PriorityQueryBuilder.php +++ b/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Location/PriorityQueryBuilder.php @@ -22,6 +22,11 @@ protected function getSortingExpression(string $locationAlias): string { return sprintf('%s.priority', $locationAlias); } + + protected function getSortFieldName(): string + { + return 'location_priority'; + } } class_alias(PriorityQueryBuilder::class, 'eZ\Publish\Core\Persistence\Legacy\Filter\SortClauseQueryBuilder\Location\PriorityQueryBuilder'); diff --git a/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Location/VisibilityQueryBuilder.php b/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Location/VisibilityQueryBuilder.php index a8659e75de..cbb45f9cd8 100644 --- a/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Location/VisibilityQueryBuilder.php +++ b/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Location/VisibilityQueryBuilder.php @@ -22,6 +22,11 @@ protected function getSortingExpression(string $locationAlias): string { return sprintf('%s.is_invisible', $locationAlias); } + + protected function getSortFieldName(): string + { + return 'location_visibility'; + } } class_alias(VisibilityQueryBuilder::class, 'eZ\Publish\Core\Persistence\Legacy\Filter\SortClauseQueryBuilder\Location\VisibilityQueryBuilder'); From b4b2d214894f0488478b1051c5c2650c36bba84c Mon Sep 17 00:00:00 2001 From: mikolaj Date: Mon, 24 Nov 2025 15:27:35 +0100 Subject: [PATCH 11/19] Enhanced query wrapping to include dynamic select columns based on order by clauses --- .../Gateway/Content/Doctrine/DoctrineGateway.php | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/lib/Persistence/Legacy/Filter/Gateway/Content/Doctrine/DoctrineGateway.php b/src/lib/Persistence/Legacy/Filter/Gateway/Content/Doctrine/DoctrineGateway.php index 25e321ae93..336e2fbabb 100644 --- a/src/lib/Persistence/Legacy/Filter/Gateway/Content/Doctrine/DoctrineGateway.php +++ b/src/lib/Persistence/Legacy/Filter/Gateway/Content/Doctrine/DoctrineGateway.php @@ -281,13 +281,24 @@ private function getColumns(): Traversable private function wrapMainQuery(FilteringQueryBuilder $query): QueryBuilder { $wrappedQuery = $this->connection->createQueryBuilder(); + + $orderByParts = $query->getQueryPart('orderBy'); + $selectColumns = array_keys(self::COLUMN_MAP); + if (!empty($orderByParts)) { + foreach ($orderByParts as $orderByPart) { + $orderExpression = preg_replace('/\\s+(ASC|DESC).*$/i', '', $orderByPart); + if (null !== $orderExpression && !in_array($orderExpression, $selectColumns, true)) { + $selectColumns[] = $orderExpression; + } + } + } + $wrappedQuery - ->select(array_keys(self::COLUMN_MAP)) + ->select($selectColumns) ->distinct() ->from(sprintf('(%s)', $query->getSQL()), 'wrapped') ->setParameters($query->getParameters(), $query->getParameterTypes()); - $orderByParts = $query->getQueryPart('orderBy'); if (!empty($orderByParts)) { $wrappedQuery->add('orderBy', $orderByParts); } From 3f4071a4b1bdde65218c8100ecf87fba4f2f2b49 Mon Sep 17 00:00:00 2001 From: mikolaj Date: Tue, 25 Nov 2025 13:05:55 +0100 Subject: [PATCH 12/19] Refactored location filtering logic and removed redundant query wrapping --- .../BaseLocationCriterionQueryBuilder.php | 28 ++++++++++++- .../Content/Doctrine/DoctrineGateway.php | 41 ++----------------- .../Filtering/ContentFilteringTest.php | 4 +- 3 files changed, 32 insertions(+), 41 deletions(-) diff --git a/src/lib/Persistence/Legacy/Filter/CriterionQueryBuilder/Location/BaseLocationCriterionQueryBuilder.php b/src/lib/Persistence/Legacy/Filter/CriterionQueryBuilder/Location/BaseLocationCriterionQueryBuilder.php index b5189a4792..b613e8265e 100644 --- a/src/lib/Persistence/Legacy/Filter/CriterionQueryBuilder/Location/BaseLocationCriterionQueryBuilder.php +++ b/src/lib/Persistence/Legacy/Filter/CriterionQueryBuilder/Location/BaseLocationCriterionQueryBuilder.php @@ -11,6 +11,7 @@ use Ibexa\Contracts\Core\Persistence\Filter\Doctrine\FilteringQueryBuilder; use Ibexa\Contracts\Core\Repository\Values\Filter\CriterionQueryBuilder; use Ibexa\Contracts\Core\Repository\Values\Filter\FilteringCriterion; +use Ibexa\Core\Persistence\Legacy\Content\Location\Gateway as LocationGateway; /** * @internal for internal use by Repository Filtering @@ -21,10 +22,35 @@ public function buildQueryConstraint( FilteringQueryBuilder $queryBuilder, FilteringCriterion $criterion ): ?string { - $queryBuilder->joinAllLocations(); + if ($this->isLocationFilteringContext($queryBuilder)) { + return null; + } + + $expressionBuilder = $queryBuilder->expr(); + $queryBuilder->joinOnce( + 'content', + LocationGateway::CONTENT_TREE_TABLE, + 'location', + (string)$expressionBuilder->andX( + 'content.id = location.contentobject_id', + 'location.node_id = location.main_node_id' + ) + ); return null; } + + private function isLocationFilteringContext(FilteringQueryBuilder $queryBuilder): bool + { + $fromParts = $queryBuilder->getQueryPart('from'); + foreach ($fromParts as $fromPart) { + if (($fromPart['alias'] ?? null) === 'location') { + return true; + } + } + + return false; + } } class_alias(BaseLocationCriterionQueryBuilder::class, 'eZ\Publish\Core\Persistence\Legacy\Filter\CriterionQueryBuilder\Location\BaseLocationCriterionQueryBuilder'); diff --git a/src/lib/Persistence/Legacy/Filter/Gateway/Content/Doctrine/DoctrineGateway.php b/src/lib/Persistence/Legacy/Filter/Gateway/Content/Doctrine/DoctrineGateway.php index 336e2fbabb..3d82b5c0b9 100644 --- a/src/lib/Persistence/Legacy/Filter/Gateway/Content/Doctrine/DoctrineGateway.php +++ b/src/lib/Persistence/Legacy/Filter/Gateway/Content/Doctrine/DoctrineGateway.php @@ -13,7 +13,6 @@ use Doctrine\DBAL\DBALException; use Doctrine\DBAL\FetchMode; use Doctrine\DBAL\Platforms\AbstractPlatform; -use Doctrine\DBAL\Query\QueryBuilder; use Ibexa\Contracts\Core\Persistence\Filter\CriterionVisitor; use Ibexa\Contracts\Core\Persistence\Filter\Doctrine\FilteringQueryBuilder; use Ibexa\Contracts\Core\Persistence\Filter\SortClauseVisitor; @@ -23,7 +22,6 @@ use Ibexa\Core\Persistence\Legacy\Content\Location\Gateway as LocationGateway; use Ibexa\Core\Persistence\Legacy\Filter\Gateway\Gateway; use function iterator_to_array; -use function sprintf; use Traversable; /** @@ -110,14 +108,12 @@ public function find( $names = $this->bulkFetchVersionNames(clone $query); $fieldValues = $this->bulkFetchFieldValues(clone $query); - // wrap query to avoid duplicate entries for multiple Locations - $wrappedQuery = $this->wrapMainQuery($query); - $wrappedQuery->setFirstResult($offset); + $query->setFirstResult($offset); if ($limit > 0) { - $wrappedQuery->setMaxResults($limit); + $query->setMaxResults($limit); } - $resultStatement = $wrappedQuery->execute(); + $resultStatement = $query->execute(); while (false !== ($row = $resultStatement->fetch(FetchMode::ASSOCIATIVE))) { $contentId = (int)$row['content_id']; $versionNo = (int)$row['content_version_no']; @@ -274,37 +270,6 @@ private function getColumns(): Traversable yield "{$columnName} AS {$columnAlias}"; } } - - /** - * Wrap query to avoid duplicate entries for multiple Locations. - */ - private function wrapMainQuery(FilteringQueryBuilder $query): QueryBuilder - { - $wrappedQuery = $this->connection->createQueryBuilder(); - - $orderByParts = $query->getQueryPart('orderBy'); - $selectColumns = array_keys(self::COLUMN_MAP); - if (!empty($orderByParts)) { - foreach ($orderByParts as $orderByPart) { - $orderExpression = preg_replace('/\\s+(ASC|DESC).*$/i', '', $orderByPart); - if (null !== $orderExpression && !in_array($orderExpression, $selectColumns, true)) { - $selectColumns[] = $orderExpression; - } - } - } - - $wrappedQuery - ->select($selectColumns) - ->distinct() - ->from(sprintf('(%s)', $query->getSQL()), 'wrapped') - ->setParameters($query->getParameters(), $query->getParameterTypes()); - - if (!empty($orderByParts)) { - $wrappedQuery->add('orderBy', $orderByParts); - } - - return $wrappedQuery; - } } class_alias(DoctrineGateway::class, 'eZ\Publish\Core\Persistence\Legacy\Filter\Gateway\Content\Doctrine\DoctrineGateway'); diff --git a/tests/integration/Core/Repository/Filtering/ContentFilteringTest.php b/tests/integration/Core/Repository/Filtering/ContentFilteringTest.php index df921f25c2..4cc614daaa 100644 --- a/tests/integration/Core/Repository/Filtering/ContentFilteringTest.php +++ b/tests/integration/Core/Repository/Filtering/ContentFilteringTest.php @@ -403,7 +403,7 @@ public function testFindContentUsingLocationCriterion( public function getDataForTestFindContentWithLocationCriterion(): iterable { - yield 'Content items with secondary Location, sorted by Content ID' => [ + yield 'Content items with secondary Location ignored in content filtering, sorted by Content ID' => [ static function (Content $parentFolder): Filter { return (new Filter()) ->withCriterion( @@ -413,7 +413,7 @@ static function (Content $parentFolder): Filter { ) ->withSortClause(new SortClause\ContentId(Query::SORT_ASC)); }, - [TestContentProvider::CONTENT_REMOTE_IDS['folder2']], + [], ]; yield 'Folders with Location, sorted by Content ID' => [ From e3fc8e9e0eef2bbc387d55d0ff09ac706c75ef2a Mon Sep 17 00:00:00 2001 From: mikolaj Date: Tue, 2 Dec 2025 17:20:23 +0100 Subject: [PATCH 13/19] Refactored ContentFilteringTest to use getContentInfo() for mainLocationId access --- .../Filtering/ContentFilteringTest.php | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/tests/integration/Core/Repository/Filtering/ContentFilteringTest.php b/tests/integration/Core/Repository/Filtering/ContentFilteringTest.php index 4cc614daaa..11e159b492 100644 --- a/tests/integration/Core/Repository/Filtering/ContentFilteringTest.php +++ b/tests/integration/Core/Repository/Filtering/ContentFilteringTest.php @@ -96,15 +96,15 @@ public function testLocationSortClausesUseMainLocationDuringContentFiltering(): ); $referenceContent = $this->createFolder( ['eng-GB' => 'Reference folder'], - $shallowParent->contentInfo->mainLocationId + $shallowParent->getContentInfo()->getMainLocationId() ); $deepParent = $this->createFolder( ['eng-GB' => 'Deep Parent'], - $referenceContent->contentInfo->mainLocationId + $referenceContent->getContentInfo()->getMainLocationId() ); $contentWithAdditionalLocation = $this->createFolder( ['eng-GB' => 'Folder with extra location'], - $deepParent->contentInfo->mainLocationId + $deepParent->getContentInfo()->getMainLocationId() ); $locationService->createLocation( $contentWithAdditionalLocation->contentInfo, @@ -114,7 +114,7 @@ public function testLocationSortClausesUseMainLocationDuringContentFiltering(): $mainLocation = $this->loadMainLocation($locationService, $contentWithAdditionalLocation); $nonMainLocations = []; foreach ($locationService->loadLocations($contentWithAdditionalLocation->contentInfo) as $location) { - if ($location->id !== $contentWithAdditionalLocation->contentInfo->mainLocationId) { + if ($location->id !== $contentWithAdditionalLocation->getContentInfo()->getMainLocationId()) { $nonMainLocations[] = $location; } } @@ -161,19 +161,19 @@ public function testLocationSortClausesStayDeterministicWithComplexCriteria(): v ); $referenceContent = $this->createFolder( ['eng-GB' => 'Ref folder'], - $shallowParent->contentInfo->mainLocationId + $shallowParent->getContentInfo()->getMainLocationId() ); $middleContent = $this->createFolder( ['eng-GB' => 'Middle folder'], - $referenceContent->contentInfo->mainLocationId + $referenceContent->getContentInfo()->getMainLocationId() ); $deepParent = $this->createFolder( ['eng-GB' => 'Deep intermediate'], - $middleContent->contentInfo->mainLocationId + $middleContent->getContentInfo()->getMainLocationId() ); $contentWithAdditionalLocation = $this->createFolder( ['eng-GB' => 'Folder with randomizing location'], - $deepParent->contentInfo->mainLocationId + $deepParent->getContentInfo()->getMainLocationId() ); $locationService->createLocation( $contentWithAdditionalLocation->contentInfo, @@ -185,7 +185,7 @@ public function testLocationSortClausesStayDeterministicWithComplexCriteria(): v $mainLocation = $this->loadMainLocation($locationService, $contentWithAdditionalLocation); $nonMainLocations = []; foreach ($locationService->loadLocations($contentWithAdditionalLocation->contentInfo) as $location) { - if ($location->id !== $contentWithAdditionalLocation->contentInfo->mainLocationId) { + if ($location->id !== $contentWithAdditionalLocation->getContentInfo()->getMainLocationId()) { $nonMainLocations[] = $location; } } @@ -305,7 +305,7 @@ static function (Content $parentFolder): Filter { private function createMultiplePagesOfContentItems(int $pageSize, int $noOfPages): int { $parentFolder = $this->createFolder(['eng-GB' => 'Parent Folder'], 2); - $parentFolderMainLocationId = $parentFolder->contentInfo->mainLocationId; + $parentFolderMainLocationId = $parentFolder->getContentInfo()->getMainLocationId(); $noOfItems = $pageSize * $noOfPages; for ($itemNo = 1; $itemNo <= $noOfItems; ++$itemNo) { @@ -425,7 +425,7 @@ static function (Content $parentFolder): Filter { ) ) ->andWithCriterion( - new Criterion\ParentLocationId($parentFolder->contentInfo->mainLocationId) + new Criterion\ParentLocationId($parentFolder->getContentInfo()->getMainLocationId()) ) ->andWithCriterion( new Criterion\ContentTypeIdentifier('folder') @@ -447,12 +447,12 @@ protected function assertFoundContentItemsByRemoteIds( foreach ($list as $content) { /** @var \Ibexa\Contracts\Core\Repository\Values\Content\Content $content */ self::assertContainsEquals( - $content->contentInfo->remoteId, + $content->getContentInfo()->remoteId, $expectedContentRemoteIds, sprintf( 'Content %d (%s) was not supposed to be found', $content->id, - $content->contentInfo->remoteId + $content->getContentInfo()->remoteId ) ); } @@ -597,7 +597,7 @@ private function buildSearchQueryFromFilter(Filter $filter): Query private function loadMainLocation(LocationService $locationService, Content $content): Location { - $mainLocationId = $content->contentInfo->mainLocationId; + $mainLocationId = $content->getContentInfo()->getMainLocationId(); self::assertNotNull($mainLocationId); return $locationService->loadLocation($mainLocationId); From 2b45e8c816343e5635c2b1ca7ebe05bd463d0cfa Mon Sep 17 00:00:00 2001 From: mikolaj Date: Wed, 3 Dec 2025 16:24:30 +0100 Subject: [PATCH 14/19] Refactored sorting expression methods for location query builders --- .../BaseLocationSortClauseQueryBuilder.php | 34 +++++++++++++++---- .../Location/DepthQueryBuilder.php | 9 +++-- .../Location/IdQueryBuilder.php | 9 +++-- .../Location/PathQueryBuilder.php | 9 +++-- .../Location/PriorityQueryBuilder.php | 9 +++-- .../Location/VisibilityQueryBuilder.php | 9 +++-- 6 files changed, 63 insertions(+), 16 deletions(-) diff --git a/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Location/BaseLocationSortClauseQueryBuilder.php b/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Location/BaseLocationSortClauseQueryBuilder.php index b855f2a168..50ad085341 100644 --- a/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Location/BaseLocationSortClauseQueryBuilder.php +++ b/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Location/BaseLocationSortClauseQueryBuilder.php @@ -31,8 +31,8 @@ public function buildQuery( ): void { $this->prepareLocationAlias($queryBuilder); - $sort = $this->getSortingExpression($this->locationAlias); - $sortAlias = $this->getSortFieldAlias(); + $sort = $this->getSortingExpressionForAlias($this->locationAlias); + $sortAlias = $this->getSortFieldAlias($sort); $queryBuilder->addSelect(sprintf('%s AS %s', $sort, $sortAlias)); if ($this->needsMainLocationJoin) { @@ -82,14 +82,36 @@ private function joinMainLocationOnly(FilteringQueryBuilder $queryBuilder, strin ); } - abstract protected function getSortingExpression(string $locationAlias): string; + /** + * Legacy entry point: implementations are expected to override this. + */ + abstract protected function getSortingExpression(): string; - protected function getSortFieldAlias(): string + /** + * Optional alias-aware override; default falls back to legacy expression with alias swap. + */ + protected function getSortingExpressionForAlias(string $locationAlias): string { - return self::SORT_FIELD_ALIAS_PREFIX . $this->getSortFieldName(); + $expression = $this->getSortingExpression(); + + if ($locationAlias === 'location') { + return $expression; + } + + $replaced = preg_replace('/\\blocation\\./', sprintf('%s.', $locationAlias), $expression); + + return $replaced ?? $expression; } - abstract protected function getSortFieldName(): string; + protected function getSortFieldAlias(string $sortExpression): string + { + return self::SORT_FIELD_ALIAS_PREFIX . $this->getSortFieldName($sortExpression); + } + + protected function getSortFieldName(string $sortExpression): string + { + return str_replace('.', '_', $sortExpression); + } } class_alias(BaseLocationSortClauseQueryBuilder::class, 'eZ\Publish\Core\Persistence\Legacy\Filter\SortClauseQueryBuilder\Location\BaseLocationSortClauseQueryBuilder'); diff --git a/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Location/DepthQueryBuilder.php b/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Location/DepthQueryBuilder.php index 596ad32640..945da63a48 100644 --- a/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Location/DepthQueryBuilder.php +++ b/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Location/DepthQueryBuilder.php @@ -21,12 +21,17 @@ public function accepts(FilteringSortClause $sortClause): bool return $sortClause instanceof Location\Depth; } - protected function getSortingExpression(string $locationAlias): string + protected function getSortingExpression(): string + { + return 'location.depth'; + } + + protected function getSortingExpressionForAlias(string $locationAlias): string { return sprintf('%s.depth', $locationAlias); } - protected function getSortFieldName(): string + protected function getSortFieldName(string $sortExpression): string { return 'location_depth'; } diff --git a/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Location/IdQueryBuilder.php b/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Location/IdQueryBuilder.php index 8c5e748a04..589537ea9c 100644 --- a/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Location/IdQueryBuilder.php +++ b/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Location/IdQueryBuilder.php @@ -21,12 +21,17 @@ public function accepts(FilteringSortClause $sortClause): bool return $sortClause instanceof Location\Id; } - protected function getSortingExpression(string $locationAlias): string + protected function getSortingExpression(): string + { + return 'location.node_id'; + } + + protected function getSortingExpressionForAlias(string $locationAlias): string { return sprintf('%s.node_id', $locationAlias); } - protected function getSortFieldName(): string + protected function getSortFieldName(string $sortExpression): string { return 'location_id'; } diff --git a/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Location/PathQueryBuilder.php b/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Location/PathQueryBuilder.php index 321cf823ab..75b946f605 100644 --- a/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Location/PathQueryBuilder.php +++ b/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Location/PathQueryBuilder.php @@ -18,12 +18,17 @@ public function accepts(FilteringSortClause $sortClause): bool return $sortClause instanceof Location\Path; } - protected function getSortingExpression(string $locationAlias): string + protected function getSortingExpression(): string + { + return 'location.path_string'; + } + + protected function getSortingExpressionForAlias(string $locationAlias): string { return sprintf('%s.path_string', $locationAlias); } - protected function getSortFieldName(): string + protected function getSortFieldName(string $sortExpression): string { return 'location_path'; } diff --git a/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Location/PriorityQueryBuilder.php b/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Location/PriorityQueryBuilder.php index b3bc1341e5..c6f07c32b4 100644 --- a/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Location/PriorityQueryBuilder.php +++ b/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Location/PriorityQueryBuilder.php @@ -18,12 +18,17 @@ public function accepts(FilteringSortClause $sortClause): bool return $sortClause instanceof Location\Priority; } - protected function getSortingExpression(string $locationAlias): string + protected function getSortingExpression(): string + { + return 'location.priority'; + } + + protected function getSortingExpressionForAlias(string $locationAlias): string { return sprintf('%s.priority', $locationAlias); } - protected function getSortFieldName(): string + protected function getSortFieldName(string $sortExpression): string { return 'location_priority'; } diff --git a/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Location/VisibilityQueryBuilder.php b/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Location/VisibilityQueryBuilder.php index cbb45f9cd8..89fed7f41d 100644 --- a/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Location/VisibilityQueryBuilder.php +++ b/src/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Location/VisibilityQueryBuilder.php @@ -18,12 +18,17 @@ public function accepts(FilteringSortClause $sortClause): bool return $sortClause instanceof Location\Visibility; } - protected function getSortingExpression(string $locationAlias): string + protected function getSortingExpression(): string + { + return 'location.is_invisible'; + } + + protected function getSortingExpressionForAlias(string $locationAlias): string { return sprintf('%s.is_invisible', $locationAlias); } - protected function getSortFieldName(): string + protected function getSortFieldName(string $sortExpression): string { return 'location_visibility'; } From 3822fa99831f90cc5f7ab4b59ee9bac5705de3b6 Mon Sep 17 00:00:00 2001 From: mikolaj Date: Wed, 3 Dec 2025 17:21:15 +0100 Subject: [PATCH 15/19] Added unit test for BaseLocationSortClauseQueryBuilder --- ...BaseLocationSortClauseQueryBuilderTest.php | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 tests/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Location/BaseLocationSortClauseQueryBuilderTest.php diff --git a/tests/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Location/BaseLocationSortClauseQueryBuilderTest.php b/tests/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Location/BaseLocationSortClauseQueryBuilderTest.php new file mode 100644 index 0000000000..1ab851fee3 --- /dev/null +++ b/tests/lib/Persistence/Legacy/Filter/SortClauseQueryBuilder/Location/BaseLocationSortClauseQueryBuilderTest.php @@ -0,0 +1,62 @@ + 'sqlite:///:memory:']); + $queryBuilder = new FilteringQueryBuilder($connection); + + $sortClause = new class() implements FilteringSortClause { + public string $direction = Query::SORT_ASC; + }; + + $builder = new class() extends BaseLocationSortClauseQueryBuilder { + protected function getSortingExpression(): string + { + return 'location.depth'; + } + + public function accepts(FilteringSortClause $sortClause): bool + { + return true; + } + }; + + $builder->buildQuery($queryBuilder, $sortClause); + + self::assertSame( + ['ibexa_sort_location.depth AS ibexa_filter_sort_ibexa_sort_location_depth'], + $queryBuilder->getQueryPart('select') + ); + + $joins = $queryBuilder->getQueryPart('join'); + self::assertArrayHasKey('content', $joins); + self::assertCount(1, $joins['content']); + self::assertSame(LocationGateway::CONTENT_TREE_TABLE, $joins['content'][0]['joinTable']); + self::assertSame('ibexa_sort_location', $joins['content'][0]['joinAlias']); + self::assertSame( + '(content.id = ibexa_sort_location.contentobject_id) AND (ibexa_sort_location.node_id = ibexa_sort_location.main_node_id)', + (string)$joins['content'][0]['joinCondition'] + ); + + $orderBy = $queryBuilder->getQueryPart('orderBy'); + self::assertSame(['ibexa_filter_sort_ibexa_sort_location_depth ASC'], $orderBy); + } +} From f95eaad99fae1f41c1cea0b7a7867d8b6be1920c Mon Sep 17 00:00:00 2001 From: mikolaj Date: Wed, 3 Dec 2025 17:21:35 +0100 Subject: [PATCH 16/19] Added legacy location sort clause and query builder for filtering --- .../Filtering/ContentFilteringTest.php | 3 + .../Fixtures/LegacyLocationSortClause.php | 21 ++++++ .../LegacyLocationSortQueryBuilder.php | 25 +++++++ .../Filtering/LegacyContentFilteringTest.php | 69 +++++++++++++++++++ .../Filtering/LegacyFilteringSetupFactory.php | 49 +++++++++++++ .../config/services/legacy_sort_clause.yaml | 4 ++ 6 files changed, 171 insertions(+) create mode 100644 tests/integration/Core/Repository/Filtering/Fixtures/LegacyLocationSortClause.php create mode 100644 tests/integration/Core/Repository/Filtering/Fixtures/LegacyLocationSortQueryBuilder.php create mode 100644 tests/integration/Core/Repository/Filtering/LegacyContentFilteringTest.php create mode 100644 tests/integration/Core/Repository/Filtering/LegacyFilteringSetupFactory.php create mode 100644 tests/integration/Core/Repository/Filtering/Resources/config/services/legacy_sort_clause.yaml diff --git a/tests/integration/Core/Repository/Filtering/ContentFilteringTest.php b/tests/integration/Core/Repository/Filtering/ContentFilteringTest.php index 11e159b492..6704f70649 100644 --- a/tests/integration/Core/Repository/Filtering/ContentFilteringTest.php +++ b/tests/integration/Core/Repository/Filtering/ContentFilteringTest.php @@ -23,7 +23,10 @@ use Ibexa\Contracts\Core\Repository\Values\ObjectState\ObjectStateCreateStruct; use Ibexa\Contracts\Core\Repository\Values\ObjectState\ObjectStateGroupCreateStruct; use Ibexa\Core\FieldType\Keyword; +use Ibexa\Core\Persistence\Legacy\Filter\SortClauseVisitor; use Ibexa\Tests\Core\Repository\Filtering\TestContentProvider; +use Ibexa\Tests\Integration\Core\Repository\Filtering\Fixtures\LegacyLocationSortClause; +use Ibexa\Tests\Integration\Core\Repository\Filtering\Fixtures\LegacyLocationSortQueryBuilder; use function iterator_to_array; use IteratorAggregate; use function sprintf; diff --git a/tests/integration/Core/Repository/Filtering/Fixtures/LegacyLocationSortClause.php b/tests/integration/Core/Repository/Filtering/Fixtures/LegacyLocationSortClause.php new file mode 100644 index 0000000000..975c9fa23d --- /dev/null +++ b/tests/integration/Core/Repository/Filtering/Fixtures/LegacyLocationSortClause.php @@ -0,0 +1,21 @@ +targetData = ['sort_direction' => $sortDirection]; + $this->direction = $sortDirection; + } +} diff --git a/tests/integration/Core/Repository/Filtering/Fixtures/LegacyLocationSortQueryBuilder.php b/tests/integration/Core/Repository/Filtering/Fixtures/LegacyLocationSortQueryBuilder.php new file mode 100644 index 0000000000..cf9b99759c --- /dev/null +++ b/tests/integration/Core/Repository/Filtering/Fixtures/LegacyLocationSortQueryBuilder.php @@ -0,0 +1,25 @@ +contentProvider = new TestContentProvider($this->getRepository(), $this); + } + + public function testLegacyLocationSortClause(): void + { + $parentFolder = $this->contentProvider->createSharedContentStructure(); + + $filter = (new Filter()) + ->withCriterion( + new Criterion\ParentLocationId($parentFolder->getContentInfo()->getMainLocationId()) + ) + ->andWithCriterion( + new Criterion\ContentTypeIdentifier('folder') + ) + ->withSortClause(new LegacyLocationSortClause(Query::SORT_ASC)); + + $list = $this->getRepository()->getContentService()->find($filter, []); + + self::assertCount(2, $list); + $remoteIds = array_map( + static fn ($content): string => $content->getContentInfo()->remoteId, + iterator_to_array($list) + ); + self::assertSame( + [ + TestContentProvider::CONTENT_REMOTE_IDS['folder1'], + TestContentProvider::CONTENT_REMOTE_IDS['folder2'], + ], + $remoteIds + ); + } + + protected function getSetupFactory(): SetupFactory + { + return new LegacyFilteringSetupFactory(); + } +} diff --git a/tests/integration/Core/Repository/Filtering/LegacyFilteringSetupFactory.php b/tests/integration/Core/Repository/Filtering/LegacyFilteringSetupFactory.php new file mode 100644 index 0000000000..e5d20bbf8a --- /dev/null +++ b/tests/integration/Core/Repository/Filtering/LegacyFilteringSetupFactory.php @@ -0,0 +1,49 @@ +load('services/legacy_sort_clause.yaml'); + + if ($containerBuilder->hasDefinition(LegacyLocationSortQueryBuilder::class)) { + $containerBuilder + ->getDefinition(LegacyLocationSortQueryBuilder::class) + ->addTag(ServiceTags::FILTERING_SORT_CLAUSE_QUERY_BUILDER); + } else { + $containerBuilder + ->register(LegacyLocationSortQueryBuilder::class, LegacyLocationSortQueryBuilder::class) + ->addTag(ServiceTags::FILTERING_SORT_CLAUSE_QUERY_BUILDER); + } + } +} diff --git a/tests/integration/Core/Repository/Filtering/Resources/config/services/legacy_sort_clause.yaml b/tests/integration/Core/Repository/Filtering/Resources/config/services/legacy_sort_clause.yaml new file mode 100644 index 0000000000..ea83b5876e --- /dev/null +++ b/tests/integration/Core/Repository/Filtering/Resources/config/services/legacy_sort_clause.yaml @@ -0,0 +1,4 @@ +services: + Ibexa\Tests\Integration\Core\Repository\Filtering\Fixtures\LegacyLocationSortQueryBuilder: + tags: + - { name: ibexa.filter.sort_clause.query.builder } From 7201d35564e3d9830d2cfb2d35134cbd230ef65f Mon Sep 17 00:00:00 2001 From: mikolaj Date: Wed, 3 Dec 2025 17:38:00 +0100 Subject: [PATCH 17/19] Refactored legacy filtering setup and improved type handling in tests --- .../Filtering/ContentFilteringTest.php | 3 - .../Fixtures/LegacyLocationSortClause.php | 3 +- .../Filtering/LegacyContentFilteringTest.php | 4 +- .../Filtering/LegacyFilteringSetupFactory.php | 60 ++++++++++++++++--- 4 files changed, 54 insertions(+), 16 deletions(-) diff --git a/tests/integration/Core/Repository/Filtering/ContentFilteringTest.php b/tests/integration/Core/Repository/Filtering/ContentFilteringTest.php index 6704f70649..11e159b492 100644 --- a/tests/integration/Core/Repository/Filtering/ContentFilteringTest.php +++ b/tests/integration/Core/Repository/Filtering/ContentFilteringTest.php @@ -23,10 +23,7 @@ use Ibexa\Contracts\Core\Repository\Values\ObjectState\ObjectStateCreateStruct; use Ibexa\Contracts\Core\Repository\Values\ObjectState\ObjectStateGroupCreateStruct; use Ibexa\Core\FieldType\Keyword; -use Ibexa\Core\Persistence\Legacy\Filter\SortClauseVisitor; use Ibexa\Tests\Core\Repository\Filtering\TestContentProvider; -use Ibexa\Tests\Integration\Core\Repository\Filtering\Fixtures\LegacyLocationSortClause; -use Ibexa\Tests\Integration\Core\Repository\Filtering\Fixtures\LegacyLocationSortQueryBuilder; use function iterator_to_array; use IteratorAggregate; use function sprintf; diff --git a/tests/integration/Core/Repository/Filtering/Fixtures/LegacyLocationSortClause.php b/tests/integration/Core/Repository/Filtering/Fixtures/LegacyLocationSortClause.php index 975c9fa23d..94c5176f52 100644 --- a/tests/integration/Core/Repository/Filtering/Fixtures/LegacyLocationSortClause.php +++ b/tests/integration/Core/Repository/Filtering/Fixtures/LegacyLocationSortClause.php @@ -15,7 +15,6 @@ final class LegacyLocationSortClause extends SortClause implements FilteringSort { public function __construct(string $sortDirection) { - $this->targetData = ['sort_direction' => $sortDirection]; - $this->direction = $sortDirection; + parent::__construct('legacy_location_depth', $sortDirection); } } diff --git a/tests/integration/Core/Repository/Filtering/LegacyContentFilteringTest.php b/tests/integration/Core/Repository/Filtering/LegacyContentFilteringTest.php index b78860b2de..50df253518 100644 --- a/tests/integration/Core/Repository/Filtering/LegacyContentFilteringTest.php +++ b/tests/integration/Core/Repository/Filtering/LegacyContentFilteringTest.php @@ -8,6 +8,7 @@ namespace Ibexa\Tests\Integration\Core\Repository\Filtering; +use function array_map; use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; use Ibexa\Contracts\Core\Repository\Values\Filter\Filter; @@ -15,7 +16,6 @@ use Ibexa\Tests\Core\Repository\Filtering\TestContentProvider; use Ibexa\Tests\Integration\Core\Repository\BaseTest; use Ibexa\Tests\Integration\Core\Repository\Filtering\Fixtures\LegacyLocationSortClause; -use function array_map; use function iterator_to_array; /** @@ -39,7 +39,7 @@ public function testLegacyLocationSortClause(): void $filter = (new Filter()) ->withCriterion( - new Criterion\ParentLocationId($parentFolder->getContentInfo()->getMainLocationId()) + new Criterion\ParentLocationId((int)$parentFolder->getContentInfo()->getMainLocationId()) ) ->andWithCriterion( new Criterion\ContentTypeIdentifier('folder') diff --git a/tests/integration/Core/Repository/Filtering/LegacyFilteringSetupFactory.php b/tests/integration/Core/Repository/Filtering/LegacyFilteringSetupFactory.php index e5d20bbf8a..9a6d0b2283 100644 --- a/tests/integration/Core/Repository/Filtering/LegacyFilteringSetupFactory.php +++ b/tests/integration/Core/Repository/Filtering/LegacyFilteringSetupFactory.php @@ -8,28 +8,70 @@ namespace Ibexa\Tests\Integration\Core\Repository\Filtering; +use Ibexa\Bundle\Core\DependencyInjection\ServiceTags; +use Ibexa\Contracts\Core\Repository\Values\Filter\CriterionQueryBuilder as FilteringCriterionQueryBuilder; +use Ibexa\Contracts\Core\Repository\Values\Filter\SortClauseQueryBuilder as FilteringSortClauseQueryBuilder; use Ibexa\Contracts\Core\Test\Repository\SetupFactory\Legacy; +use Ibexa\Core\Base\Container\Compiler; use Ibexa\Core\Base\ServiceContainer; -use Ibexa\Bundle\Core\DependencyInjection\ServiceTags; +use Ibexa\Tests\Integration\Core\LegacyTestContainerBuilder; use Ibexa\Tests\Integration\Core\Repository\Filtering\Fixtures\LegacyLocationSortQueryBuilder; use Symfony\Component\Config\FileLocator; -use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; final class LegacyFilteringSetupFactory extends Legacy { - public function getServiceContainer() + public function getServiceContainer(): ServiceContainer { - // Force rebuilding the container to include test-only services. - self::$serviceContainer = null; + if (self::$serviceContainer instanceof ServiceContainer) { + return self::$serviceContainer; + } + + $installDir = self::getInstallationDir(); + + $containerBuilder = new LegacyTestContainerBuilder(); + $loader = $containerBuilder->getCoreLoader(); + $loader->load('search_engines/legacy.yml'); + $loader->load('integration_legacy.yml'); + + $this->externalBuildContainer($containerBuilder); + + $containerBuilder->setParameter( + 'ibexa.persistence.legacy.dsn', + self::$dsn + ); + + $storageParam = $containerBuilder->hasParameter('ibexa.io.dir.storage') + ? $containerBuilder->getParameter('ibexa.io.dir.storage') + : null; + $storageDir = is_string($storageParam) ? $storageParam : 'storage'; + $containerBuilder->setParameter( + 'ibexa.io.dir.root', + self::$ioRootDir . '/' . $storageDir + ); + + $containerBuilder->addCompilerPass(new Compiler\Search\FieldRegistryPass()); + $containerBuilder->registerForAutoconfiguration(FilteringCriterionQueryBuilder::class) + ->addTag(ServiceTags::FILTERING_CRITERION_QUERY_BUILDER); + $containerBuilder->registerForAutoconfiguration(FilteringSortClauseQueryBuilder::class) + ->addTag(ServiceTags::FILTERING_SORT_CLAUSE_QUERY_BUILDER); + + $loader->load('override.yml'); - return parent::getServiceContainer(); + self::$serviceContainer = new ServiceContainer( + $containerBuilder, + $installDir, + self::getCacheDir(), + true, + true + ); + + return self::$serviceContainer; } - protected function externalBuildContainer(ContainerBuilder $containerBuilder) + protected function externalBuildContainer(ContainerBuilder $containerBuilder): void { - parent::externalBuildContainer($containerBuilder); - $loader = new YamlFileLoader( $containerBuilder, new FileLocator(__DIR__ . '/Resources/config') From 8c8e690298873031b6ad39ed423dce2a15205450 Mon Sep 17 00:00:00 2001 From: mikolaj Date: Thu, 4 Dec 2025 15:26:53 +0100 Subject: [PATCH 18/19] Rebuilt service container to ensure test-only services are loaded --- .../Repository/Filtering/LegacyFilteringSetupFactory.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/integration/Core/Repository/Filtering/LegacyFilteringSetupFactory.php b/tests/integration/Core/Repository/Filtering/LegacyFilteringSetupFactory.php index 9a6d0b2283..cc0c992b9f 100644 --- a/tests/integration/Core/Repository/Filtering/LegacyFilteringSetupFactory.php +++ b/tests/integration/Core/Repository/Filtering/LegacyFilteringSetupFactory.php @@ -24,9 +24,8 @@ final class LegacyFilteringSetupFactory extends Legacy { public function getServiceContainer(): ServiceContainer { - if (self::$serviceContainer instanceof ServiceContainer) { - return self::$serviceContainer; - } + // Always rebuild to ensure test-only services are loaded, regardless of previous caches. + self::$serviceContainer = null; $installDir = self::getInstallationDir(); From 89af2c884cf4ba332debbea2b6346249481bc830 Mon Sep 17 00:00:00 2001 From: mikolaj Date: Thu, 4 Dec 2025 16:33:17 +0100 Subject: [PATCH 19/19] Refactored LegacyContentFilteringTest to use RepositoryTestCase and improve folder creation logic --- .../Filtering/ContentFilteringTest.php | 2 - .../Filtering/LegacyContentFilteringTest.php | 63 +++++++++---- .../Filtering/LegacyFilteringSetupFactory.php | 90 ------------------- .../Repository/Filtering/LegacyTestKernel.php | 22 +++++ 4 files changed, 67 insertions(+), 110 deletions(-) delete mode 100644 tests/integration/Core/Repository/Filtering/LegacyFilteringSetupFactory.php create mode 100644 tests/integration/Core/Repository/Filtering/LegacyTestKernel.php diff --git a/tests/integration/Core/Repository/Filtering/ContentFilteringTest.php b/tests/integration/Core/Repository/Filtering/ContentFilteringTest.php index 11e159b492..52ef1722c9 100644 --- a/tests/integration/Core/Repository/Filtering/ContentFilteringTest.php +++ b/tests/integration/Core/Repository/Filtering/ContentFilteringTest.php @@ -180,8 +180,6 @@ public function testLocationSortClausesStayDeterministicWithComplexCriteria(): v $locationService->newLocationCreateStruct(2) ); - $referenceLocation = $this->loadMainLocation($locationService, $referenceContent); - $middleLocation = $this->loadMainLocation($locationService, $middleContent); $mainLocation = $this->loadMainLocation($locationService, $contentWithAdditionalLocation); $nonMainLocations = []; foreach ($locationService->loadLocations($contentWithAdditionalLocation->contentInfo) as $location) { diff --git a/tests/integration/Core/Repository/Filtering/LegacyContentFilteringTest.php b/tests/integration/Core/Repository/Filtering/LegacyContentFilteringTest.php index 50df253518..641b10ddfa 100644 --- a/tests/integration/Core/Repository/Filtering/LegacyContentFilteringTest.php +++ b/tests/integration/Core/Repository/Filtering/LegacyContentFilteringTest.php @@ -9,13 +9,12 @@ namespace Ibexa\Tests\Integration\Core\Repository\Filtering; use function array_map; +use Ibexa\Contracts\Core\Repository\Values\Content\Content; use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; use Ibexa\Contracts\Core\Repository\Values\Filter\Filter; -use Ibexa\Contracts\Core\Test\Repository\SetupFactory; -use Ibexa\Tests\Core\Repository\Filtering\TestContentProvider; -use Ibexa\Tests\Integration\Core\Repository\BaseTest; use Ibexa\Tests\Integration\Core\Repository\Filtering\Fixtures\LegacyLocationSortClause; +use Ibexa\Tests\Integration\Core\RepositoryTestCase; use function iterator_to_array; /** @@ -23,19 +22,21 @@ * * @group repository */ -final class LegacyContentFilteringTest extends BaseTest +final class LegacyContentFilteringTest extends RepositoryTestCase { - private TestContentProvider $contentProvider; - - protected function setUp(): void - { - parent::setUp(); - $this->contentProvider = new TestContentProvider($this->getRepository(), $this); - } - public function testLegacyLocationSortClause(): void { - $parentFolder = $this->contentProvider->createSharedContentStructure(); + $parentFolder = $this->createFolderWithRemoteId('legacy-parent', 'Legacy Parent'); + $folder1 = $this->createFolderWithRemoteId( + 'legacy-folder-1', + 'Legacy Folder 1', + (int)$parentFolder->getContentInfo()->getMainLocationId() + ); + $folder2 = $this->createFolderWithRemoteId( + 'legacy-folder-2', + 'Legacy Folder 2', + (int)$parentFolder->getContentInfo()->getMainLocationId() + ); $filter = (new Filter()) ->withCriterion( @@ -46,7 +47,8 @@ public function testLegacyLocationSortClause(): void ) ->withSortClause(new LegacyLocationSortClause(Query::SORT_ASC)); - $list = $this->getRepository()->getContentService()->find($filter, []); + $contentService = self::getContentService(); + $list = $contentService->find($filter, []); self::assertCount(2, $list); $remoteIds = array_map( @@ -55,15 +57,40 @@ public function testLegacyLocationSortClause(): void ); self::assertSame( [ - TestContentProvider::CONTENT_REMOTE_IDS['folder1'], - TestContentProvider::CONTENT_REMOTE_IDS['folder2'], + $folder1->getContentInfo()->remoteId, + $folder2->getContentInfo()->remoteId, ], $remoteIds ); } - protected function getSetupFactory(): SetupFactory + protected static function getKernelClass(): string { - return new LegacyFilteringSetupFactory(); + return LegacyTestKernel::class; + } + + private function createFolderWithRemoteId( + string $remoteId, + string $name, + int $parentLocationId = self::CONTENT_TREE_ROOT_ID + ): Content { + $contentService = self::getContentService(); + $contentTypeService = self::getContentTypeService(); + $locationService = self::getLocationService(); + + /** @var \Ibexa\Contracts\Core\Repository\Values\ContentType\ContentType $folderType */ + $folderType = $contentTypeService->loadContentTypeByIdentifier('folder'); + $createStruct = $contentService->newContentCreateStruct($folderType, 'eng-GB'); + $createStruct->setField('name', $name, 'eng-GB'); + $createStruct->remoteId = $remoteId; + + $locationCreateStruct = $locationService->newLocationCreateStruct($parentLocationId); + + $draft = $contentService->createContent( + $createStruct, + [$locationCreateStruct] + ); + + return $contentService->publishVersion($draft->versionInfo); } } diff --git a/tests/integration/Core/Repository/Filtering/LegacyFilteringSetupFactory.php b/tests/integration/Core/Repository/Filtering/LegacyFilteringSetupFactory.php deleted file mode 100644 index cc0c992b9f..0000000000 --- a/tests/integration/Core/Repository/Filtering/LegacyFilteringSetupFactory.php +++ /dev/null @@ -1,90 +0,0 @@ -getCoreLoader(); - $loader->load('search_engines/legacy.yml'); - $loader->load('integration_legacy.yml'); - - $this->externalBuildContainer($containerBuilder); - - $containerBuilder->setParameter( - 'ibexa.persistence.legacy.dsn', - self::$dsn - ); - - $storageParam = $containerBuilder->hasParameter('ibexa.io.dir.storage') - ? $containerBuilder->getParameter('ibexa.io.dir.storage') - : null; - $storageDir = is_string($storageParam) ? $storageParam : 'storage'; - $containerBuilder->setParameter( - 'ibexa.io.dir.root', - self::$ioRootDir . '/' . $storageDir - ); - - $containerBuilder->addCompilerPass(new Compiler\Search\FieldRegistryPass()); - $containerBuilder->registerForAutoconfiguration(FilteringCriterionQueryBuilder::class) - ->addTag(ServiceTags::FILTERING_CRITERION_QUERY_BUILDER); - $containerBuilder->registerForAutoconfiguration(FilteringSortClauseQueryBuilder::class) - ->addTag(ServiceTags::FILTERING_SORT_CLAUSE_QUERY_BUILDER); - - $loader->load('override.yml'); - - self::$serviceContainer = new ServiceContainer( - $containerBuilder, - $installDir, - self::getCacheDir(), - true, - true - ); - - return self::$serviceContainer; - } - - protected function externalBuildContainer(ContainerBuilder $containerBuilder): void - { - $loader = new YamlFileLoader( - $containerBuilder, - new FileLocator(__DIR__ . '/Resources/config') - ); - $loader->load('services/legacy_sort_clause.yaml'); - - if ($containerBuilder->hasDefinition(LegacyLocationSortQueryBuilder::class)) { - $containerBuilder - ->getDefinition(LegacyLocationSortQueryBuilder::class) - ->addTag(ServiceTags::FILTERING_SORT_CLAUSE_QUERY_BUILDER); - } else { - $containerBuilder - ->register(LegacyLocationSortQueryBuilder::class, LegacyLocationSortQueryBuilder::class) - ->addTag(ServiceTags::FILTERING_SORT_CLAUSE_QUERY_BUILDER); - } - } -} diff --git a/tests/integration/Core/Repository/Filtering/LegacyTestKernel.php b/tests/integration/Core/Repository/Filtering/LegacyTestKernel.php new file mode 100644 index 0000000000..17f1a93366 --- /dev/null +++ b/tests/integration/Core/Repository/Filtering/LegacyTestKernel.php @@ -0,0 +1,22 @@ +load(__DIR__ . '/Resources/config/services/legacy_sort_clause.yaml'); + } +}