Skip to content

perf(adapter): rewrite permissions condition as EXISTS instead of IN subquery#877

Open
premtsd-code wants to merge 2 commits into
mainfrom
perf/permissions-exists-rewrite
Open

perf(adapter): rewrite permissions condition as EXISTS instead of IN subquery#877
premtsd-code wants to merge 2 commits into
mainfrom
perf/permissions-exists-rewrite

Conversation

@premtsd-code
Copy link
Copy Markdown
Contributor

@premtsd-code premtsd-code commented May 15, 2026

Summary

Rewrites getSQLPermissionsCondition from IN (SELECT ... FROM <coll>_perms) to a correlated EXISTS clause. This is appended to every authenticated find/count/sum, so the per-query cost reduction compounds across the entire platform.

The previous IN (subquery) pattern forces the planner to either materialize the inner permission set or unnest into a semi-join — strategies that scan a non-trivial chunk of _perms per outer query. Correlated EXISTS lets the planner short-circuit at the first matching _perms row per outer document, using the existing _document index for single-row lookups.

Also adds an early 1 = 0 return for empty $roles, so anonymous users no longer produce the invalid _permission IN () SQL fragment.

Why

Production telemetry on Appwrite Cloud (fra region, project DBs) shows ~600 rows scanned per SELECT on authenticated reads, sustained over a 7-day window. For a typical LIMIT 25 query that's ~24× over-scanning, attributable to the permissions subquery materializing more than necessary.

EXISTS directly targets this: short-circuit at first match means roughly 1–5 _perms rows scanned per outer document instead of the full filtered subset. Predicted impact on the rows-read-per-SELECT ratio: drop from ~600 → ~50–150 (5–10×), translating to ~15–30% MySQL CPU reduction on read-heavy workloads.

Scope

  • SQL.php only — MariaDB.php, Postgres.php, MySQL.php inherit the method unchanged
  • Method signature unchanged → no caller updates needed in find, count, sum
  • getTenantQuery already accepts an alias, so tenant scoping carries through to the perms correlation alias

Test plan

  • Existing test suite passes (composer test)
  • Manual verification that find() with documentSecurity: true returns identical row sets before/after
  • Manual verification that empty-roles (anonymous) read paths still behave correctly
  • Staging deploy: confirm sum(rate(mysql_global_status_innodb_row_ops_total{operation="read"})) / sum(rate(mysql_global_status_commands_total{command="select"})) drops from baseline (~600) to <200
  • Production canary (one region) before full rollout

Summary by CodeRabbit

  • Bug Fixes

    • Fixed an edge case in permission filtering that could result in invalid queries when no roles are assigned.
  • Refactor

    • Optimized permission validation logic for improved query efficiency.

Review Change Stack

…subquery

The previous IN (SELECT ... FROM <coll>_perms) pattern forces MySQL to
materialize the entire matching permission set per query. Rewriting as
correlated EXISTS lets the planner short-circuit at the first matching
_perms row per outer document, which directly reduces innodb_rows_read
per SELECT on authenticated reads.

Also adds an early '1 = 0' return for empty roles to avoid emitting the
invalid 'IN ()' SQL fragment for anonymous users.

The method signature is unchanged so all callers (find, count, sum) and
adapter subclasses (MariaDB, Postgres, MySQL) inherit the fix without
modification.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 15, 2026

Warning

Rate limit exceeded

@premtsd-code has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 54 minutes and 8 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 4717282d-1dff-44fc-827c-df0e823c7ae0

📥 Commits

Reviewing files that changed from the base of the PR and between cefa13f and 85aa9b0.

📒 Files selected for processing (1)
  • src/Database/Adapter/SQL.php
📝 Walkthrough

Walkthrough

The PR refactors SQL permissions condition generation in getSQLPermissionsCondition() to use a correlated EXISTS subquery instead of IN (subquery), enabling short-circuit evaluation per outer document row. An explicit guard prevents invalid SQL when roles are empty by returning '1 = 0'. The correlated query ties the permissions table to the outer document via UID and applies tenant constraints.

Changes

Permission Condition Query Optimization

Layer / File(s) Summary
Correlated EXISTS permissions condition
src/Database/Adapter/SQL.php
getSQLPermissionsCondition() replaces IN (subquery) with a correlated EXISTS subquery tied to the outer document's UID. An empty-roles guard returns '1 = 0' to prevent invalid SQL. Tenant filtering is applied via getTenantQuery() on the permissions alias.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A correlated query hops in the night,
EXISTS where IN once made queries slow,
Short-circuiting through each row's light,
Tenant-safe and permission-clean below!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: rewriting the permissions condition from an IN subquery to an EXISTS subquery pattern for performance optimization.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/permissions-exists-rewrite

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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
Copy Markdown
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.

