Skip to content

fix: stop internal keys stealing user attributes named after them - #961

Open
HarshMN2345 wants to merge 1 commit into
mainfrom
fix/internal-attribute-key-collision
Open

fix: stop internal keys stealing user attributes named after them#961
HarshMN2345 wants to merge 1 commit into
mainfrom
fix/internal-attribute-key-collision

Conversation

@HarshMN2345

@HarshMN2345 HarshMN2345 commented Sep 8, 2026

Copy link
Copy Markdown
Member

What

Database::decode() falls back to the adapter-filtered key when an attribute's value is null:

if (\is_null($value)) {
    $filteredKey = $this->adapter->filter($key);
    $value = $document->getAttribute($filteredKey);

Adapter::filter() strips everything outside [A-Za-z0-9_-], so for an internal key it strips the leading $: filter('$collection') === 'collection', which is a name a user attribute is allowed to have. An internal value never reaches the document under that name — the adapters map their own _-prefixed columns back themselves — so the lookup has nothing of its own to find and can only take the user's attribute.

Two keys are actually reachable with a null value at decode time:

  • $collectionfind() stamps it after decode ($node->setAttribute('$collection', ...)), while getDocument(), createDocument(), createDocuments() and upsertDocuments() all stamp it before. So on a list read the fallback removed the user's collection attribute from the document and wrote its value into $collection; find() then overwrote $collection with the real id, and the user's value was simply gone. Where the value was null it was worse: the elseif alias cleanup deleted the key outright, so the attribute vanished from the response entirely.
  • $tenant — with shared tables on, getAttributeProjection() does not project _tenant, so any Query::select(...) returns a row without it and $tenant is null at decode. validateSelections() does not list $tenant either, so the stolen value was not even re-exposed under $tenant — it disappeared silently.

$permissions is safe (an earlier continue), and $id / $sequence / $createdAt / $updatedAt are always projected and mapped back, so they are never null on either read path.

Fix

Skip the alias fallback for internal keys.

The guard keys off the internal-attribute set rather than the $ prefix on purpose: a collection id may itself start with $, and the derived relationship attribute key then also starts with $ and does need the fallback (its column is the filtered name). Guarding on the prefix breaks testManyToManyRelationshipKeyWithSymbols.

Tests

Two regression tests in DocumentTests, so every adapter scope picks them up:

  • testFindAttributeNamedAfterInternalKey — an attribute named collection, with a non-null and a null row. The null row asserts on offsetExists(), since getAttribute() cannot tell a dropped key from a null value.
  • testFindAttributeNamedAfterTenantKey — the shared-tables tenant case behind a getSharedTables() guard, read through Query::select(['tenant']), which is what drops _tenant from the projection.

Both fail on main and pass here. Verified across all 16 adapter suites: MariaDB, MySQL, SQLite, Redis, Mirror and SharedTables/{MariaDB,MySQL,Redis,SQLite} are green; Postgres, SharedTables/Postgres, Memory and MongoDB carry only their existing testObjectAttribute* / testIntegersBeyondInt32 failures; Pool, Schemaless/MongoDB and SharedTables/MongoDB fail wholesale on clean main locally with the same counts, so they are unchanged by this.

Refs appwrite/appwrite#6944 — Appwrite needs a dependency bump before that issue closes.

Summary by CodeRabbit

  • Bug Fixes

    • Fixed document retrieval so user attributes named collection or tenant are preserved correctly.
    • Null-valued attributes with names matching internal keys now round-trip consistently.
    • Projected document reads retain user attributes that overlap with internal tenant keys.
  • Tests

    • Added coverage for attributes that share names with internal metadata keys.

decode() falls back to the adapter-filtered key when an attribute's value is
null, and filter() strips the leading "$" off an internal key, so "$collection"
resolves to "collection". find() stamps $collection after decode, unlike every
other read path, so on a list read the fallback took the user's "collection"
attribute, removed it from the document, and wrote its value into $collection.
"$tenant" is reachable the same way on shared tables, where a select query
leaves _tenant out of the projection.

Guarding on the internal-attribute set rather than the "$" prefix keeps the
alias fallback for relationship keys, which start with "$" when the related
collection id does.

Refs appwrite/appwrite#6944
@HarshMN2345
HarshMN2345 force-pushed the fix/internal-attribute-key-collision branch from 836d5bf to 5d7b005 Compare September 8, 2026 12:24
@coderabbitai

coderabbitai Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: 7ce10bb2-9f43-43f4-9ae0-3d7da8c8bc17

📥 Commits

Reviewing files that changed from the base of the PR and between 9147205 and 5d7b005.

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

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

Database::decode() now distinguishes internal keys from filtered aliases. End-to-end tests verify that collection and tenant user attributes retain their stored values, including null values and projected results.

Changes

Attribute decoding

Layer / File(s) Summary
Internal key lookup
src/Database/Database.php
decode() records internal attribute identifiers and skips alias lookup for matching keys.
Conflicting attribute coverage
tests/e2e/Adapter/Scopes/DocumentTests.php
Tests verify collection and tenant attributes, including null values and projected results.

Priority: ➖ Normal — Schedule the database decoding fix because user attributes named `collection` or `tenant` can otherwise be consumed by internal keys, with regression coverage for both cases.

Estimated code review effort: 2 (Simple) | ~10 minutes

Merge Risk: ⚪ Minimal · up to 5d7b0

This change preserves user attributes named like internal database keys without altering relationship-key fallback behavior. Coverage includes the affected null-value and projected-query paths, with no current merge-blocking risk identified.

Suggested reviewers: fogelito

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 5 functions across 2 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main fix: preventing internal keys from taking user attributes with matching names.
  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/internal-attribute-key-collision

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.

@greptile-apps

greptile-apps Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR prevents null internal metadata fields from consuming user attributes whose names match their filtered aliases.

  • Builds an exact set of active internal attribute keys during document decoding.
  • Restricts filtered-key fallback to non-internal schema attributes.
  • Adds cross-adapter regressions for collection collisions, null-value preservation, and projected shared-table tenant collisions.

Confidence Score: 5/5

The PR appears safe to merge, with the decoder change narrowly addressing internal-key alias collisions and focused regressions covering the reachable failure paths.

No actionable failures remain; internal metadata is mapped independently by adapters, symbol-prefixed relationship aliases retain their fallback behavior, and the new tests verify preservation of both non-null and explicit-null colliding user attributes.

Important Files Changed

Filename Overview
src/Database/Database.php Excludes reserved internal attributes from filtered-alias fallback while retaining fallback behavior for user and relationship attributes.
tests/e2e/Adapter/Scopes/DocumentTests.php Adds focused regressions covering list reads, explicit null user values, and shared-table selection projections.

Reviews (1): Last reviewed commit: "fix: stop internal keys stealing user at..." | Re-trigger Greptile

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