Skip to content

fix: refuse a non-string select value instead of fatalling on it - #931

Merged
abnegate merged 1 commit into
mainfrom
fix/dat-2222-select-nested-value
Aug 9, 2026
Merged

fix: refuse a non-string select value instead of fatalling on it#931
abnegate merged 1 commit into
mainfrom
fix/dat-2222-select-nested-value

Conversation

@abnegate

@abnegate abnegate commented Aug 8, 2026

Copy link
Copy Markdown
Member

Fixes Appwrite DAT-2222.

The defect

A nested-array value on select returns 500 Server Error. Every other query method answers the same malformation with a typed 400.

query before after
{"method":"select","values":["sku"]} 200 200
{"method":"select","values":[["sku"]]} 500 400 Attribute selection must be a string, got array
{"method":"select","values":[["*"]]} 500 400
{"method":"equal","attribute":"sku","values":[["x"]]} 400 400
{"method":"limit","values":[[5]]} 400 400

Select::isValid() fed a values element straight into str_contains(). A TypeError is an Error, not an Exception, so it escaped the QueryException catch in every caller and came out as an unhandled 500.

Why select was the only one

Structural, not accidental:

  • Filter and Order read $query->getAttribute(), backed by protected string $attribute on Query — it can never be an array.
  • Limit and Offset read getValue() into a Numeric validator, which does no string operation.
  • Cursor reads getValue() through UID.
  • Select is the only method whose payload lives in the untyped values array and is consumed as a string.

Query::parseQuery() validates that values is an array but never its element types, and getValues() is honestly typed array<mixed> — the callers were not.

Sweep of the other query methods

Asked for by the issue, and answered by execution rather than reading: every method constant on Query was driven through Validator\Queries\Documents with the same nested-array value, before and after.

Before — 1 fatal of 49:

=== FATAL (would be HTTP 500) ===
select                 -> FATAL TypeError: str_contains(): Argument #1 ($haystack) must be of type string, array given

After — 0 fatals of 49:

=== FATAL (would be HTTP 500) ===
(none)
...
select                 -> 400  Invalid query: Attribute selection must be a string, got array

The remaining 48 already refused cleanly and are unchanged. Five (exists, notExists, orderAsc, orderDesc, orderRandom) accept the query because they ignore values entirely, which is correct.

The fix

  1. Select::isValid() rejects a non-string element with a typed message before any string operation.
  2. It runs before the duplicate check on purpose: array_unique() casts every array to the string "Array", so [["a"],["b"]] collapsed to one element and reported Duplicate attributes selected — a wrong answer that masked the real error. That is also why this looked intermittent: only value sets surviving the stringified dedupe reached the fatal.
  3. The two downstream str_contains() sites in Database.php (validateSelections(), processRelationshipQueries()) are guarded too. They are unreachable while the validator refuses first, but live whenever validation is skipped (skipValidation(), internal and worker calls).

[1] and [null] now report the type rather than Attribute not found in schema: 1 and a pair of PHP deprecations.

Regression tests, seen red

Two levels. tests/unit/SelectProjectionTest.php drives Database::find() — the entry point the HTTP layer calls — not the validator alone.

Red — fix reverted

Tests: 11, Assertions: 9, Errors: 4, Failures: 4.

3) SelectProjectionTest::testTheRefusalIsCatchableAsAnException with data set "nested array"
TypeError: str_contains(): Argument #1 ($haystack) must be of type string, array given

src/Database/Validator/Query/Select.php:77
src/Database/Validator/Queries.php:139
src/Database/Validator/IndexedQueries.php:89
src/Database/Database.php:8531
tests/unit/SelectProjectionTest.php:85

1) SelectProjectionTest::testAMalformedSelectionIsRefusedRatherThanFatal with data set "nested array"
Failed asserting that exception of type "TypeError" matches expected exception
"Utopia\Database\Exception\Query". Message was: "str_contains(): Argument #1
($haystack) must be of type string, array given"

Validator level, same revert:

There were 5 errors:

1) SelectTest::testANonStringSelectionIsRefusedByType with data set "nested array"
TypeError: str_contains(): Argument #1 ($haystack) must be of type string, array given
src/Database/Validator/Query/Select.php:77

3) SelectTest::testANonStringSelectionIsRefusedByType with data set "mixed flat and nested"
Array to string conversion
src/Database/Validator/Query/Select.php:71

Note error 3 lands on line 71, the array_unique() duplicate check — the reason the type check has to precede it.

Green

tests/unit/SelectProjectionTest.php          OK (11 tests, 17 assertions)
tests/unit/Validator/Query/SelectTest.php    OK (10 tests, 23 assertions)
--testsuite unit                             OK (428 tests, 2309 assertions)

Coverage: nested array, nested wildcard, mixed flat+nested, two nested values, assoc array, int, null; that the refusal is catchable as an Exception (a TypeError is not, which is the whole defect); and that the legitimate flat form still projects, the wildcard still projects, and an unknown attribute is still refused by schema rather than swallowed by the new check.