🧹 Nitpick comments (1)
src/Database/Adapter/SQL.php (1)

1944-1955: Validate/maintain a composite index for the EXISTS path.

To consistently realize the expected perf gain, ensure <collection>_perms lookup is indexed on the correlated/filter columns (typically leading with _document, plus _tenant for shared tables, then _type, _permission as appropriate).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Database/Adapter/SQL.php` around lines 1944 - 1955, The EXISTS subquery
in the SQL generator uses {$permsTable} and filters on perms._document,
perms._permission, perms._type and optionally tenant via getTenantQuery; to
guarantee the expected performance you must ensure the corresponding
<collection>_perms table has a composite index with leading column
perms._document, then perms._tenant (if getTenantQuery can apply shared-tenant
filtering), followed by perms._type and perms._permission. Add or document a
migration/schema change that creates/validates this composite index for the
table referenced by $permsTable so the EXISTS(...) path (the correlated
condition between {$outerAlias}._uid and perms._document) is covered by the
index.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/Database/Adapter/SQL.php`:
- Around line 1944-1955: The EXISTS subquery in the SQL generator uses
{$permsTable} and filters on perms._document, perms._permission, perms._type and
optionally tenant via getTenantQuery; to guarantee the expected performance you
must ensure the corresponding <collection>_perms table has a composite index
with leading column perms._document, then perms._tenant (if getTenantQuery can
apply shared-tenant filtering), followed by perms._type and perms._permission.
Add or document a migration/schema change that creates/validates this composite
index for the table referenced by $permsTable so the EXISTS(...) path (the
correlated condition between {$outerAlias}._uid and perms._document) is covered
by the index.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 051fc0cf-2e18-43b7-9510-eed73a13350c

📥 Commits

Reviewing files that changed from the base of the PR and between 3391c97 and cefa13f.

📒 Files selected for processing (1)
  • src/Database/Adapter/SQL.php

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 15, 2026

Greptile Summary

This PR rewrites getSQLPermissionsCondition in SQL.php to use a correlated EXISTS clause instead of IN (SELECT … FROM _perms), and adds an early 1 = 0 return when $roles is empty to prevent the previously invalid IN () fragment.

  • The EXISTS rewrite is logically equivalent to the original IN-subquery form: both short-circuit on the first matching _perms row per document; the planner can now use the _document index directly rather than materializing the full filtered subset.
  • The inner alias _perms_inner (with a leading-underscore sentinel) replaces the previous unqualified perms alias that was flagged in an earlier review thread, and getTenantQuery is called with this alias so the tenant condition is correctly scoped to the inner table.
  • The empty-roles guard (1 = 0) is a correctness fix: anonymous users with zero roles previously generated invalid SQL.

Confidence Score: 5/5

Safe to merge — the EXISTS rewrite is logically equivalent to the replaced IN subquery and the only other change is a correctness fix for an edge case that previously produced invalid SQL.

The change is narrow (one method, one file), the EXISTS and IN forms produce identical result sets, the inner alias collision concern raised in a prior review has been addressed with the _perms_inner sentinel, and the empty-roles guard closes a pre-existing invalid-SQL path. No regressions were identified across the MySQL, MariaDB, and Postgres adapters that inherit this method.

No files require special attention.

Important Files Changed

Filename Overview
src/Database/Adapter/SQL.php Rewrites getSQLPermissionsCondition from IN (subquery) to a correlated EXISTS clause, and adds an early 1 = 0 guard for empty $roles; logically equivalent to the old behaviour, no regressions found

Reviews (2): Last reviewed commit: "fix(adapter): use sentinel alias '_perms..." | Re-trigger Greptile

Comment thread src/Database/Adapter/SQL.php Outdated
…llision

Using the unquoted string 'perms' as the inner alias would collide if a
caller ever passed 'perms' as the outer alias. Switching to a leading-
underscore sentinel ('_perms_inner') makes the rewrite robust to any
future caller-provided alias, since outer aliases come from Query::DEFAULT_ALIAS
('main') or explicit user input, neither of which uses the underscore prefix
convention.
@premtsd-code premtsd-code mentioned this pull request May 15, 2026
5 tasks
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