Skip to content

Add SQL trigger in getDocument#880

Merged
abnegate merged 8 commits into
mainfrom
skip-silent-hooks
May 19, 2026
Merged

Add SQL trigger in getDocument#880
abnegate merged 8 commits into
mainfrom
skip-silent-hooks

Conversation

@fogelito
Copy link
Copy Markdown
Contributor

@fogelito fogelito commented May 18, 2026

Summary by CodeRabbit

  • New Features

    • Document read operations can be intercepted and modified via event hooks, allowing dynamic adjustment of read queries.
  • Bug Fixes

    • Event hook removal is now supported, preventing stale handlers from affecting subsequent operations.
  • Tests

    • Tests improved to set and clean up event hooks and verify scoped document reads behave as expected.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 18, 2026

📝 Walkthrough

Walkthrough

getDocument() now routes generated SELECT SQL through the Database::EVENT_DOCUMENT_READ trigger and applies any returned SQL. Database::before accepts null callbacks. Tests register, capture, assert, and then unregister the document-read listener and reset metadata.

Changes

Document-read event trigger

Layer / File(s) Summary
Trigger hook activation in getDocument
src/Database/Adapter/SQL.php
getDocument() now passes the constructed SELECT SQL through trigger(Database::EVENT_DOCUMENT_READ, $sql) and assigns the returned value back to $sql before execution.
Nullable Database::before signature
src/Database/Database.php
Database::before() signature and docblock updated to accept ?callable $callback; the method forwards the value to the adapter unchanged.
Test: register, capture, assert, and cleanup
tests/e2e/Adapter/Scopes/CollectionTests.php
testTransformations imports Utopia\Database\Adapter\SQL, sets metadata scope, registers an EVENT_DOCUMENT_READ before-hook that appends to and captures SQL, runs getDocument and asserts an empty result, conditionally asserts SQL contains the scope comment for SQL adapters, unregisters the hook by passing null, and resets metadata.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I chewed a query, soft and neat,
A listener laced each SQL beat.
Before-hooks snap, then hop away —
Tests tidy trails at end of day.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Add SQL trigger in getDocument' directly describes the main change: adding a trigger call in the getDocument() method to pass SQL through the EVENT_DOCUMENT_READ event.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch skip-silent-hooks

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.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 18, 2026

Greptile Summary

This PR hooks trigger(EVENT_DOCUMENT_READ, $sql) into SQL::getDocument so that registered before callbacks can inspect or rewrite the query string, and relaxes Database::before() to accept null as a way to deregister a hook.

  • SQL.php: trigger(EVENT_DOCUMENT_READ, $sql) is called after the full query is built (including FOR UPDATE), before PDO prepares it — consistent with how other events are already wired.
  • Database.php: before() signature widens to ?callable; Adapter::before() already handles null to unset the transformation.
  • CollectionTests.php: New test registers a hook that appends AND 1=0, asserts the document is empty, and conditionally checks for the metadata SQL comment — but the cleanup block removes only the EVENT_DOCUMENT_READ hook and does not remove the EVENT_ALL metadata transformation, leaving it active for subsequent tests.

Confidence Score: 4/5

The SQL and Database changes are safe; the test cleanup leaves a metadata transformation registered on the shared adapter, which will prepend a SQL comment to every query in tests that run afterward.

The adapter and Database changes are straightforward and correct. The only real defect is in the test teardown: resetMetadata() clears the metadata array but leaves the EVENT_ALL metadata callback still in place, so any test running after testTransformations on the same adapter instance will have the scope comment silently prepended to its queries.

tests/e2e/Adapter/Scopes/CollectionTests.php — the cleanup block needs an extra before(EVENT_ALL, 'metadata', null) call to fully undo the metadata transformation.

Important Files Changed

