Skip to content

Fix Upsert No change#10363

Merged
abnegate merged 2 commits into1.7.xfrom
upsert-document-no-change
Aug 25, 2025
Merged

Fix Upsert No change#10363
abnegate merged 2 commits into1.7.xfrom
upsert-document-no-change

Conversation

@fogelito
Copy link
Copy Markdown
Contributor

@fogelito fogelito commented Aug 24, 2025

What does this PR do?

Fixes #10326

Test Plan

(Write your test plan here. If you changed any code, please provide us with clear instructions on how you verified your changes work. Screenshots may also be helpful.)

Related PRs and Issues

Checklist

  • Have you read the Contributing Guidelines on issues?
  • If the PR includes a change to an API's metadata (desc, label, params, etc.), does it also include updated API specs and example docs?

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Aug 24, 2025

📝 Walkthrough

Walkthrough

Adds a fallback in app/controllers/api/databases.php: after createOrUpdateDocuments, if upserted[0] is empty, the code fetches the document by documentId from the collection and assigns it to upserted[0]. No other control flow or error handling changes. Updates tests/e2e/Services/Databases/DatabasesBase.php: expands testUpsertDocument to exercise idempotent upserts, multiple upsert flows (including full-attribute upsert, title update, and permission modifications), and corresponding GET assertions.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested reviewers

  • abnegate
  • Meldiron
  • stnguyen90

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch upsert-document-no-change

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@github-actions
Copy link
Copy Markdown

github-actions bot commented Aug 24, 2025

Security Scan Results for PR

Docker Image Scan Results

Package Version Vulnerability Severity
binutils 2.42-r0 CVE-2025-0840 HIGH
git 2.45.3-r0 CVE-2025-46334 HIGH
git 2.45.3-r0 CVE-2025-48384 HIGH
git 2.45.3-r0 CVE-2025-48385 HIGH
git-init-template 2.45.3-r0 CVE-2025-46334 HIGH
git-init-template 2.45.3-r0 CVE-2025-48384 HIGH
git-init-template 2.45.3-r0 CVE-2025-48385 HIGH
icu 74.2-r0 CVE-2025-5222 HIGH
icu-data-en 74.2-r0 CVE-2025-5222 HIGH
icu-dev 74.2-r0 CVE-2025-5222 HIGH
icu-libs 74.2-r0 CVE-2025-5222 HIGH
libecpg 16.8-r0 CVE-2025-8714 HIGH
libecpg 16.8-r0 CVE-2025-8715 HIGH
libecpg-dev 16.8-r0 CVE-2025-8714 HIGH
libecpg-dev 16.8-r0 CVE-2025-8715 HIGH
libexpat 2.6.4-r0 CVE-2024-8176 HIGH
libpq 16.8-r0 CVE-2025-8714 HIGH
libpq 16.8-r0 CVE-2025-8715 HIGH
libpq-dev 16.8-r0 CVE-2025-8714 HIGH
libpq-dev 16.8-r0 CVE-2025-8715 HIGH
libxml2 2.12.7-r0 CVE-2024-56171 HIGH
libxml2 2.12.7-r0 CVE-2025-24928 HIGH
libxml2 2.12.7-r0 CVE-2025-27113 HIGH
libxml2 2.12.7-r0 CVE-2025-32414 HIGH
libxml2 2.12.7-r0 CVE-2025-32415 HIGH
postgresql16-dev 16.8-r0 CVE-2025-8714 HIGH
postgresql16-dev 16.8-r0 CVE-2025-8715 HIGH
pyc 3.12.9-r0 CVE-2024-12718 HIGH
pyc 3.12.9-r0 CVE-2025-4138 HIGH
pyc 3.12.9-r0 CVE-2025-4330 HIGH
pyc 3.12.9-r0 CVE-2025-4517 HIGH
python3 3.12.9-r0 CVE-2024-12718 HIGH
python3 3.12.9-r0 CVE-2025-4138 HIGH
python3 3.12.9-r0 CVE-2025-4330 HIGH
python3 3.12.9-r0 CVE-2025-4517 HIGH
python3-pyc 3.12.9-r0 CVE-2024-12718 HIGH
python3-pyc 3.12.9-r0 CVE-2025-4138 HIGH
python3-pyc 3.12.9-r0 CVE-2025-4330 HIGH
python3-pyc 3.12.9-r0 CVE-2025-4517 HIGH
python3-pycache-pyc0 3.12.9-r0 CVE-2024-12718 HIGH
python3-pycache-pyc0 3.12.9-r0 CVE-2025-4138 HIGH
python3-pycache-pyc0 3.12.9-r0 CVE-2025-4330 HIGH
python3-pycache-pyc0 3.12.9-r0 CVE-2025-4517 HIGH
sqlite-libs 3.45.3-r1 CVE-2025-29087 HIGH
xz 5.6.2-r0 CVE-2025-31115 HIGH
xz-libs 5.6.2-r0 CVE-2025-31115 HIGH
golang.org/x/crypto v0.31.0 CVE-2025-22869 HIGH
golang.org/x/oauth2 v0.24.0 CVE-2025-22868 HIGH
stdlib 1.22.10 CVE-2025-47907 HIGH

