Skip to content

fix(client): decode BSON int64 to native PHP integers - #49

Merged
abnegate merged 2 commits into
mainfrom
fix/int64-native-integers
Aug 13, 2026
Merged

fix(client): decode BSON int64 to native PHP integers#49
abnegate merged 2 commits into
mainfrom
fix/int64-native-integers

Conversation

@abnegate

@abnegate abnegate commented Aug 13, 2026

Copy link
Copy Markdown
Member

Fixes the root cause of appwrite/appwrite#13175BigInt[] Breaks MongoDb.

The bug

MongoDB\BSON\Document::toPHP() returns a MongoDB\BSON\Int64 instance for every 64-bit BSON integer, on all platforms — php.net documents this explicitly, and since PECL mongodb 1.16.0 it is no longer 32-bit-only behaviour. Verified on both ext-mongodb 2.1.1 and 2.3.3. Client::receive() passed that tree straight through, so any stored value outside the int32 range reached callers as an object where a PHP int was expected.

Two consequences downstream:

  • Fatal on array access. Cannot use object of type MongoDB\BSON\Int64 as array — the error in the linked issue. Store [-3408048000] in an integer/bigint array column and every subsequent read of the table 500s; the table is unusable from the Console until deleted.
  • Wrong JSON. json_encode() on an Int64 emits {"$numberLong":"-3408048000"} instead of a number, because the class implements JsonSerializable in extended-JSON form. Any value that reaches a response without passing through an explicit cast is served to clients in the wrong shape.

[-42] works because a small value is encoded as int32 and decodes to a plain int — which is why this arrived as a "big numbers break my table" report rather than a type bug.

The fix

Unwrap at the decode boundary in receive(), so the whole client returns plain PHP values. On 64-bit PHP the unwrap is lossless. On 32-bit builds the wrapper is the only lossless representation, so it is left in place there.

Why here, and not in each consumer

The alternative is teaching every consumer to recognise Int64. That leaves the same trap set for the next caller, and it does not help paths with no schema to consult — utopia-php/database normalises typed integer/bigint attributes in its castingAfter() step, but anything outside that (schemaless mode, and integers nested inside object columns) has no attribute list to drive a cast. The client's contract is "PHP values in, PHP values out"; toArray() already normalises nested stdClass for the same reason. This puts int64 on the same footing.

Cost is one recursive walk of a freshly-decoded (refcount-1) response, so array writes mutate in place rather than separating.

$clusterTime is deliberately excluded

$clusterTime is stored verbatim by updateCausalConsistency() and echoed back to the server on every later command. Its signature.keyId is an int64; unwrapping it would let Document::fromPHP() re-encode a small value as an int32 and change the signed payload the server validates. Skipping that one top-level key keeps the wire representation byte-identical. This was the one non-obvious hazard in the change and is called out in a comment at the call site.

Verification

This repo

  • Added MongoTest::testInt64ValuesDecodeToNativeIntegers — a real round-trip through Mongo covering a scalar int64, an int64 inside a list, a mixed int32/int64 list, PHP_INT_MAX, and a nested document. Seen red before the fix (Failed asserting that MongoDB\BSON\Int64 Object ('integer' => '-3408048000') is of type "int") and green after.
  • Full suite: 62 tests, 310 assertions, OK. TransactionTest passes, which exercises the $clusterTime round-trip.
  • pint --test: PASS. phpstan: no errors.

Downstream, against utopia-php/database main with this client patched in

  • Reproduced a currently-live defect that castingAfter() does not cover: an object column holding {"count": -3408048000} reads back as MongoDB\BSON\Int64 and serialises to {"count":{"$numberLong":"-3408048000"}}. With this fix: int, and {"count":-3408048000}.
  • Full MongoDBTest e2e suite: 661 tests, 4962 assertions, 2 failures. Both are testObjectAttribute*EmptyObject* ({} vs []) and are unrelated to this change — they reproduce identically with an unmodified client, and are caused by my local image having utopia-php/cache 4.0.1 baked in where that repo's lock requires 4.0.2.