Filename Overview
src/Database/Adapter/SQL.php Adds trigger(EVENT_DOCUMENT_READ, $sql) call after the full SQL string is assembled (including FOR UPDATE), wiring the new event into the SQL adapter's getDocument path.
src/Database/Database.php Relaxes the before() signature from callable to ?callable, delegating null-means-remove semantics to Adapter::before() which already handles this correctly.
tests/e2e/Adapter/Scopes/CollectionTests.php Test registers a hook that appends AND 1=0 to prove the trigger fires and checks the metadata SQL comment, but the EVENT_ALL metadata transformation is not removed in the cleanup block, causing it to persist for subsequent tests.

Reviews (5): Last reviewed commit: "resetMetadata" | Re-trigger Greptile

…ests

testTransformations registers a before() callback that rewrites SQL to
"SELECT 1". Now that SQL.php::getDocument triggers EVENT_DOCUMENT_READ,
this callback persists across subsequent tests in the same class
instance, causing PDO HY093 errors when bindValue(':_uid') is called on
the parameter-less rewritten query. Wrap the assertion in try/finally
and unregister via the adapter's before(name, null) form.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

Claude pushed fixes from: healing

0414b92...4c291dd

… SQL

The transformation rewrote the prepared SQL to "SELECT 1", but SQL.php's
getDocument still calls bindValue(':_uid', $id) (and ':_tenant' for
sharedTables). PDO throws HY093 because the rewritten query has no
placeholders.

Wrap the original query as a derived table with WHERE 1=0 instead. The
inner query keeps the :_uid / :_tenant placeholders so the bindValue
calls match, while the outer WHERE forces zero rows so isEmpty() still
asserts the trigger was applied.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

Claude pushed fixes from: healing

4c291dd...34c3482

Comment thread tests/e2e/Adapter/Scopes/CollectionTests.php Outdated
fogelito added 3 commits May 18, 2026 14:04
…ent-hooks

# Conflicts:
#	tests/e2e/Adapter/Scopes/CollectionTests.php
@utopia-php utopia-php deleted a comment from claude Bot May 18, 2026
@utopia-php utopia-php deleted a comment from claude Bot May 18, 2026
Comment on lines +1691 to +1692
$database->before(Database::EVENT_DOCUMENT_READ, 'test', null);
$database->resetMetadata();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 resetMetadata() empties $this->metadata but does not remove the before(EVENT_ALL, 'metadata', ...) transformation that setMetadata() registered. The closure captures $output = "/* scope: api.users */ " by value, so after this test every subsequent query in the same adapter instance will still have that comment prepended. This silently pollutes every test that runs afterwards on the same database object.

Suggested change
$database->before(Database::EVENT_DOCUMENT_READ, 'test', null);
$database->resetMetadata();
$database->before(Database::EVENT_DOCUMENT_READ, 'test', null);
$database->before(Database::EVENT_ALL, 'metadata', null);
$database->resetMetadata();

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.

Actionable comments posted: 1

🤖 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.

Inline comments:
In `@tests/e2e/Adapter/Scopes/CollectionTests.php`:
- Around line 1674-1692: The test registers a listener via
Database::before('test', Database::EVENT_DOCUMENT_READ) and sets metadata with
Database::setMetadata('scope', 'api.users') but does not guarantee they are
cleaned up if an assertion fails; wrap the invocation of
$database->getDocument('docs', 'doc1') and subsequent assertions in a
try/finally block and move the $database->before(..., null) removal call and
$database->resetMetadata() into the finally so the EVENT_DOCUMENT_READ hook (the
'test' listener) and metadata are always unregistered/reset even on failure.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 193f3449-d9f0-423d-a9e1-6c73ab45f2c1

📥 Commits

Reviewing files that changed from the base of the PR and between 4c291dd and 0d95a66.

📒 Files selected for processing (2)
  • src/Database/Database.php
  • tests/e2e/Adapter/Scopes/CollectionTests.php

Comment on lines +1674 to +1692
$database->setMetadata('scope', 'api.users');

