Skip to content

Comments

Nested subqueries#822

Open
abnegate wants to merge 10 commits intomainfrom
fix-performance
Open

Nested subqueries#822
abnegate wants to merge 10 commits intomainfrom
fix-performance

Conversation

@abnegate
Copy link
Member

@abnegate abnegate commented Feb 24, 2026

Summary by CodeRabbit

  • New Features
    • Added internal subquery-style relationship filters to enable declarative relationship queries.
  • Performance
    • Batch processing for document fetches and relationship resolution to reduce latency and round-trips.
  • Chores
    • Slimmed container build for a smaller runtime image.
    • Added a standalone many-to-many benchmark and integrated M2M benchmarks into the benchmark workflow.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 24, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds internal subquery support and batched many-to-many relationship resolution across the database layer, extends adapters/Pool with subquery and junction-mapping APIs, implements SQL subquery rendering and junction retrieval, refactors Database for batch casting/subquery paths, and adds M2M benchmarking scripts.

Changes

Cohort / File(s) Summary
Query types & factories
src/Database/Query.php
Adds internal subquery constants and factory methods (TYPE_IN_SUBQUERY, TYPE_IN_SUBQUERY_ALL, inSubquery, inSubqueryAll, inSubqueryDirect).
Adapter base & Pool delegation
src/Database/Adapter.php, src/Database/Adapter/Pool.php
Adds getSupportForSubqueries() and findJunctionMapping() to Adapter; Pool delegates both to underlying adapter.
SQL adapter implementations
src/Database/Adapter/SQL.php, src/Database/Adapter/MariaDB.php, src/Database/Adapter/Postgres.php
Adds subquery condition rendering (renderConditionsForSubquery, getSQLSubqueryCondition), routes IN_SUBQUERY cases to handler, implements findJunctionMapping (chunked queries, tenant scoping), and advertises subquery support.
Non-SQL adapter
src/Database/Adapter/Mongo.php
Adds getSupportForSubqueries() returning false (capability flag).
Core Database logic
src/Database/Database.php
Adds castAndDecodeBatch, refactors getDocument/find to use batch decoding, emits subquery descriptors in relationship resolution paths, and uses subquery-backed M2M population when supported.
Benchmarks & tasks
bin/bench_m2m.php, bin/tasks/relationships.php
Adds standalone M2M benchmark script and integrates benchmarkM2M() into task outputs and aggregated results.
Build config
Dockerfile
Simplifies base image and removes multiple extension build stages and related runtime wiring.

Sequence Diagram

sequenceDiagram
    participant Client as Client
    participant DB as Database
    participant Pool as Adapter/Pool
    participant SQL as SQL Engine
    participant Junction as JunctionCollection

    Note over Client,DB: High-level M2M resolution (subquery vs mapping)
    Client->>DB: find(collection, filters with M2M)
    DB->>DB: convertRelationshipQueries()
    DB->>Pool: getSupportForSubqueries()
    alt Pool supports subqueries
        DB->>Pool: emit inSubquery / inSubqueryDirect descriptor
        Pool->>SQL: execute inline subquery
        SQL-->>Pool: matching IDs
        Pool-->>DB: subquery-backed condition
    else Pool does not support subqueries
        DB->>Pool: findJunctionMapping(junction, parentCol, childCol, parentIds)
        Pool->>Junction: SELECT junction rows (chunked)
        Junction-->>Pool: junction mappings
        Pool-->>DB: mapping rows
        DB->>DB: build filters from mappings
    end
    DB->>Pool: final find on target collection
    Pool-->>DB: raw documents
    DB->>DB: castAndDecodeBatch()
    DB-->>Client: returned documents
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • Feat relationship updates #724: Overlapping changes for batched relationship population, subquery-based relationship resolution, and adapter APIs (getSupportForSubqueries/findJunctionMapping).
  • Feat mongo tmp #647: Related adapter surface adjustments including Mongo adapter capability flags.
  • Feat mongodb #721: Changes touching adapter/SQL/Database/Query layers that intersect with subquery and junction mapping logic.

Suggested reviewers

  • fogelito
  • ArnabChatterjee20k

Poem

🐰 I hopped through junction rows at night,
I stitched subqueries snug and tight,
Batches hummed and IDs took flight,
Benchmarks thumped at morning light,
Hooray — M2M now feels just right! 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Nested subqueries' directly relates to the PR's primary objective of introducing subquery support throughout the database layer, which is the main change across all modified files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix-performance

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/Database/Database.php (2)

9781-9857: ⚠️ Potential issue | 🔴 Critical

Fix phpstan failures: make resolveRelationshipGroupToIds() return shapes explicit before indexing ids.

CodeQL/phpstan is right: $result is typed with optional ids, but you index it at Line 9824 and Line 9932. Even though your control-flow intends “non-subquery => ids exist”, the type doesn’t guarantee it.

This should be fixed before merge because it’s already breaking CI.