Review notes

  • Greptile and CodeRabbit both flagged that the new test's unconditional native-int assertions contradicted the 32-bit branch. Correct, and fixed in 4875ed6 by declaring the 64-bit requirement via markTestSkipped rather than adding per-architecture expectations that this repo's CI can never execute. Reasoning is on the threads.
  • An earlier revision of this description claimed an (int) cast on Int64 yields 1 with a warning. That is wrong for ext-mongodb 2.x, which supplies a numeric cast handler — (int)$int64 returns the correct value. The real failure modes are the two listed above.

Not verified

  • 32-bit PHP. The PHP_INT_SIZE branch is reasoned, not exercised — CI has no 32-bit target, and the new test now skips there rather than pretending otherwise.
  • A small signature.keyId. The $clusterTime exclusion is verified only in that transactions still pass with real (large) keyIds; I did not synthesise a keyId small enough to re-encode as int32 and confirm the server would have rejected it. The exclusion is a type-preservation guarantee, not a fix for an observed failure.
  • Propagation. Landing this in Appwrite needs a utopia-php/mongo release, then composer update in utopia-php/database and appwrite. No code change is needed in either — both take this via the existing 1.* constraint.
  • The "write should have been rejected" half of BigInt[] Breaks MongoDb appwrite/appwrite#13175. Not a bug: an Appwrite integer column created without explicit min/max gets size = 8, i.e. a 64-bit column, so -3408048000 is a legal value. Structure does range-validate array elements when a narrower max is set. Only the read path was broken.

MongoDB\BSON\Document::toPHP() returns a MongoDB\BSON\Int64 instance for
every 64-bit BSON integer, on all platforms, not just 32-bit ones. Any
stored value outside the int32 range therefore came back to callers as an
object where a PHP int was expected.