$capturedSql = '';
$database->before(Database::EVENT_DOCUMENT_READ, 'test', function (string $sql) use (&$capturedSql) {
$sql .= ' AND 1=0';
$capturedSql = $sql;
return $sql;
});

$result = $database->getDocument('docs', 'doc1');

$this->assertTrue($result->isEmpty());

if ($database->getAdapter() instanceof SQL) {
$this->assertStringContainsString('/* scope: api.users */', $capturedSql);
}

$database->before(Database::EVENT_DOCUMENT_READ, 'test', null);
$database->resetMetadata();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard listener + metadata cleanup with finally.

If this test fails before cleanup, the Database::EVENT_DOCUMENT_READ hook and metadata remain active and can pollute later tests. Wrap execution/assertions in try/finally and always unregister/reset in finally.

Proposed fix
         $database->setMetadata('scope', 'api.users');

         $capturedSql = '';
         $database->before(Database::EVENT_DOCUMENT_READ, 'test', function (string $sql) use (&$capturedSql) {
             $sql .= ' AND 1=0';
             $capturedSql = $sql;
             return $sql;
         });

-        $result = $database->getDocument('docs', 'doc1');
-
-        $this->assertTrue($result->isEmpty());
-
-        if ($database->getAdapter() instanceof SQL) {
-            $this->assertStringContainsString('/* scope: api.users */', $capturedSql);
-        }
-
-        $database->before(Database::EVENT_DOCUMENT_READ, 'test', null);
-        $database->resetMetadata();
+        try {
+            $result = $database->getDocument('docs', 'doc1');
+            $this->assertTrue($result->isEmpty());
+
+            if ($database->getAdapter() instanceof SQL) {
+                $this->assertStringContainsString('/* scope: api.users */', $capturedSql);
+            }
+        } finally {
+            $database->before(Database::EVENT_DOCUMENT_READ, 'test', null);
+            $database->resetMetadata();
+        }
📝 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
$database->setMetadata('scope', 'api.users');
$capturedSql = '';
$database->before(Database::EVENT_DOCUMENT_READ, 'test', function (string $sql) use (&$capturedSql) {
$sql .= ' AND 1=0';
$capturedSql = $sql;
return $sql;
});
$result = $database->getDocument('docs', 'doc1');
$this->assertTrue($result->isEmpty());
if ($database->getAdapter() instanceof SQL) {
$this->assertStringContainsString('/* scope: api.users */', $capturedSql);
}
$database->before(Database::EVENT_DOCUMENT_READ, 'test', null);
$database->resetMetadata();
$database->setMetadata('scope', 'api.users');
$capturedSql = '';
$database->before(Database::EVENT_DOCUMENT_READ, 'test', function (string $sql) use (&$capturedSql) {
$sql .= ' AND 1=0';
$capturedSql = $sql;
return $sql;
});
try {
$result = $database->getDocument('docs', 'doc1');
$this->assertTrue($result->isEmpty());
if ($database->getAdapter() instanceof SQL) {
$this->assertStringContainsString('/* scope: api.users */', $capturedSql);
}
} finally {
$database->before(Database::EVENT_DOCUMENT_READ, 'test', null);
$database->resetMetadata();
}
🤖 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 `@tests/e2e/Adapter/Scopes/CollectionTests.php` around lines 1674 - 1692, The
test registers a listener via Database::before('test',
Database::EVENT_DOCUMENT_READ) and sets metadata with
Database::setMetadata('scope', 'api.users') but does not guarantee they are
cleaned up if an assertion fails; wrap the invocation of
$database->getDocument('docs', 'doc1') and subsequent assertions in a
try/finally block and move the $database->before(..., null) removal call and
$database->resetMetadata() into the finally so the EVENT_DOCUMENT_READ hook (the
'test' listener) and metadata are always unregistered/reset even on failure.

@abnegate abnegate merged commit 5679019 into main May 19, 2026
40 checks passed
@abnegate abnegate deleted the skip-silent-hooks branch May 19, 2026 02:54
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.

2 participants