The testTwoNestedSelectionsReportTheTypeNotAFalseDuplicate case is built with Query::parse() from JSON rather than Query::select(), because that is the path a hand-written HTTP client takes and the only one that can carry a value the constructor's array<string> type would reject.

Verification

  • composer lint (Pint) — passed
  • composer check (PHPStan level 7, src + tests) — no errors
  • composer test --testsuite unit — 428 tests, 2309 assertions, OK

The e2e suite needs MySQL/Postgres/Mongo/Redis via docker-compose and was not run locally; the change is adapter-independent (validator plus two guards in Database.php), and the new tests use the Memory adapter so they run in the unit suite everywhere.

Blast radius for consumers

Queries\Base::isSelectQueryAllowed() wires Select wherever it is true, so in Appwrite the identical 500 is reachable on listProjects and listDeployments, not only on documents/rows.

Consumers need a release: appwrite/cloud locks utopia-php/database at 7.1.0 and resolves it through appwrite/server-ce's ^7.0.0, so a 7.1.1 patch tag flows to it with a plain composer update utopia-php/database.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Invalid, non-text field selections now return clear, catchable query errors instead of causing fatal failures.
    • Nested selections are reported with accurate type validation messages rather than misleading duplicate-selection errors.
    • Valid flat-field and wildcard projections continue to work as expected.
    • Unknown fields remain correctly rejected by schema validation.
  • Tests

    • Added coverage for malformed selections, valid projections, wildcard queries, and schema validation.

A nested-array value on `select` reached str_contains() and raised a TypeError.
A TypeError is an Error, not an Exception, so it escaped the QueryException
catch in every caller and surfaced as a 500 — where the same malformation on
`equal`, `limit` and every other method returns a typed refusal, because their
payload either lives on Query's typed string $attribute or never meets a string
function. select was the only method whose values array is consumed as a string
without a type check; a sweep of all 49 methods confirms it was the only fatal.

The check runs before the duplicate check on purpose: array_unique() casts every
array to "Array", so two nested values collapsed into one and reported a
duplicate that was not there, masking the real error.

The two downstream sites in Database.php are guarded too. They are unreachable
while the validator refuses first, but live whenever validation is skipped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Review 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: Pro Plus

Run ID: 91fa5c1e-99b1-4a91-971e-0db376e02f30

📥 Commits

Reviewing files that changed from the base of the PR and between 69e6cb5 and f54070d.

📒 Files selected for processing (4)
  • src/Database/Database.php
  • src/Database/Validator/Query/Select.php
  • tests/unit/SelectProjectionTest.php
  • tests/unit/Validator/Query/SelectTest.php

📝 Walkthrough

Walkthrough

Selection handling now rejects non-string values with typed QueryException errors. Relationship processing skips invalid values before path parsing. Tests cover malformed selections, catchable failures, valid projections, wildcards, and schema validation.

Changes

Selection validation

Layer / File(s) Summary
Validate selection types
src/Database/Validator/Query/Select.php, tests/unit/Validator/Query/SelectTest.php
The validator rejects non-string selections before duplicate detection and preserves valid flat, wildcard, system, and nested string selections.
Handle malformed projections
src/Database/Database.php, tests/unit/SelectProjectionTest.php
Projection handling raises catchable QueryException errors for malformed values, skips non-string relationship values, and preserves projection and schema validation behavior.

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

Possibly related PRs

Suggested reviewers: fogelito

🚥 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 clearly and concisely describes the main fix: rejecting non-string select values instead of causing a fatal error.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
✨ 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 fix/dat-2222-select-nested-value

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 Aug 8, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR converts malformed non-string select values from uncaught PHP errors into typed query-validation failures and adds defensive checks to downstream selection processing.

  • Validates every selected attribute is a string before duplicate detection or string operations.
  • Guards relationship-selection processing when validation is bypassed or queries originate internally.
  • Adds validator-level and Database::find() regression coverage for malformed, wildcard, valid, and unknown selections.

Confidence Score: 5/5

The PR appears safe to merge, with malformed selections consistently rejected before reaching string operations or adapters.

The new checks enforce the existing string-only selection contract, and the downstream query flow rejects malformed values with the established query exception while preserving valid projections.

Important Files Changed

Filename Overview
src/Database/Validator/Query/Select.php Adds the necessary early element-type check before duplicate detection and attribute parsing, preserving all valid string selections.
src/Database/Database.php Adds defensive non-string handling at both downstream selection-processing sites without altering valid relationship-selection behavior.
tests/unit/SelectProjectionTest.php Exercises the public database query path and verifies malformed selections produce catchable query exceptions while valid projections remain functional.
tests/unit/Validator/Query/SelectTest.php Covers non-string value types, false duplicate detection for nested arrays, and unaffected valid selection forms.

Reviews (1): Last reviewed commit: "fix: refuse a non-string select value in..." | Re-trigger Greptile

@abnegate
abnegate merged commit 80b5b3d into main Aug 9, 2026
22 checks passed
@abnegate
abnegate deleted the fix/dat-2222-select-nested-value branch August 9, 2026 01:37
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