The wrapper is not inert. Consumers that array-access a decoded value
fatal with "Cannot use object of type MongoDB\BSON\Int64 as array", and
consumers that apply an (int) cast get 1 plus a warning — silent
corruption of the value on read. utopia-php/database hits both: a single
out-of-int32-range element inside an array column makes every subsequent
read of the table fail with a 500 (appwrite/appwrite#13175).

Unwrap at the decode boundary so the whole client returns plain PHP
values, rather than asking each consumer to recognise the wrapper. On
64-bit PHP the unwrap is lossless; on 32-bit builds the wrapper is the
only lossless representation, so it is left alone there.

$clusterTime is excluded: it is echoed back to the server verbatim on
later commands, and its signature.keyId is an int64 that must not be
re-encoded as an int32.
@coderabbitai

coderabbitai Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The client now converts BSON Int64 response values to native PHP integers on 64-bit systems. It preserves BSON wrappers on 32-bit systems and keeps $clusterTime types unchanged. Tests cover scalar, array, nested, maximum, negative, and toArray() values.

Changes

BSON Int64 normalization

Layer / File(s) Summary
Response normalization and validation
src/Client.php, tests/MongoTest.php
The client recursively converts BSON Int64 values in response results, preserves $clusterTime, and validates scalar, array, nested, boundary, and toArray() values.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Mergeability Score: 🟡 Moderate · up to 108ff

The PR correctly targets BSON int64 decoding, but its new round-trip test uses large integer literals that may become floats on 32-bit PHP, leaving that supported behavior inadequately verified and potentially causing incorrect test results. Merge should wait for platform-independent fixtures or explicit acceptance of the 32-bit test limitation.

Suggested reviewers: chiragagg5k

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: decoding BSON int64 values as native PHP integers.
✨ 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/int64-native-integers

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 13, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The client now recursively converts decoded BSON Int64 wrappers to native PHP integers on 64-bit platforms while preserving $clusterTime BSON types and retaining wrappers on 32-bit platforms.

  • Adds decode-boundary Int64 normalization for arrays and nested objects.
  • Excludes the top-level $clusterTime response field from normalization.
  • Adds 64-bit round-trip coverage for scalar, list, extreme, and nested integer values.
  • Skips the native-integer contract test on 32-bit platforms before incompatible fixtures are evaluated.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains; the prior 32-bit test incompatibility is resolved by skipping before out-of-range fixtures and native-integer assertions are reached.

Important Files Changed

Filename Overview
src/Client.php Adds recursive BSON Int64 normalization at the response boundary with a top-level $clusterTime exclusion that preserves protocol-sensitive BSON types.
tests/MongoTest.php Adds comprehensive 64-bit integer round-trip coverage and correctly skips the architecture-specific assertions on 32-bit PHP.

Reviews (2): Last reviewed commit: "test: skip the int64 round-trip on 32-bi..." | Re-trigger Greptile

Comment thread tests/MongoTest.php

@coderabbitai coderabbitai Bot left a comment

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.

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/MongoTest.php`:
- Around line 327-364: Update the int64 fixture setup in the MongoDB test around
the $negative, $positive, and $extreme values to construct BSON Int64 instances
from string literals, avoiding platform-dependent float conversion. Adjust the
assertions for result and toArray values to expect native integers with matching
values on 64-bit PHP, while expecting MongoDB\BSON\Int64 instances with matching
string representations on 32-bit PHP, including nested and list values.
🪄 Autofix

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 Plus

Run ID: c892cc42-6689-477c-aaf7-22f685015cfe

📥 Commits

Reviewing files that changed from the base of the PR and between 20f9a64 and 108ff34.

📒 Files selected for processing (2)
  • src/Client.php
  • tests/MongoTest.php

Comment thread tests/MongoTest.php
The test asserts the 64-bit contract: native integers out of the client.
On a 32-bit build normalizeInt64() keeps the wrapper on purpose, because
there it is the only lossless representation, and the literals in the
fixture would already have been coerced to float before reaching the
driver. Declare that requirement instead of letting the assertions
contradict the implementation.
@abnegate

Copy link
Copy Markdown
Member Author

@greptileai review

Pushed 4875ed6 addressing the one finding from both reviewers: the int64 round-trip test now declares a 64-bit requirement instead of asserting native integers unconditionally, which contradicted normalizeInt64()'s deliberate 32-bit branch. Reasoning for declining the per-architecture-assertions variant is on the threads.

Also worth a look:

  • The $clusterTime exclusion in receive() — the one non-obvious part. It is echoed back to the server verbatim on later commands and its signature.keyId is an int64, so unwrapping it would let a small value re-encode as int32 and change the signed payload. I want a second opinion on whether a top-level key skip is the right granularity, or whether this should be keyed on BSON type instead.
  • Placement at the decode boundary rather than in each consumer. Rationale is in the description; the deciding factor was that consumers with no schema to consult (utopia-php/database's schemaless mode, and integers nested inside object columns) have no cast step to fix.
  • Correction to the description: an earlier revision claimed (int)$int64 yields 1 with a warning. That is wrong on ext-mongodb 2.x, which has a numeric cast handler. The two real failure modes are the fatal on array access and json_encode() emitting {"$numberLong":"..."}.

@abnegate
abnegate merged commit be29ee2 into main Aug 13, 2026
5 checks passed
@abnegate
abnegate deleted the fix/int64-native-integers branch August 13, 2026 01:55
bhardwajparth51 pushed a commit to bhardwajparth51/appwrite that referenced this pull request Aug 13, 2026
Reading a row with an integer outside the int32 range returned a 500 on
MongoDB. The driver decodes 64-bit BSON integers as MongoDB\BSON\Int64
wrappers, and array-accessing one fatals with "Cannot use object of type
MongoDB\BSON\Int64 as array", so any table holding such a value became
unlistable from the Console.

1.5.3 unwraps at the decode boundary (utopia-php/mongo#49).

Fixes appwrite#13175
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