Source Code Scan Results

🎉 No vulnerabilities found!

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: 2

Caution

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

⚠️ Outside diff range comments (1)
app/controllers/api/databases.php (1)

281-286: Bug: Misplaced parentheses break attribute validation in updateAttribute()

Both conditionals use getAttribute(( 'type') !== $type) and getAttribute(( 'filter') !== $filter), passing a boolean instead of the attribute key. This will always read the wrong attribute and makes validation incorrect.

Fix:

-    if ($attribute->getAttribute(('type') !== $type)) {
+    if ($attribute->getAttribute('type') !== $type) {
         throw new Exception(Exception::ATTRIBUTE_TYPE_INVALID);
     }

-    if ($attribute->getAttribute('type') === Database::VAR_STRING && $attribute->getAttribute(('filter') !== $filter)) {
+    if ($attribute->getAttribute('type') === Database::VAR_STRING && $attribute->getAttribute('filter') !== $filter) {
         throw new Exception(Exception::ATTRIBUTE_TYPE_INVALID);
     }

This is unrelated to the current PR, but it’s a correctness bug that will surface during attribute updates.

🧹 Nitpick comments (2)
app/controllers/api/databases.php (1)

4724-4729: Avoid undefined index notice when checking $permissions in bulk update

if ($data['$permissions']) may trigger an “undefined index” notice. Use a safe check.

-        if ($data['$permissions']) {
+        if (isset($data['$permissions']) && $data['$permissions']) {
             $validator = new Permissions();
             if (!$validator->isValid($data['$permissions'])) {
                 throw new Exception(Exception::GENERAL_BAD_REQUEST, $validator->getDescription());
             }
         }

Low impact, but keeps logs clean.

tests/e2e/Services/Databases/DatabasesBase.php (1)

1733-1765: Strengthen "no-change" upsert verification by asserting $updatedAt is unchanged.

To verify the controller fallback truly avoids a write when no attributes change, also assert the document’s $updatedAt didn’t mutate.

Apply this diff:

-        /**
+        /**
          * Resend same document for no change
          * Had to add all attributes because of partial comparison $old->getAttributes() == $document->getAttributes()
          * If nothing is upserted there will be a regular call to getDocument
          */
+        // Capture timestamp to ensure idempotent upsert doesn't mutate metadata
+        $updatedAtBefore = $document['body']['$updatedAt'];
 
         $document = $this->client->call(Client::METHOD_PUT, '/databases/' . $databaseId . '/collections/' . $data['moviesId'] . '/documents/' . $documentId, array_merge([
             'content-type' => 'application/json',
             'x-appwrite-project' => $this->getProject()['$id'],
         ], $this->getHeaders()), [
             'data' => [
                 'title' => 'Thor: Ragnarok',
                 'releaseYear' => 2000,
                 'integers' => [],
                 'birthDay' => null,
                 'duration' => null,
                 'starringActors' => [],
                 'actors' => [],
                 'tagline' => '',
                 'description' => '',
             ],
             'permissions' => [
                 Permission::read(Role::users()),
                 Permission::update(Role::users()),
                 Permission::delete(Role::users()),
             ],
         ]);
 
         $this->assertEquals(200, $document['headers']['status-code']);
         $this->assertEquals('Thor: Ragnarok', $document['body']['title']);
         $this->assertCount(3, $document['body']['$permissions']);
+        // No-op upsert should not bump system timestamp
+        $this->assertEquals($updatedAtBefore, $document['body']['$updatedAt']);
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 4a6a805 and c7a7692.

📒 Files selected for processing (2)
  • app/controllers/api/databases.php (1 hunks)
  • tests/e2e/Services/Databases/DatabasesBase.php (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
tests/e2e/Services/Databases/DatabasesBase.php (1)
tests/e2e/Client.php (1)
  • Client (8-328)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (20)
  • GitHub Check: E2E Service Test (Projects)
  • GitHub Check: E2E Service Test (Proxy)
  • GitHub Check: E2E Service Test (Locale)
  • GitHub Check: E2E Service Test (Users)
  • GitHub Check: E2E Service Test (Tokens)
  • GitHub Check: E2E Service Test (Webhooks)
  • GitHub Check: E2E Service Test (Realtime)
  • GitHub Check: E2E Service Test (Console)
  • GitHub Check: E2E Service Test (Databases)
  • GitHub Check: E2E Service Test (FunctionsSchedule)
  • GitHub Check: E2E Service Test (GraphQL)
  • GitHub Check: E2E Service Test (Avatars)
  • GitHub Check: E2E Service Test (Health)
  • GitHub Check: E2E Service Test (Account)
  • GitHub Check: E2E Service Test (Dev Keys)
  • GitHub Check: E2E Service Test (Site Screenshots)
  • GitHub Check: E2E General Test
  • GitHub Check: Unit Test
  • GitHub Check: Setup & Build Appwrite Image
  • GitHub Check: scan
🔇 Additional comments (2)
tests/e2e/Services/Databases/DatabasesBase.php (2)

1707-1707: Minor formatting is fine.

The added blank line after documentId improves readability.


1723-1725: LGTM: baseline upsert assertions.

Status and permissions count checks after the initial upsert look correct.

throw new Exception(Exception::DOCUMENT_INVALID_STRUCTURE, $e->getMessage());
}

if (empty($upserted[0])) {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I think we should return a header for $modified
that was returned from
$modified = $dbForProject->createOrUpdateDocuments
Otherwise will not know if upserted was skipped.
Like now in this test had to add all attributes

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: 0

♻️ Duplicate comments (1)
tests/e2e/Services/Databases/DatabasesBase.php (1)

1761-1764: Previous failing placeholder assertion is gone — resolved.

The hard-coded failing assertion reported earlier no longer exists in this block. Good cleanup.

🧹 Nitpick comments (3)
tests/e2e/Services/Databases/DatabasesBase.php (3)

1733-1738: Reword to describe observable behavior, not internals.

The comment references internal implementation ($old->getAttributes() == $document->getAttributes()) which can drift. Prefer stating the behavioral contract we’re validating (idempotent upsert returns the current document without mutating timestamps).

Apply this comment tweak:

-        /**
-         * Resend same document for no change
-         * Had to add all attributes because of partial comparison $old->getAttributes() == $document->getAttributes()
-         * If nothing is upserted there will be a regular call to getDocument
-         */
+        /**
+         * Resend the same fields to verify a no-op upsert:
+         * server should return the existing document (no mutation) and not bump $updatedAt.
+         * When no changes are detected, API should behave like a GET of the current document.
+         */

1739-1759: Ensure a true “no-change” upsert: reuse fetched data to avoid null/[]/'' drift.

Hard-coding defaults (e.g., empty arrays/strings) can differ from stored values (often null), accidentally turning this into an update instead of a no-op. Build the payload from the previously fetched document body and strip system fields so the PUT is guaranteed identical.

Apply this diff to construct an idempotent payload and reuse permissions:

-        $document = $this->client->call(Client::METHOD_PUT, '/databases/' . $databaseId . '/collections/' . $data['moviesId'] . '/documents/' . $documentId, array_merge([
+        // Build idempotent payload from the current document snapshot
+        $sameData = $document['body'];
+        foreach (['$id', '$databaseId', '$collectionId', '$permissions', '$createdAt', '$updatedAt', '$sequence', '$tenant'] as $sys) {
+            unset($sameData[$sys]);
+        }
+        $samePermissions = $document['body']['$permissions'] ?? [
+            Permission::read(Role::users()),
+            Permission::update(Role::users()),
+            Permission::delete(Role::users()),
+        ];
+
+        $document = $this->client->call(Client::METHOD_PUT, '/databases/' . $databaseId . '/collections/' . $data['moviesId'] . '/documents/' . $documentId, array_merge([
             'content-type' => 'application/json',
             'x-appwrite-project' => $this->getProject()['$id'],
         ], $this->getHeaders()), [
-            'data' => [
-                'title' => 'Thor: Ragnarok',
-                'releaseYear' => 2000,
-                'integers' => [],
-                'birthDay' => null,
-                'duration' => null,
-                'starringActors' => [],
-                'actors' => [],
-                'tagline' => '',
-                'description' => '',
-            ],
-            'permissions' => [
-                Permission::read(Role::users()),
-                Permission::update(Role::users()),
-                Permission::delete(Role::users()),
-            ],
+            'data' => $sameData,
+            'permissions' => $samePermissions,
         ]);

Optionally also assert that $updatedAt didn’t change (see next comment) and that the response body still matches the snapshot for key user fields.


1761-1764: Make idempotency observable: assert $updatedAt is unchanged.

If the upsert is a true no-op, $updatedAt should remain the same. Capture it before the second PUT and compare after.

Patch this block to include the timestamp assertion:

         $this->assertEquals(200, $document['headers']['status-code']);
         $this->assertEquals('Thor: Ragnarok', $document['body']['title']);
         $this->assertCount(3, $document['body']['$permissions']);
+        $this->assertEquals($prevUpdatedAt, $document['body']['$updatedAt'], 'No-op upsert should not bump $updatedAt');

And record the baseline right after the first GET (shown here for clarity; add near Lines 1726–1731):

// After the initial GET of the upserted doc
$prevUpdatedAt = $document['body']['$updatedAt'] ?? null;
$this->assertNotNull($prevUpdatedAt, 'Precondition: $updatedAt should be present');
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between c7a7692 and d2dedf9.

📒 Files selected for processing (1)
  • tests/e2e/Services/Databases/DatabasesBase.php (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
tests/e2e/Services/Databases/DatabasesBase.php (1)
tests/e2e/Client.php (1)
  • Client (8-328)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (20)
  • GitHub Check: E2E Service Test (Messaging)
  • GitHub Check: E2E Service Test (Users)
  • GitHub Check: E2E Service Test (Migrations)
  • GitHub Check: E2E Service Test (Proxy)
  • GitHub Check: E2E Service Test (GraphQL)
  • GitHub Check: E2E Service Test (Realtime)
  • GitHub Check: E2E Service Test (Webhooks)
  • GitHub Check: E2E Service Test (VCS)
  • GitHub Check: E2E Service Test (FunctionsSchedule)
  • GitHub Check: E2E Service Test (Account)
  • GitHub Check: E2E Service Test (Locale)
  • GitHub Check: E2E Service Test (Teams)
  • GitHub Check: E2E Service Test (Health)
  • GitHub Check: E2E Service Test (Site Screenshots)
  • GitHub Check: E2E Service Test (Databases)
  • GitHub Check: E2E Service Test (Dev Keys)
  • GitHub Check: E2E General Test
  • GitHub Check: Unit Test
  • GitHub Check: Benchmark
  • GitHub Check: scan
🔇 Additional comments (1)
tests/e2e/Services/Databases/DatabasesBase.php (1)

1707-1707: No-op whitespace change — fine to keep.

Non-functional blank line; no action needed.

@github-actions
Copy link
Copy Markdown

✨ Benchmark results

  • Requests per second: 989
  • Requests with 200 status code: 178,055
  • P99 latency: 0.187286299

⚡ Benchmark Comparison

Metric This PR Latest version
RPS 989 999
200 178,055 179,778
P99 0.187286299 0.197056907

@abnegate abnegate merged commit 2dbe238 into 1.7.x Aug 25, 2025
40 checks passed
@abnegate abnegate deleted the upsert-document-no-change branch August 25, 2025 04:03
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