Proposed fix (tighten typing + guard before indexing)
@@
-            foreach ($query->getValues() as $value) {
+            foreach ($query->getValues() as $value) {
                 $relatedQuery = Query::equal($nestedAttribute, [$value]);
                 $result = $this->resolveRelationshipGroupToIds($relationship, [$relatedQuery], $collection);
@@
                 if (isset($result['subquery'])) {
                     $additionalQueries[] = $this->createSubqueryFromResult($result);
                     $usedSubqueries = true;
                 } else {
+                    if (!isset($result['ids'])) {
+                        throw new \LogicException('Expected "ids" for non-subquery relationship resolution result.');
+                    }
                     $resolvedAttribute = $result['attribute'];
                     $parentIdSets[] = $result['ids'];
                 }
             }
@@
-                if (isset($result['subquery'])) {
+                if (isset($result['subquery'])) {
                     $additionalQueries[] = $this->createSubqueryFromResult($result);
                 } else {
+                    if (!isset($result['ids'])) {
+                        throw new \LogicException('Expected "ids" for non-subquery relationship resolution result.');
+                    }
                     $additionalQueries[] = Query::equal($result['attribute'], $result['ids']);
                 }

Also applies to: 9929-9934

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Database/Database.php` around lines 9781 - 9857, The phpstan error is
caused by indexing $result['ids'] without guaranteeing the 'ids' key exists from
resolveRelationshipGroupToIds(); update resolveRelationshipGroupToIds()
PHPDoc/return annotation to an explicit shape (e.g. array{ids?: array,
attribute?: string, subquery?: array}|null) and add guards wherever $result is
used (in the loop inside the method handling $query->getValues()) — specifically
before accessing $result['ids'] (replace direct indexing at the spots using
$result['ids'] with a check like if (!isset($result['ids'])) { return null; } or
handle the subquery case first) so the code only indexes 'ids' when present and
phpstan's typing matches the runtime checks.

9997-10176: ⚠️ Potential issue | 🟠 Major

Subquery path bypasses find() conversion/typing — convert relationship queries against the related collection before embedding in SQL.

Right now, when $canUseSubquery is true you return subquery “conditions” built from $relatedQueries (Line 10063-10066, 10112-10116, 10159-10162), but those $relatedQueries were constructed from the parent query set and have not been run through convertQueries() using the related collection’s attributes/indexes/type rules.

This can change results for at least:

  • datetime queries (timezone normalization done in convertQuery() for VAR_DATETIME)
  • object nested queries (where convertQuery() sets attribute type / object handling)
  • any adapter-specific query typing needed to render correct SQL conditions

You already special-case spatial queries (Line 9997-10015). Datetime/object typing needs similar care, but can likely be handled by converting the query objects rather than disabling subqueries.

Proposed fix (convert queries for related collection when using subqueries)
@@
-        $canUseSubquery = $this->adapter->getSupportForSubqueries();
+        $canUseSubquery = $this->adapter->getSupportForSubqueries();
         if ($canUseSubquery) {
@@
             foreach ($relatedQueries as $rq) {
                 if (\in_array($rq->getMethod(), $spatialMethods)) {
                     $canUseSubquery = false;
                     break;
                 }
             }
         }
+
+        // If we will emit a subquery, ensure conditions are converted/typed against the related collection schema.
+        // This mirrors the normal find() pipeline for value normalization (e.g. datetime) and attribute typing.
+        $relatedCollectionDoc = null;
+        if ($canUseSubquery) {
+            $relatedCollectionDoc = $this->silent(fn () => $this->getCollection($relatedCollection));
+            $relatedQueries = $this->convertQueries($relatedCollectionDoc, $relatedQueries);
+        }
@@
-            $relatedCollectionDoc = $this->silent(fn () => $this->getCollection($relatedCollection));
+            $relatedCollectionDoc ??= $this->silent(fn () => $this->getCollection($relatedCollection));
             $junction = $this->getJunctionCollection($collection, $relatedCollectionDoc, $side);

Based on learnings: “Relationship selects are always evaluated in the main alias context… preserve the root of dotted paths and any projection alias…”.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Database/Database.php` around lines 9997 - 10176, The subquery branches
embed $relatedQueries directly without running them through
convertQueries/convertQuery for the related collection, so convert the related
queries before building any subquery conditions: call the conversion helper
(e.g. $convertedRelated = $this->convertQueries($relatedCollection,
$relatedQueries) or per-query $this->convertQuery(...)) and use
$convertedRelated in the three places that return a 'subquery' with 'conditions'
(the nested subquery with Query::inSubqueryDirect, the many-to-many junction
subquery branch returning 'conditions', and the simple FK resolution branch
returning 'conditions') so that datetime/object typing and adapter-specific
typing are applied while preserving dotted-path roots/projection aliases.
🧹 Nitpick comments (3)
bin/tasks/relationships.php (1)

461-569: Consider isolating query-filter performance from relationship hydration.
These M2M timings may be dominated by relationship expansion. If the intent is to measure the subquery filter path, wrap the timed find() calls in skipRelationships() to reduce noise.

♻️ Suggested adjustment (pattern example)
-    $docs = $database->find('authors', [
-        Query::equal('articles.$id', [$singleId]),
-        Query::limit(5000),
-    ]);
+    $docs = $database->skipRelationships(fn () => $database->find('authors', [
+        Query::equal('articles.$id', [$singleId]),
+        Query::limit(5000),
+    ]));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bin/tasks/relationships.php` around lines 461 - 569, The benchmarkM2M timings
include relationship hydration, so wrap the timed and warmup Query::find() calls
in $database->skipRelationships(...) to isolate filter/subquery cost: update the
blocks that call $database->find(...) for the equal_single, equal_multi,
containsAll and reverse_equal tests (both warmup and timed loops) to use
$database->skipRelationships(fn () => $database->find(...)) while leaving the
initial ID-selection calls as-is; reference the benchmarkM2M function and the
Query::equal/Query::containsAll invocations to locate each find() to change.
src/Database/Database.php (2)

10179-10206: createSubqueryFromResult() is a nice consolidation; consider hardening the subquery payload schema.

Given how dynamic the $sub array is, it may be worth adding minimal validation (e.g., required keys present) to fail fast with a clearer error if a caller accidentally returns a malformed subquery payload (especially as more subquery “modes” get added over time).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Database/Database.php` around lines 10179 - 10206, The
createSubqueryFromResult function trusts a dynamic $sub payload; add minimal
validation at the top of createSubqueryFromResult(array $result): Query to
assert required keys exist and have expected types before calling
Query::inSubqueryDirect or Query::inSubquery. Specifically, validate that
$result['attribute'] is set and that $sub contains 'table' and 'column'; if
using the 'conditions' branch ensure 'conditions' is present and is an array,
and for the other branch ensure 'filterColumn' and 'filterValues' exist (and
that 'filterValues' is iterable); throw a clear InvalidArgumentException (or
similar) with a message including the missing key(s) so callers fail fast and
debugging is easier.

8806-9000: castAndDecodeBatch() is a correctness hotspot — ensure it matches casting() + decode() semantics across adapters.

A few specific risks in this implementation:

  • castingAfter() is always called (Line 8906) even when $supportsCasting is false; if castingAfter() does more than casting, that’s fine, but if it assumes a prior castingBefore()/DB-specific shape, this could be adapter-sensitive.
  • The method selectively processes only “work attributes” (Line 8878-8887). That’s great for perf, but it increases the chance that some transformation previously done in casting()/decode() is skipped for certain types/filters/edge cases (especially where adapters return filtered column names or where attributes have implicit filters).

Given how central this is (it now gates all find() results), I’d strongly recommend adding targeted tests for:

  • filtered key fallback ($this->adapter->filter($key))
  • array attributes stored as JSON strings
  • selections containing dotted paths (relationship/object nested selection patterns)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Database/Database.php` around lines 8806 - 9000, castAndDecodeBatch can
diverge from the original casting()/decode() semantics (castingAfter called
unconditionally and selective "workAttributes" may skip transformations), so
update it to preserve adapter semantics: only call
$this->adapter->castingAfter(...) when $this->adapter->getSupportForCasting() is
true or when adapter docs state castingAfter must always run; ensure keys
filtered by $this->adapter->filter($key) are handled exactly like
casting()/decode() by re-checking attributes omitted from $workAttributes
(especially attributes with implicit filters or adapter-mapped names) and by
reusing decodeAttribute(...) for all filtered-key fallbacks and JSON-array
handling (string->json) so behavior matches decode(); add a clear pass to apply
casting rules from getSupportForCasting() and to run decodeAttribute for
dotted/relationship selections before createDocumentInstance(...) so selections
and relationship renames match previous find() behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@bin/bench_m2m.php`:
- Around line 146-151: The printf call and other multiline function calls
(notably bench(...)) violate PSR-12 method_argument_space by placing multiple
arguments on a single line; update each multiline invocation (e.g., the printf
at the shown diff and all bench(...) calls) so each argument is on its own line,
with the opening parenthesis on the same line as the function name and the
closing parenthesis and semicolon on their own line; ensure alignment matches
PSR-12 (one argument per line) for all affected calls in the file.

In `@src/Database/Adapter.php`:
- Around line 1574-1608: The docblock for findJunctionMapping promises
array<array{string, string}> but getAttribute() returns mixed, causing phpstan
errors; update the method to coerce/validate both values to string before
pushing into $mappings (use the $parentColumn/$childColumn keys and the $results
from find(), e.g. retrieve $doc->getAttribute(...) and cast or explicitly
validate and handle nulls/other types) so the returned arrays always contain
string values, and keep or adjust the docblock accordingly if you choose to
allow nulls instead of casting.

In `@src/Database/Adapter/SQL.php`:
- Around line 2353-2379: The HAVING COUNT(DISTINCT ...) comparison uses the raw
$filterValues length which breaks when inputs contain duplicates; before
building placeholders for the Query::TYPE_IN_SUBQUERY_ALL branch deduplicate and
reindex $filterValues (e.g. array_unique + array_values), handle the case it
becomes empty (return '1 = 0'), then build $valuePlaceholders and $binds from
the deduplicated list and set $binds[$countKey] to count($filterValues) so the
HAVING uses the unique count.

---

Outside diff comments:
In `@src/Database/Database.php`:
- Around line 9781-9857: The phpstan error is caused by indexing $result['ids']
without guaranteeing the 'ids' key exists from resolveRelationshipGroupToIds();
update resolveRelationshipGroupToIds() PHPDoc/return annotation to an explicit
shape (e.g. array{ids?: array, attribute?: string, subquery?: array}|null) and
add guards wherever $result is used (in the loop inside the method handling
$query->getValues()) — specifically before accessing $result['ids'] (replace
direct indexing at the spots using $result['ids'] with a check like if
(!isset($result['ids'])) { return null; } or handle the subquery case first) so
the code only indexes 'ids' when present and phpstan's typing matches the
runtime checks.
- Around line 9997-10176: The subquery branches embed $relatedQueries directly
without running them through convertQueries/convertQuery for the related
collection, so convert the related queries before building any subquery
conditions: call the conversion helper (e.g. $convertedRelated =
$this->convertQueries($relatedCollection, $relatedQueries) or per-query
$this->convertQuery(...)) and use $convertedRelated in the three places that
return a 'subquery' with 'conditions' (the nested subquery with
Query::inSubqueryDirect, the many-to-many junction subquery branch returning
'conditions', and the simple FK resolution branch returning 'conditions') so
that datetime/object typing and adapter-specific typing are applied while
preserving dotted-path roots/projection aliases.

---

Nitpick comments:
In `@bin/tasks/relationships.php`:
- Around line 461-569: The benchmarkM2M timings include relationship hydration,
so wrap the timed and warmup Query::find() calls in
$database->skipRelationships(...) to isolate filter/subquery cost: update the
blocks that call $database->find(...) for the equal_single, equal_multi,
containsAll and reverse_equal tests (both warmup and timed loops) to use
$database->skipRelationships(fn () => $database->find(...)) while leaving the
initial ID-selection calls as-is; reference the benchmarkM2M function and the
Query::equal/Query::containsAll invocations to locate each find() to change.

In `@src/Database/Database.php`:
- Around line 10179-10206: The createSubqueryFromResult function trusts a
dynamic $sub payload; add minimal validation at the top of
createSubqueryFromResult(array $result): Query to assert required keys exist and
have expected types before calling Query::inSubqueryDirect or Query::inSubquery.
Specifically, validate that $result['attribute'] is set and that $sub contains
'table' and 'column'; if using the 'conditions' branch ensure 'conditions' is
present and is an array, and for the other branch ensure 'filterColumn' and
'filterValues' exist (and that 'filterValues' is iterable); throw a clear
InvalidArgumentException (or similar) with a message including the missing
key(s) so callers fail fast and debugging is easier.
- Around line 8806-9000: castAndDecodeBatch can diverge from the original
casting()/decode() semantics (castingAfter called unconditionally and selective
"workAttributes" may skip transformations), so update it to preserve adapter
semantics: only call $this->adapter->castingAfter(...) when
$this->adapter->getSupportForCasting() is true or when adapter docs state
castingAfter must always run; ensure keys filtered by
$this->adapter->filter($key) are handled exactly like casting()/decode() by
re-checking attributes omitted from $workAttributes (especially attributes with
implicit filters or adapter-mapped names) and by reusing decodeAttribute(...)
for all filtered-key fallbacks and JSON-array handling (string->json) so
behavior matches decode(); add a clear pass to apply casting rules from
getSupportForCasting() and to run decodeAttribute for dotted/relationship
selections before createDocumentInstance(...) so selections and relationship
renames match previous find() behavior.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ba1ee9c and 4580ba2.

📒 Files selected for processing (10)
  • bin/bench_m2m.php
  • bin/tasks/relationships.php
  • src/Database/Adapter.php
  • src/Database/Adapter/MariaDB.php
  • src/Database/Adapter/Mongo.php
  • src/Database/Adapter/Pool.php
  • src/Database/Adapter/Postgres.php
  • src/Database/Adapter/SQL.php
  • src/Database/Database.php
  • src/Database/Query.php

Comment on lines 1574 to 1608
/**
* Fetch junction table mappings as lightweight arrays (no Document hydration).
*
* Returns array of [parentColumn => parentId, childColumn => childId] rows.
* Override in SQL adapters for maximum performance; default falls back to find().
*
* @param Document $collection Junction collection document
* @param string $parentColumn Column name for parent FK
* @param string $childColumn Column name for child FK
* @param array<string> $parentIds Parent document IDs to look up
* @return array<array{string, string}>
*/
public function findJunctionMapping(Document $collection, string $parentColumn, string $childColumn, array $parentIds): array
{
// Default: use find() with select (adapters can override for raw performance)
$results = $this->find(
$collection,
[
Query::equal($parentColumn, $parentIds),
Query::select([$parentColumn, $childColumn]),
Query::limit(PHP_INT_MAX),
],
PHP_INT_MAX
);

$mappings = [];
foreach ($results as $doc) {
$mappings[] = [
$parentColumn => $doc->getAttribute($parentColumn),
$childColumn => $doc->getAttribute($childColumn),
];
}

return $mappings;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fix phpstan return-type mismatch in findJunctionMapping().
The new default implementation returns array<array<string, mixed>> (because getAttribute() is mixed), but the docblock requires array<array{string, string}>, and CI already flags this. Please align the return type by validating/casting to string (or relaxing the docblock if null is valid).

🛠️ Suggested fix (type-safe mapping + docblock)
-     * `@return` array<array{string, string}>
+     * `@return` array<array<string, string>>
...
-        foreach ($results as $doc) {
-            $mappings[] = [
-                $parentColumn => $doc->getAttribute($parentColumn),
-                $childColumn => $doc->getAttribute($childColumn),
-            ];
-        }
+        foreach ($results as $doc) {
+            $parentId = $doc->getAttribute($parentColumn);
+            $childId = $doc->getAttribute($childColumn);
+            if ($parentId === null || $childId === null) {
+                continue;
+            }
+            $mappings[] = [
+                $parentColumn => (string) $parentId,
+                $childColumn => (string) $childId,
+            ];
+        }
🧰 Tools
🪛 GitHub Actions: CodeQL

[error] 1607-1607: phpstan: Method Utopia\Database\Adapter::findJunctionMapping() should return array<array{string, string}> but returns array<int<0, max>, non-empty-array<string, mixed>>.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Database/Adapter.php` around lines 1574 - 1608, The docblock for
findJunctionMapping promises array<array{string, string}> but getAttribute()
returns mixed, causing phpstan errors; update the method to coerce/validate both
values to string before pushing into $mappings (use the
$parentColumn/$childColumn keys and the $results from find(), e.g. retrieve
$doc->getAttribute(...) and cast or explicitly validate and handle nulls/other
types) so the returned arrays always contain string values, and keep or adjust
the docblock accordingly if you choose to allow nulls instead of casting.

Comment on lines +2353 to +2379
// Literal filter values
$filterCol = $this->quote($this->filter($meta['filterColumn']));
$filterValues = $meta['filterValues'];

if (empty($filterValues)) {
return '1 = 0';
}

$valuePlaceholders = [];
foreach ($filterValues as $i => $val) {
$key = ":{$placeholder}_sub_{$i}";
$binds[$key] = $val;
$valuePlaceholders[] = $key;
}

$inClause = implode(', ', $valuePlaceholders);

if ($query->getMethod() === Query::TYPE_IN_SUBQUERY_ALL) {
$countKey = ":{$placeholder}_sub_count";
$binds[$countKey] = count($filterValues);
return "{$alias}.{$attribute} IN (" .
"SELECT {$selectCol}" .
" FROM {$subqueryTable}" .
" WHERE {$filterCol} IN ({$inClause}){$tenantCondition}" .
" GROUP BY {$selectCol}" .
" HAVING COUNT(DISTINCT {$filterCol}) = {$countKey}" .
")";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Deduplicate filter values for TYPE_IN_SUBQUERY_ALL to avoid false negatives.

COUNT(DISTINCT ...) is compared to the raw filterValues count; duplicates in the input will make the HAVING clause fail even when matches exist. Consider normalizing the list before building placeholders and the count.

🛠️ Suggested fix
-        $filterValues = $meta['filterValues'];
+        $filterValues = array_values(array_unique($meta['filterValues'], SORT_REGULAR));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Literal filter values
$filterCol = $this->quote($this->filter($meta['filterColumn']));
$filterValues = $meta['filterValues'];
if (empty($filterValues)) {
return '1 = 0';
}
$valuePlaceholders = [];
foreach ($filterValues as $i => $val) {
$key = ":{$placeholder}_sub_{$i}";
$binds[$key] = $val;
$valuePlaceholders[] = $key;
}
$inClause = implode(', ', $valuePlaceholders);
if ($query->getMethod() === Query::TYPE_IN_SUBQUERY_ALL) {
$countKey = ":{$placeholder}_sub_count";
$binds[$countKey] = count($filterValues);
return "{$alias}.{$attribute} IN (" .
"SELECT {$selectCol}" .
" FROM {$subqueryTable}" .
" WHERE {$filterCol} IN ({$inClause}){$tenantCondition}" .
" GROUP BY {$selectCol}" .
" HAVING COUNT(DISTINCT {$filterCol}) = {$countKey}" .
")";
// Literal filter values
$filterCol = $this->quote($this->filter($meta['filterColumn']));
$filterValues = array_values(array_unique($meta['filterValues'], SORT_REGULAR));
if (empty($filterValues)) {
return '1 = 0';
}
$valuePlaceholders = [];
foreach ($filterValues as $i => $val) {
$key = ":{$placeholder}_sub_{$i}";
$binds[$key] = $val;
$valuePlaceholders[] = $key;
}
$inClause = implode(', ', $valuePlaceholders);
if ($query->getMethod() === Query::TYPE_IN_SUBQUERY_ALL) {
$countKey = ":{$placeholder}_sub_count";
$binds[$countKey] = count($filterValues);
return "{$alias}.{$attribute} IN (" .
"SELECT {$selectCol}" .
" FROM {$subqueryTable}" .
" WHERE {$filterCol} IN ({$inClause}){$tenantCondition}" .
" GROUP BY {$selectCol}" .
" HAVING COUNT(DISTINCT {$filterCol}) = {$countKey}" .
")";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Database/Adapter/SQL.php` around lines 2353 - 2379, The HAVING
COUNT(DISTINCT ...) comparison uses the raw $filterValues length which breaks
when inputs contain duplicates; before building placeholders for the
Query::TYPE_IN_SUBQUERY_ALL branch deduplicate and reindex $filterValues (e.g.
array_unique + array_values), handle the case it becomes empty (return '1 = 0'),
then build $valuePlaceholders and $binds from the deduplicated list and set
$binds[$countKey] to count($filterValues) so the HAVING uses the unique count.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
Dockerfile (1)

67-68: ⚠️ Potential issue | 🔴 Critical

Missing COPY --from=pcov directive prevents pcov extension from reaching the final stage.

The pcov extension is built in a separate FROM compile AS pcov stage (lines 49–55) using make install, which installs the .so to that stage's filesystem. However, there is no COPY --from=pcov directive to transfer it to the final FROM compile AS final stage. The final stage is a fresh instance of the base image and will not include the pcov binary. Line 67 attempts to enable a non-existent extension, causing the container to fail on startup.

Add COPY --from=pcov /usr/local/lib/php/extensions/ /usr/local/lib/php/extensions/ (or equivalent path matching your build output) to the final stage before line 67.

The xdebug extension (line 68) is assumed to come from the base image appwrite/utopia-base:8.4-1.0.0. Verify it is included there; if not, a similar build stage and copy is needed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Dockerfile` around lines 67 - 68, The final stage enables pcov with "RUN echo
extension=pcov.so >> /usr/local/etc/php/conf.d/pcov.ini" but the built pcov .so
from the build stage named "pcov" isn't copied into the final image; add a COPY
--from=pcov directive to copy the built extension files (e.g., from
/usr/local/lib/php/extensions/) into the final stage's extension directory
before the RUN that enables pcov so the extension actually exists at runtime;
verify xdebug is present in the base image or apply the same copy pattern if it
was also built separately.
src/Database/Database.php (1)

9997-10164: ⚠️ Potential issue | 🟠 Major

Convert related-collection queries before embedding them in subqueries.
Right now the subquery path uses $relatedQueries without running convertQueries() for the related collection, so attribute typing/UTC normalization (e.g., datetime/object casting) can diverge from the non-subquery path that goes through find(). This can yield incorrect results when subqueries are enabled. Please normalize the related queries against the related collection before returning subquery conditions.

Suggested fix
-        $canUseSubquery = $this->adapter->getSupportForSubqueries();
+        $canUseSubquery = $this->adapter->getSupportForSubqueries();
+        if ($canUseSubquery) {
+            $relatedCollectionDoc = $this->silent(fn () => $this->getCollection($relatedCollection));
+            $relatedQueries = $this->convertQueries($relatedCollectionDoc, $relatedQueries);
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Database/Database.php` around lines 9997 - 10164, The subquery branches
use $relatedQueries directly without normalizing types; call convertQueries()
for the related collection and replace $relatedQueries with the converted
queries before constructing any 'subquery' arrays. Retrieve the related
collection document via $this->getCollection($relatedCollection) (as in
$relatedCollectionDoc), run $this->convertQueries($relatedCollectionDoc,
$relatedQueries) (wrapped in $this->silent() like other lookups), and use the
returned/converted queries in the Query::inSubqueryDirect call and in all places
that set 'conditions' for subqueries (the nested M2M subquery, the O2M/M2O
parent-resolution subquery, and the simple FK resolution subquery).
🧹 Nitpick comments (2)
Dockerfile (2)

1-1: Consider pinning the Composer image tag for reproducible builds.

Switching from composer:2.8 to composer:2 means builds will silently pick up new minor/patch versions. For CI reproducibility, a pinned minor (e.g., composer:2.8) is preferable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Dockerfile` at line 1, The Dockerfile currently uses an unpinned base image
"FROM composer:2 AS composer" which allows automatic minor/patch updates; update
that FROM directive to a pinned minor tag (for example "composer:2.8") to ensure
reproducible builds, and document the chosen pinned tag in the Dockerfile or
repo release notes so CI and local builds use the exact Composer version.

58-88: No non-root USER directive in the final stage.

Static analysis (Trivy DS-0002) flags that the container runs as root. Since this appears to be a development/testing Dockerfile (CMD ["tail", "-f", "/dev/null"]), this is likely acceptable — but worth noting if this image is ever used in less trusted environments.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Dockerfile` around lines 58 - 88, Add a non-root user in the final stage and
switch to it so the container does not run as root: create a user (e.g.,
appuser) and group with a fixed UID/GID, chown the workdir and important copied
paths (referenced by WORKDIR /usr/src/code and the copied paths COPY
--from=composer ... /usr/src/code/vendor, COPY ./bin, ./src, ./dev) to that
user, then add a USER appuser line before the CMD [ "tail", "-f", "/dev/null" ];
ensure the chown and USER commands are conditional/placed after files are copied
and that any debug-file removal logic (the DEBUG-related RUN lines) still runs
as root if needed before switching users.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Dockerfile`:
- Line 15: Update the Dockerfile's base image tag in the FROM instruction:
replace the incorrect image tag "appwrite/utopia-base:8.4-1.0.0" with the
correct tag "appwrite/utopia-base:php-8.4-1.0.0" so the FROM line uses the
existing image name.

---

Outside diff comments:
In `@Dockerfile`:
- Around line 67-68: The final stage enables pcov with "RUN echo
extension=pcov.so >> /usr/local/etc/php/conf.d/pcov.ini" but the built pcov .so
from the build stage named "pcov" isn't copied into the final image; add a COPY
--from=pcov directive to copy the built extension files (e.g., from
/usr/local/lib/php/extensions/) into the final stage's extension directory
before the RUN that enables pcov so the extension actually exists at runtime;
verify xdebug is present in the base image or apply the same copy pattern if it
was also built separately.

In `@src/Database/Database.php`:
- Around line 9997-10164: The subquery branches use $relatedQueries directly
without normalizing types; call convertQueries() for the related collection and
replace $relatedQueries with the converted queries before constructing any
'subquery' arrays. Retrieve the related collection document via
$this->getCollection($relatedCollection) (as in $relatedCollectionDoc), run
$this->convertQueries($relatedCollectionDoc, $relatedQueries) (wrapped in
$this->silent() like other lookups), and use the returned/converted queries in
the Query::inSubqueryDirect call and in all places that set 'conditions' for
subqueries (the nested M2M subquery, the O2M/M2O parent-resolution subquery, and
the simple FK resolution subquery).

---

Nitpick comments:
In `@Dockerfile`:
- Line 1: The Dockerfile currently uses an unpinned base image "FROM composer:2
AS composer" which allows automatic minor/patch updates; update that FROM
directive to a pinned minor tag (for example "composer:2.8") to ensure
reproducible builds, and document the chosen pinned tag in the Dockerfile or
repo release notes so CI and local builds use the exact Composer version.
- Around line 58-88: Add a non-root user in the final stage and switch to it so
the container does not run as root: create a user (e.g., appuser) and group with
a fixed UID/GID, chown the workdir and important copied paths (referenced by
WORKDIR /usr/src/code and the copied paths COPY --from=composer ...
/usr/src/code/vendor, COPY ./bin, ./src, ./dev) to that user, then add a USER
appuser line before the CMD [ "tail", "-f", "/dev/null" ]; ensure the chown and
USER commands are conditional/placed after files are copied and that any
debug-file removal logic (the DEBUG-related RUN lines) still runs as root if
needed before switching users.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4580ba2 and ecbd6fc.

📒 Files selected for processing (4)
  • Dockerfile
  • bin/bench_m2m.php
  • src/Database/Adapter.php
  • src/Database/Database.php
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/Database/Adapter.php
  • bin/bench_m2m.php

The tag format is php-8.4-1.0.0, not 8.4-1.0.0.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
Dockerfile (2)

32-41: ⚠️ Potential issue | 🔴 Critical

$PHP_MONGODB_VERSION is undefined — this breaks the build.

The old Dockerfile declared ENV PHP_MONGODB_VERSION=... explicitly; that block was removed when switching to appwrite/utopia-base, but the base image does not export this variable. As confirmed by the pipeline failure, pecl install mongodb-$PHP_MONGODB_VERSION expands to pecl install mongodb-, which is an invalid package name and halts the build.

Pin the version directly or introduce a build-arg with a default:

🔧 Proposed fix
+ARG PHP_MONGODB_VERSION=1.21.0
+
 RUN apk update && apk add --no-cache \
     ...
  && (pecl install mongodb-$PHP_MONGODB_VERSION \

Or pin inline to remove the variable dependency entirely:

- && (pecl install mongodb-$PHP_MONGODB_VERSION \
-     || (git clone --depth 1 --branch $PHP_MONGODB_VERSION --recurse-submodules https://github.com/mongodb/mongo-php-driver.git /tmp/mongodb \
+ && (pecl install mongodb-1.21.0 \
+     || (git clone --depth 1 --branch 1.21.0 --recurse-submodules https://github.com/mongodb/mongo-php-driver.git /tmp/mongodb \
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Dockerfile` around lines 32 - 41, The Dockerfile uses $PHP_MONGODB_VERSION in
the pecl install command but that build ARG/ENV is not defined, causing pecl
install to run with an empty suffix; fix by adding a default build argument or
environment variable (e.g., declare ARG PHP_MONGODB_VERSION=<version> or ENV
PHP_MONGODB_VERSION=<version> at top of the Dockerfile) or by pinning the driver
inline (replace pecl install mongodb-$PHP_MONGODB_VERSION with pecl install
mongodb-<fixed-version>); update the Dockerfile near the build-stage
declarations so the pecl install mongodb-$PHP_MONGODB_VERSION invocation (and
the fallback git clone branch name) use the defined value.

49-55: ⚠️ Potential issue | 🟠 Major

Orphaned pcov build stage causes PHP startup failure — missing artifact is never copied to final image.

The FROM compile AS pcov stage (lines 49–55) builds pcov.so, but the final stage contains no COPY --from=pcov instruction. Combined with the unconditional extension=pcov.so on line 67, this causes a runtime failure: PHP will attempt to load a .so file that does not exist in the final image.

Solution: Either remove the build stage entirely, or add the missing COPY instruction to transfer the compiled extension:

Option A — Remove the build stage (recommended if unused)
-## PCOV Extension
-FROM compile AS pcov
-RUN \
-   git clone --depth 1 https://github.com/krakjoe/pcov.git \
-   && cd pcov \
-   && phpize \
-   && ./configure --enable-pcov \
-   && make && make install
-
 FROM compile AS final
Option B — Copy the built extension
 FROM compile AS final
 
+COPY --from=pcov /usr/local/lib/php/extensions/ /usr/local/lib/php/extensions/
+
 LABEL maintainer="team@appwrite.io"

Note: A similar issue exists with xdebug.ini (line 68) — when DEBUG=false, the .so file is removed (line 87) but the .ini remains, also causing startup errors. Both should be conditionally created.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Dockerfile` around lines 49 - 55, The Dockerfile builds a pcov extension in
the "pcov" build stage but never copies the resulting pcov.so into the final
image while still unconditionally enabling "extension=pcov.so", causing PHP
startup failures; fix by either removing the "FROM compile AS pcov" stage if
pcov isn't used, or add a COPY --from=pcov to copy the compiled pcov.so into the
final image and ensure the php ini enabling it is only written when the .so
exists; likewise make creation of "xdebug.ini" conditional on DEBUG=true (or
mirror removal of the .so) so you don't leave an .ini that references a missing
.so.
🧹 Nitpick comments (1)
Dockerfile (1)

1-1: composer:2 is a floating tag — consider re-pinning to a minor version.

Switching from composer:2.8 to composer:2 means any new 2.x Composer release will be silently picked up on the next build, potentially causing unexpected behaviour. Pinning to at least a minor version (e.g. composer:2.8) restores reproducibility.

♻️ Proposed fix
-FROM composer:2 AS composer
+FROM composer:2.8 AS composer
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Dockerfile` at line 1, The Dockerfile uses a floating image tag "FROM
composer:2"; update this to a pinned minor version (e.g., change "FROM
composer:2" to "FROM composer:2.8" or another specific 2.x minor you have
tested) to ensure reproducible builds and avoid silently pulling new 2.x
Composer releases.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@Dockerfile`:
- Around line 32-41: The Dockerfile uses $PHP_MONGODB_VERSION in the pecl
install command but that build ARG/ENV is not defined, causing pecl install to
run with an empty suffix; fix by adding a default build argument or environment
variable (e.g., declare ARG PHP_MONGODB_VERSION=<version> or ENV
PHP_MONGODB_VERSION=<version> at top of the Dockerfile) or by pinning the driver
inline (replace pecl install mongodb-$PHP_MONGODB_VERSION with pecl install
mongodb-<fixed-version>); update the Dockerfile near the build-stage
declarations so the pecl install mongodb-$PHP_MONGODB_VERSION invocation (and
the fallback git clone branch name) use the defined value.
- Around line 49-55: The Dockerfile builds a pcov extension in the "pcov" build
stage but never copies the resulting pcov.so into the final image while still
unconditionally enabling "extension=pcov.so", causing PHP startup failures; fix
by either removing the "FROM compile AS pcov" stage if pcov isn't used, or add a
COPY --from=pcov to copy the compiled pcov.so into the final image and ensure
the php ini enabling it is only written when the .so exists; likewise make
creation of "xdebug.ini" conditional on DEBUG=true (or mirror removal of the
.so) so you don't leave an .ini that references a missing .so.

---

Nitpick comments:
In `@Dockerfile`:
- Line 1: The Dockerfile uses a floating image tag "FROM composer:2"; update
this to a pinned minor version (e.g., change "FROM composer:2" to "FROM
composer:2.8" or another specific 2.x minor you have tested) to ensure
reproducible builds and avoid silently pulling new 2.x Composer releases.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ecbd6fc and 80d00f9.

📒 Files selected for processing (1)
  • Dockerfile

abnegate and others added 2 commits February 25, 2026 01:40
The utopia-base image already includes mongodb, pdo_mysql, pdo_pgsql,
docker-cli, and docker-cli-compose. Only opcache and pgsql still need
to be installed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The base image may not include these drivers in the tagged version.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
Dockerfile (3)

52-52: ⚠️ Potential issue | 🟡 Minor

xdebug.ini retains extension=xdebug.so when DEBUG=false, causing a PHP startup warning.

Line 52 unconditionally writes extension=xdebug.so into xdebug.ini. When DEBUG=false, line 70 removes xdebug.so but does not touch xdebug.ini, so every PHP invocation emits a startup warning about the missing library. Clean up the ini when running without debug:

🔧 Suggested fix
-RUN if [ "$DEBUG" = "false" ]; then rm -f /usr/local/lib/php/extensions/no-debug-non-zts-20240924/xdebug.so; fi
+RUN if [ "$DEBUG" = "false" ]; then \
+      rm -f /usr/local/lib/php/extensions/no-debug-non-zts-20240924/xdebug.so \
+      && rm -f /usr/local/etc/php/conf.d/xdebug.ini; \
+    fi

Also applies to: 67-70

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Dockerfile` at line 52, The Dockerfile unconditionally appends "RUN echo
extension=xdebug.so >> /usr/local/etc/php/conf.d/xdebug.ini" which leaves
xdebug.ini present when DEBUG=false and causes startup warnings after the xdebug
extension is removed; change the Dockerfile so that the xdebug.ini file is only
created when DEBUG=true (wrap the echo in the same conditional that
installs/enables xdebug) or ensure the ini is removed when DEBUG=false (delete
/usr/local/etc/php/conf.d/xdebug.ini alongside the existing removal of the
xdebug .so), keeping the existing DEBUG check and the commands that remove the
extension in sync.

42-72: ⚠️ Potential issue | 🟡 Minor

Container runs as root — no USER directive present.

The final stage has no USER instruction; the container process runs as root. While acceptable for a CI/test environment, adding a non-root user is a security best practice (flagged by Trivy DS-0002).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Dockerfile` around lines 42 - 72, The Dockerfile final stage leaves the
container running as root; add a non-root user and switch to it to satisfy
DS-0002: create a dedicated system user/group (e.g., app/app) in the final stage
(use RUN to add group and user), ensure ownership of the application dirs (chown
-R /usr/src/code) after COPY, and then set USER to that non-root user (USER app)
before CMD; keep the DEBUG-related file ops in a place where the non-root user
has access (either perform chown after those RUNs or create the user earlier and
adjust permissions) so builds still succeed while avoiding root runtime.

32-42: ⚠️ Potential issue | 🟠 Major

Remove the orphaned pcov build stage.

Lines 33–39 build and install pcov.so, but the final stage (line 42) derives from compile, not from pcov, and there is no COPY --from=pcov anywhere. The evidence that xdebug.so is removed at line 70 without being copied in indicates both extensions come from the base image appwrite/utopia-base:php-8.4-1.0.0. The entire pcov build stage is dead code.

Remove lines 32–40
 && rm -rf /var/cache/apk/*

-## PCOV Extension
-FROM compile AS pcov
-RUN \
-   git clone --depth 1 https://github.com/krakjoe/pcov.git \
-   && cd pcov \
-   && phpize \
-   && ./configure --enable-pcov \
-   && make && make install
-

 FROM compile AS final
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Dockerfile` around lines 32 - 42, Remove the orphaned pcov build stage:
delete the "FROM compile AS pcov" stage and its block that clones/builds pcov
(the git clone, cd pcov, phpize, ./configure --enable-pcov, make && make
install) because the final image is built from "FROM compile AS final" and there
is no COPY --from=pcov; ensure only the "FROM compile AS final" stage remains so
there is no dead pcov build stage left in the Dockerfile.
🧹 Nitpick comments (1)
Dockerfile (1)

1-1: Pin the Composer image to a specific version for reproducible builds.

composer:2 is a floating tag; future 2.x releases could silently change behaviour. The previous composer:2.8 was more reproducible.

🔧 Suggested fix
-FROM composer:2 AS composer
+FROM composer:2.8 AS composer
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Dockerfile` at line 1, The Dockerfile uses a floating image tag "composer:2"
which can introduce unreproducible builds; change the base image reference in
the Dockerfile's FROM instruction (the "FROM composer:2 AS composer" line) to a
specific pinned Composer 2.x tag (e.g., composer:2.8) to ensure reproducible
builds and prevent unexpected behaviour from future 2.x releases.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@Dockerfile`:
- Line 52: The Dockerfile unconditionally appends "RUN echo extension=xdebug.so
>> /usr/local/etc/php/conf.d/xdebug.ini" which leaves xdebug.ini present when
DEBUG=false and causes startup warnings after the xdebug extension is removed;
change the Dockerfile so that the xdebug.ini file is only created when
DEBUG=true (wrap the echo in the same conditional that installs/enables xdebug)
or ensure the ini is removed when DEBUG=false (delete
/usr/local/etc/php/conf.d/xdebug.ini alongside the existing removal of the
xdebug .so), keeping the existing DEBUG check and the commands that remove the
extension in sync.
- Around line 42-72: The Dockerfile final stage leaves the container running as
root; add a non-root user and switch to it to satisfy DS-0002: create a
dedicated system user/group (e.g., app/app) in the final stage (use RUN to add
group and user), ensure ownership of the application dirs (chown -R
/usr/src/code) after COPY, and then set USER to that non-root user (USER app)
before CMD; keep the DEBUG-related file ops in a place where the non-root user
has access (either perform chown after those RUNs or create the user earlier and
adjust permissions) so builds still succeed while avoiding root runtime.
- Around line 32-42: Remove the orphaned pcov build stage: delete the "FROM
compile AS pcov" stage and its block that clones/builds pcov (the git clone, cd
pcov, phpize, ./configure --enable-pcov, make && make install) because the final
image is built from "FROM compile AS final" and there is no COPY --from=pcov;
ensure only the "FROM compile AS final" stage remains so there is no dead pcov
build stage left in the Dockerfile.

---

Nitpick comments:
In `@Dockerfile`:
- Line 1: The Dockerfile uses a floating image tag "composer:2" which can
introduce unreproducible builds; change the base image reference in the
Dockerfile's FROM instruction (the "FROM composer:2 AS composer" line) to a
specific pinned Composer 2.x tag (e.g., composer:2.8) to ensure reproducible
builds and prevent unexpected behaviour from future 2.x releases.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 80d00f9 and 2fb6b74.

📒 Files selected for processing (1)
  • Dockerfile

abnegate and others added 3 commits February 25, 2026 02:13
Add back libpq, git, brotli-dev, docker-cli, and docker-cli-compose
which may not be present in the base image tagged version. docker-cli
is needed at runtime for cache fallback tests that stop/start Redis.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…atial queries

TYPE_CONTAINS/TYPE_NOT_CONTAINS and TYPE_EQUAL/TYPE_NOT_EQUAL are dual-purpose
query methods used for both regular and spatial queries. The spatial methods list
only checked explicitly-spatial method types, missing these shared methods.
When a spatial CONTAINS query wasn't caught, it flowed through the subquery path
to escapeWildcards() which can't handle array values (coordinates).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The reconstructed Query objects in relationship resolution don't have
attributeType set (validateQuery doesn't resolve dot-notation relationship
attributes to spatial types), so isSpatialAttribute() always returns false.

For dual-purpose methods like TYPE_CONTAINS and TYPE_EQUAL that are used
for both string/array and spatial queries, detect spatial usage by checking
if the values are arrays (coordinates) rather than strings.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
Dockerfile (1)

57-57: ⚠️ Potential issue | 🔴 Critical

xdebug.so does not exist in the base image — this line will fail at runtime.

The base image appwrite/utopia-base:php-8.4-1.0.0 does not ship with xdebug pre-installed. According to the official xdebug documentation, xdebug must be compiled and enabled separately via pecl install xdebug && docker-php-ext-enable xdebug. Line 57 writes a config referencing a non-existent extension, and line 75's attempt to remove it will silently fail. This is the same critical issue as pcov on line 56.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Dockerfile` at line 57, The Dockerfile currently writes "extension=xdebug.so"
but xdebug is not present in the base image; either install and enable xdebug
before referencing it or remove the config line — specifically, run a proper
installation step (pecl install xdebug and docker-php-ext-enable xdebug) prior
to the RUN echo that appends xdebug to php conf (the line that writes
extension=xdebug.so) and ensure any later cleanup that tries to remove xdebug
(the removal referencing xdebug.so) is only executed if installation succeeded;
alternatively, delete the RUN echo for xdebug.so and the corresponding removal
to avoid referencing a non-existent extension.
src/Database/Database.php (1)

9781-9857: ⚠️ Potential issue | 🟠 Major

Subquery path bypasses related-collection permission filters.

At Line 9784 and Line 10056, the subquery branches avoid find() on the related collection, which is where documentSecurity/permission filtering is applied. This can cause parent documents to match via related documents the caller cannot read. Please either (a) inject the related-collection permission predicates into the subquery, or (b) gate subquery usage when the related collection has documentSecurity enabled (or when collection-level read isn’t granted). Based on learnings: “In Utopia\Database\Database::convertRelationshipQueries, Many-to-Many filtering does not need the parent collection or a direct junction query: it calls find() on the related collection without skipping relationships, which populates relationship attributes (including the two-way key). Parent IDs are derived from that populated attribute. Therefore, calls should remain convertRelationshipQueries($relationships, $queries).”

Also applies to: 9929-9933, 10056-10176

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Database/Database.php` around lines 9781 - 9857, The subquery branch in
Database::convertRelationshipQueries (where it builds Query::inSubqueryAll via
getJunctionCollection and createSubqueryFromResult) bypasses related-collection
permission filtering; update the logic to check the related collection's
documentSecurity and collection-level read permission (use the
$relatedCollectionDoc from getCollection and the collection permission API) and
if documentSecurity is enabled or read is not granted, do NOT generate the
subquery — instead resolve IDs via the existing non-subquery path (use
resolveRelationshipGroupToIds or perform a find() on the related collection so
permission predicates are applied). Ensure this check is performed wherever
subqueries are created (see createSubqueryFromResult usage and the many-to-many
early-return branch) and fall back to the permission-aware resolution to avoid
returning IDs from unreadable related documents.
🧹 Nitpick comments (1)
Dockerfile (1)

77-77: Consider adding a non-root USER for defense-in-depth.

Trivy flags DS-0002: no non-root user specified. Since this is a dev/test container, running as root is common practice and not blocking. If you'd like to harden it, adding a USER directive before CMD is straightforward.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Dockerfile` at line 77, Trivy warns the image runs as root because no
non-root USER is set; create a non-root user and switch to it before CMD to
reduce risk. Add steps to create a user/group (e.g., with addgroup/adduser or
useradd), chown any runtime directories/files the container needs (so the
non-root user can access them), and include a USER <username> directive before
the existing CMD ["tail","-f","/dev/null"]; ensure any necessary setup commands
that require root remain earlier in the Dockerfile so the final image drops
privileges.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@Dockerfile`:
- Line 57: The Dockerfile currently writes "extension=xdebug.so" but xdebug is
not present in the base image; either install and enable xdebug before
referencing it or remove the config line — specifically, run a proper
installation step (pecl install xdebug and docker-php-ext-enable xdebug) prior
to the RUN echo that appends xdebug to php conf (the line that writes
extension=xdebug.so) and ensure any later cleanup that tries to remove xdebug
(the removal referencing xdebug.so) is only executed if installation succeeded;
alternatively, delete the RUN echo for xdebug.so and the corresponding removal
to avoid referencing a non-existent extension.

In `@src/Database/Database.php`:
- Around line 9781-9857: The subquery branch in
Database::convertRelationshipQueries (where it builds Query::inSubqueryAll via
getJunctionCollection and createSubqueryFromResult) bypasses related-collection
permission filtering; update the logic to check the related collection's
documentSecurity and collection-level read permission (use the
$relatedCollectionDoc from getCollection and the collection permission API) and
if documentSecurity is enabled or read is not granted, do NOT generate the
subquery — instead resolve IDs via the existing non-subquery path (use
resolveRelationshipGroupToIds or perform a find() on the related collection so
permission predicates are applied). Ensure this check is performed wherever
subqueries are created (see createSubqueryFromResult usage and the many-to-many
early-return branch) and fall back to the permission-aware resolution to avoid
returning IDs from unreadable related documents.

---

Nitpick comments:
In `@Dockerfile`:
- Line 77: Trivy warns the image runs as root because no non-root USER is set;
create a non-root user and switch to it before CMD to reduce risk. Add steps to
create a user/group (e.g., with addgroup/adduser or useradd), chown any runtime
directories/files the container needs (so the non-root user can access them),
and include a USER <username> directive before the existing CMD
["tail","-f","/dev/null"]; ensure any necessary setup commands that require root
remain earlier in the Dockerfile so the final image drops privileges.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2fb6b74 and 3a8e8ab.

📒 Files selected for processing (2)
  • Dockerfile
  • src/Database/Database.php

skipValidation() called $this->disableValidation() which, in Mirror,
delegates to source and destination, permanently disabling their
validation. The finally block only restored $this->validate directly
without re-enabling source/destination. Use direct property assignment
instead to avoid unwanted delegation in subclasses.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant