Skip to content

feat(async): event-loop async support — fd primitive, adapters, native Swoole, shared reactor - #138

Merged
CodeLieutenant merged 20 commits into
trunkfrom
claude/scylladb-php-async-support-904e8f
Aug 8, 2026
Merged

feat(async): event-loop async support — fd primitive, adapters, native Swoole, shared reactor#138
CodeLieutenant merged 20 commits into
trunkfrom
claude/scylladb-php-async-support-904e8f

Conversation

@CodeLieutenant

Copy link
Copy Markdown
Member

Implements #24 — makes the driver usable inside non-blocking PHP event loops instead of only blocking on Future::get().

The C driver resolves futures on its own IO thread(s); this exposes that completion to userland so any loop can await a query without blocking. get() stays blocking, so existing sync code is unaffected and pays nothing.

What's in it

1. Core primitive (C) — works with any loop, zero deps
Every Future gains:

$future->getResource(): resource   // readable once the future resolves
$future->isReady(): bool

getResource() returns a php_stream that becomes readable exactly once when the driver resolves the future (via cass_future_set_callback + a self-pipe). Watch it with stream_select/ReactPHP/AMPHP/Swoole; on readable, get() returns without blocking. Linux uses eventfd (1 syscall, 1 fd, no SIGPIPE); macOS falls back to a pipe. src/FutureNotifier.{c,h}.

2. Framework adapters (lib/Async/, optional peer deps — require-dev + suggest)
Revolt (fiber await, the default), Amp (toFuture), ReactPhp (toPromise), Swoole (coroutine waitEvent).

3. Native Swoole/OpenSwoole — opt-in build flag
-DPHP_SCYLLADB_ENABLE_SWOOLE / _OPENSWOOLE (+ -DPHP_SCYLLADB_SWOOLE_SRC=…) compiles one small C++ shim (src/Async/SwooleBridge.cc) so Future::get() suspends the current coroutine instead of blocking. Default build stays pure C; swoole symbols resolve lazily at runtime.

4. Shared reactor — opt-in, O(1) fds for high fan-out
Cassandra\Async\Reactor (::add/resource/poll/pending): one eventfd + a mutex-guarded MPSC completion queue shared across many futures, so the loop watches one fd regardless of concurrency. Per-future fds cap out at FD_SETSIZE (~512 on macOS); the reactor scales to thousands and is even a touch faster at matched fan-out. lib/Async/ReactorRevolt.php adapter. See docs/async.md.

Testing

  • tests/Feature/Async/* against live ScyllaDB: primitive (stream_select multiplexing, error propagation, early-free SIGPIPE regression), Revolt/Amp/React (concurrency), Swoole (skip-guarded), reactor (io_threads=4 multi-producer, errors, model-mixing guards, Revolt adapter). All green; full suite 880 passed.
  • Benchmarks benchmarks/Live/{AsyncBench,ReactorBench}.php + committed baselines under benchmarks/baselines/ (JSON + Markdown, via composer bench:baseline:export).
  • fd/mem leak checks clean; two adversarial cross-thread C reviews (the reactor one caught + fixed a narrow-window UAF — signal now under the lock).

Notes for reviewers

  • Draft because two things want CI/hardware this environment lacks:
    • Native Swoole path: the flag-off default build + the C++ shim compile-check against real swoole-src are verified here, but the runtime coroutine behavior needs an actual (Open)Swoole runtime (not installed locally). Its tests are skip-guarded.
    • Reactor cross-thread code: local macOS ASan doesn't instrument cleanly, so the Linux CI ASan job is the authoritative race gate (covered locally by the adversarial review + the io_threads=4 stress + leak checks).
  • Generated *_arginfo.h / *_descriptor.c are build-time artifacts (git-ignored), regenerated from the committed stubs.
  • The one pre-existing full-suite failure (UuidTest › unique UUIDs across separate processes) is environmental — it spawns php -d extension=cassandra children by bare name and passes in CI where the extension is installed.

🤖 Generated with Claude Code

CodeLieutenant and others added 9 commits August 5, 2026 09:15
…e Swoole, shared reactor

Implements #24: let the driver participate in non-blocking PHP event loops
instead of only blocking on Future::get().

Core primitive (C, src/FutureNotifier.{c,h}):
- Every Future gains getResource(): resource and isReady(): bool. getResource()
  returns a php_stream that becomes readable once, when the driver resolves the
  future on its IO thread (cass_future_set_callback + self-pipe). Any loop
  (stream_select / ReactPHP / AMPHP / Swoole) can await without blocking; get()
  stays blocking, so sync users pay nothing.
- Refcounted, persistent-malloc notifier. Linux eventfd fast path (1 syscall,
  1 fd, no SIGPIPE) with a POSIX pipe fallback on macOS; SIGPIPE-safe dup for the
  stream; register-then-recheck closes lost-wakeup races.

Framework adapters (lib/Async, optional peer deps — require-dev + suggest):
- Revolt (fiber await, default), Amp (toFuture), ReactPhp (toPromise),
  Swoole (coroutine waitEvent).

Native Swoole/OpenSwoole (opt-in build flag):
- -DPHP_SCYLLADB_ENABLE_SWOOLE / _OPENSWOOLE compiles one C++ shim
  (src/Async/SwooleBridge.cc) so Future::get() suspends the current coroutine
  instead of blocking. Default build stays pure C; swoole symbols resolve lazily.

Shared reactor (opt-in, O(1) fds for high fan-out):
- Cassandra\Async\Reactor + src/Async/Reactor.{c,h}: one eventfd + mutex-guarded
  MPSC completion queue shared across many futures, so the loop watches ONE fd
  regardless of concurrency. fd+mutex are module/thread-lifetime; registration
  state resets each request. lib/Async/ReactorRevolt.php adapter.

Tests + benchmarks:
- tests/Feature/Async/*: primitive, Revolt/Amp/React/Swoole (guarded), reactor
  (io_threads=4 multi-producer, error propagation, model-mixing guards).
- benchmarks/Live/{AsyncBench,ReactorBench}.php + committed baselines under
  benchmarks/baselines/ (JSON + Markdown) via composer bench:baseline:export.
- docs/async.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…guard

- FutureNotifier.c: reorder notifier_ensure so the register-then-recheck poke
  happens BEFORE dropping the callback ref, not after. Functionally identical
  (refcount is 2 across the poke, never freed), but removes the path clang-tidy's
  analyzer flagged as use-after-free (clang-analyzer-unix.Malloc).
- SwooleBridge.cc: wrap the whole TU in #ifdef HAVE_SWOOLE_COROUTINE so tooling
  that analyzes the default build (clang-tidy) doesn't choke on the absent
  <swoole.h> — the file is only compiled when the swoole build flag is set.
- ReactorTest: "surfaces a failed query" now loops until its future is
  dispatched instead of asserting a single select+poll. The reactor fd is
  level-triggered and can report a spurious wake, so the single-shot form was
  flaky across tests (leaked a pending future into the next test on CI).
- test.yml (pre-existing ASan failure, unrelated to async): disable opcache in
  the ASan job — PHP dlopen()s opcache with RTLD_DEEPBIND, which the sanitizer
  runtime rejects and aborts on before any test runs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…DEEPBIND exts under ASan

- FutureNotifier.c: on the set_callback-failed path drop the callback ref with a
  direct atomic decrement instead of unref(). Refcount is provably 2→1 there
  (creation ref always survives), so it can never free; the inline decrement
  stops clang-analyzer-unix.Malloc from modelling an impossible free of the
  pointer returned in *notifier_slot.
- Reactor.c: reactor_consume now drops the reg's two refs (ready-list +
  registered) in a single atomic sub-2 via reg_release() instead of two
  reg_unref() calls, so the analyzer no longer sees a deref-after-free between
  the two decrements. Verified clean with clang-analyzer-unix.Malloc locally.
- test.yml ASan job: also disable mysqli/pdo_mysql/mysqlnd (pre-existing; the
  8.4 runner dlopen()s mysqlnd with RTLD_DEEPBIND, same abort as opcache). The
  8.5 ASan job already runs the full suite (incl. reactor) clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reverts the opcache/mysql extension-disabling in the ASan job. On PHP 8.4 the
runner dlopen()s opcache/mysqlnd with RTLD_DEEPBIND (incompatible with the
sanitizer runtime → abort before tests) — a pre-existing failure that also fails
on trunk and is unrelated to this PR. Disabling those extensions to work around
it broke the 8.4/8.5 mysqli↔mysqlnd dependency at startup, so it's not a net
improvement. Left as-is for a dedicated CI-infra change; the 8.5 ASan job already
exercises the full suite (incl. the reactor) memory-safety clean.

The C-side clang-tidy fixes (FutureNotifier direct decrement, Reactor reg_release)
are retained.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
PHP 8.4's runner dlopen()s opcache/mysqlnd with RTLD_DEEPBIND, which the
sanitizer runtime rejects (aborts before any test runs) — a pre-existing failure
(also red on trunk). Disable those extensions for 8.4 only via a matrix-
conditional so the 8.4 ASan run can start; 8.5 is left untouched (it doesn't hit
the abort, and disabling mysqlnd there breaks the mysqli↔mysqlnd startup dep).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…fra issue

Disabling extensions one-by-one on the 8.4 ASan job is whack-a-mole: the runner
dlopen()s opcache, mysqlnd, pdo, … with RTLD_DEEPBIND, which the sanitizer
runtime rejects. This is a fundamental 8.4-runner/ASan incompatibility that also
fails on trunk and is unrelated to this PR; it needs a dedicated CI change (e.g.
drop 8.4 from the ASan matrix, or build PHP without RTLD_DEEPBIND). Leaving
test.yml untouched. The 8.5 ASan job already validates this PR's memory safety.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Add a `swoole` CI job (matrix: swoole@8.3/8.4, openswoole@8.3) that builds the
  extension with -DPHP_SCYLLADB_ENABLE_SWOOLE/_OPENSWOOLE against the runtime's
  own source headers (checked out at the installed version for ABI match), loads
  the coroutine runtime, and runs the swoole test group against live ScyllaDB.
- lib/Async/Swoole.php: make the adapter flavor-aware — detect Swoole\ vs
  OpenSwoole\ Coroutine/System at runtime and use the runtime's EVENT_READ
  constant instead of a hardcoded value.
- SwooleTest: flavor-aware coroutine runner; cover native coroutine-aware get(),
  the adapter, and many in-flight futures. Runs when swoole/openswoole is loaded.
- test.yml: add lib/** to the path filter so async userland changes trigger CI.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The CI swoole jobs failed with `undefined symbol: php_scylladb_swoole_current_cid`.
Two causes:
- The #ifdef HAVE_SWOOLE_COROUTINE guard on SwooleBridge.cc compiled it to an
  empty object under GCC (the target define wasn't reaching the .cc), so the shim
  symbols were absent. The guard was only for clang-tidy; drop it and instead
  exclude the file from the clang-tidy file list (it needs the swoole source
  headers to parse).
- OpenSwoole renamed its headers (openswoole_*.h), C++ namespace (`openswoole`),
  and event flag (OSW_EVENT_READ). Select the runtime with __has_include(<openswoole.h>)
  (keyed on the include path, not a define) + a namespace alias, so one shim
  builds against either tree.

Also: don't re-load the coroutine ext via -d in the test run (setup-php already
loads it via ini — avoids a "module already loaded" warning).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… fresh configure

The project declares LANGUAGES C. CXX is otherwise enabled late by the
legacy .cpp modules added after src/. This subdir is configured before
them, so a fresh configure silently drops SwooleBridge.cc from the src
target and the native shim symbols (php_scylladb_swoole_current_cid)
are left undefined. Call enable_language(CXX) in the swoole block.
@CodeLieutenant
CodeLieutenant force-pushed the claude/scylladb-php-async-support-904e8f branch from 6116998 to 51deb3a Compare August 5, 2026 07:16
@CodeLieutenant CodeLieutenant self-assigned this Aug 5, 2026
@CodeLieutenant
CodeLieutenant marked this pull request as ready for review August 8, 2026 10:28
@CodeLieutenant
CodeLieutenant requested a lite review from Copilot August 8, 2026 10:28
@mergify

mergify Bot commented Aug 8, 2026

Copy link
Copy Markdown

Tick the box to add this pull request to the merge queue (same as @mergifyio queue).

  • Queue this pull request

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Adds non-blocking async/event-loop support to the ScyllaDB PHP driver by exposing a “readable-on-completion” stream for futures, plus optional framework adapters and an opt-in shared reactor to reduce FD usage under high concurrency. This enables awaiting query completion inside ReactPHP/Amp/Revolt/Swoole loops without blocking, while keeping Future::get() blocking behavior unchanged for sync users (except the opt-in native Swoole build path).

Changes:

  • Extend Cassandra\Future (and concrete Future types) with getResource() and isReady() backed by a new C notifier (eventfd on Linux, pipe fallback elsewhere).
  • Add Cassandra\Async\Reactor (shared eventfd + MPSC completion queue) and Revolt adapter for O(1) watcher scaling.
  • Add optional userland adapters/tests/benchmarks/docs plus CI coverage for the opt-in native (Open)Swoole coroutine build.

Reviewed changes

Copilot reviewed 44 out of 45 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
tests/Feature/Async/SwooleTest.php Adds feature tests for Swoole/OpenSwoole coroutine awaiting and native build behavior.
tests/Feature/Async/RevoltAwaitTest.php Adds Revolt fiber-await feature tests (single + concurrent + error propagation).
tests/Feature/Async/ResourcePrimitiveTest.php Adds framework-agnostic tests for Future::getResource() / isReady() semantics.
tests/Feature/Async/ReactPromiseTest.php Adds ReactPHP promise adapter feature tests.
tests/Feature/Async/ReactorTest.php Adds shared reactor feature tests (fan-out, MPSC IO threads, mixing guards).
tests/Feature/Async/AmpFutureTest.php Adds Amp adapter feature tests.
src/php_scylladb.c Wires reactor lifecycle into module globals (GINIT/GSHUTDOWN/RSHUTDOWN).
src/FutureValue.stub.php Extends FutureValue API surface with getResource() / isReady().
src/FutureValue.c Implements FutureValue getResource()/isReady() and notifier lifetime management.
src/FutureSession.stub.php Extends FutureSession API surface with getResource() / isReady().
src/FutureSession.c Makes get() coroutine-aware via notifier wait; adds getResource()/isReady().
src/FutureRows.stub.php Extends FutureRows API surface with getResource() / isReady().
src/FutureRows.c Adds resource primitive + reactor mixing guard + notifier cleanup for FutureRows.
src/FuturePreparedStatement.stub.php Extends FuturePreparedStatement API surface with getResource() / isReady().
src/FuturePreparedStatement.c Makes get() coroutine-aware; adds getResource()/isReady() + notifier cleanup.
src/FutureNotifier.h Introduces cross-thread completion notifier API (callback-safe, persistent allocation).
src/FutureNotifier.c Implements notifier backend (eventfd/pipe), stream publication, and coroutine-aware waiting.
src/FutureClose.stub.php Extends FutureClose API surface with getResource() / isReady().
src/FutureClose.c Makes get() coroutine-aware; adds getResource()/isReady() + notifier cleanup.
src/Future.stub.php Extends Cassandra\Future interface with getResource() and isReady() docs/contract.
src/CMakeLists.txt Adds reactor/notifier sources and opt-in native Swoole C++ bridge build logic.
src/Async/SwooleBridge.h Declares C-callable wrappers for Swoole coroutine wait APIs (guarded by build flag).
src/Async/SwooleBridge.cc Implements native coroutine waiting shim for (Open)Swoole (only C++ TU).
src/Async/Reactor.stub.php Adds PHP stub for Cassandra\Async\Reactor API.
src/Async/Reactor.h Declares reactor lifecycle functions for module init/shutdown integration.
src/Async/Reactor.c Implements shared reactor (single fd + ready queue) and PHP-facing static methods.
lib/Async/Swoole.php Adds userland Swoole/OpenSwoole await adapter using waitEvent + getResource().
lib/Async/Revolt.php Adds Revolt-based fiber await adapter (await/awaitAll) built on onReadable watcher.
lib/Async/ReactPhp.php Adds ReactPHP adapter converting a Future to a Promise settled on fd readability.
lib/Async/ReactorRevolt.php Adds Revolt adapter for the shared reactor (single watcher dispatching many futures).
lib/Async/Amp.php Adds Amp v3 adapter converting a Future to an Amp\Future via Revolt watcher.
include/php_scylladb_types.h Extends Future structs with notifier/stream fields and reactor registration pointer.
include/php_scylladb_globals.h Adds reactor pointer to module globals (forward-declared).
docs/async.md Documents the async primitive, adapters, native Swoole build, and shared reactor usage/limits.
composer.lock Adds dev dependencies for Revolt/Amp/React used by adapters and tests.
composer.json Adds adapter autoload mapping, suggests optional deps, and introduces benchmark export script.
CMakeLists.txt Adds top-level CMake options for native Swoole/OpenSwoole build configuration.
benchmarks/Live/ReactorBench.php Adds live benchmark comparing per-future fds vs shared reactor at varying fan-out.
benchmarks/Live/AsyncBench.php Adds an event-loop-style benchmark using getResource() + stream_select.
benchmarks/baselines/README.md Adds committed baseline documentation for PhpBench regression tracking.
benchmarks/baselines/php85-offline.json Adds committed offline benchmark baseline data.
benchmarks/baselines/php85-live.json Adds committed live benchmark baseline data.
benchmarks/baseline.sh Adds helper script to export baselines (best-effort live group, always offline).
benchmarks/baseline.php Adds parser to convert PhpBench XML dumps into portable JSON + Markdown summaries.
.github/workflows/test.yml Expands CI triggers to lib/** and adds a dedicated native Swoole/OpenSwoole job.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/FutureNotifier.c
Comment thread benchmarks/Live/ReactorBench.php
Comment thread src/Async/Reactor.c Outdated
Comment thread src/FutureNotifier.c
Adds Cassandra\Async\PollHandle, a driver notification descriptor as a native
Io\Poll\Handle, and Cassandra\Async\Poll, a loop over Io\Poll\Context that
watches futures and drops each watcher as its future resolves. A driver
descriptor stays readable after it fires, so a watcher left in place makes the
loop spin.

Io\Poll\Context::add() takes an Io\Poll\Handle, and 8.6 ships no handle class
for a plain PHP stream, so a driver descriptor cannot reach the polling API
without one. The handle also skips a layer: it registers the driver's own
descriptor, with no dup() and no stream resource per watched future.

Off by default. PHP 8.6 is unreleased and Io\Poll\Context::wait() changed shape
after 8.6.0alpha3, so a default-on build would track a moving target. Build with
-DPHP_SCYLLADB_ENABLE_POLL_API=ON, or --enable-poll-api through pie or pecl.

The class registry now resolves an external dependency in CG(class_table)
instead of zend_lookup_class(). The latter reads EG(class_table), which
init_executor() only fills at request start, so at MINIT it dereferences a null
table. Io\Poll\Handle comes from ext/standard, whose MINIT runs first.
The Revolt, Amp, ReactPHP and Swoole adapters are userland PHP with framework
dependencies the extension itself does not need. They move from lib/Async to
packages/async-adapters, published as codelieutenant/scylla-driver-async-adapters
and pulled in here through a path repository.

Installing the driver no longer implies installing a framework, and a user who
wants one adapter no longer carries all four.
FutureRows::get() caches the decoded row array and hands every Rows object a
refcounted copy of it, so the array's own internal pointer was shared between
the future and every Rows built from it. Iterating one moved the other. A debug
PHP aborts on the ZEND_ASSERT in zend_hash_internal_pointer_reset_ex, and a
release build silently shares the cursor.

The async work makes this the normal path: a loop holds futures alive and calls
get() on each as it resolves, which is exactly the case that shares the array.

Rows now carries its own HashPosition and uses the _ex iteration API.
Rows::first() is fixed by the same change. It reset a local position and then
read through the internal pointer, so after any iteration it returned the
current row instead of the first one.
Reactor::add() took Cassandra\Future but threw for anything except FutureRows.
It now accepts every concrete future, so a boot sequence can fan out
prepareAsync() the same way a request fans out executeAsync(). A FutureValue
resolves on construction and goes straight to the ready list.

Each future keeps its notifier and its reactor registration at its own struct
offset. One resolver in FutureNotifier.c now knows where, and the reactor, the
Io\Poll handle and the poll loop share it instead of each carrying a copy of the
class ladder. A registration records the resolved CassFuture and a pointer to
the future's own slot, so dispatch and drain no longer re-derive either from the
class. The "one async model per future" guard is shared too, and now covers all
five types in both directions.

Two contract fixes reported by review on #138:

  - cass_future_set_callback() failing on a still-pending future left the
    descriptor readable with nothing to read. poll() would hand back a future
    whose get() blocks. Both the notifier and the reactor now roll the
    registration back and throw, and only self-signal a future that has already
    resolved.

  - A callback that throws makes poll() return nothing, which dropped whatever
    that call had already collected. Those futures were unregistered but never
    handed to userland, so they are registered again at the front of the queue
    and the next poll() returns them first.

Under a native Swoole build, get() on a reactor-registered future tried to
install a second driver callback. wait_coro() now takes the registration and
falls back to the plain timed wait, which does not block: poll() only returns
futures that have already resolved.
Nothing in the matrix built against PHP 8.6, so Cassandra\Async\Poll and
PollHandle had no coverage at all. Adds a job that does, and a
build-extension input to drive the flag.

The job builds with PHP_SCYLLADB_ENABLE_POLL_API=ON rather than AUTO on
purpose: AUTO against a PHP without the header produces an extension with no
Poll classes and a test file that skips itself green. ON fails the build
instead. The job asserts the classes exist before it runs the suite.

It reports without gating (continue-on-error), because 8.6 is unreleased and
Io\Poll\Context::wait() is still changing shape.

The push and pull_request path filters watched lib/**, which no longer exists
after the adapters moved to packages/.
Reported by review on #138: ReactorBench declared ParamProviders at class level
and again on benchSharedReactorHighFanout. PhpBench combines providers, and both
used the 'requests' key, so the high-fanout subject produced duplicate variants
and duplicate baseline rows. Each subject now names the provider it wants, which
gives 64/256 twice and 1024/4096 once.

docs/async.md said a throwing callback loses nothing only for the queued
remainder, which was not true for the completions poll() had already collected,
and it offered Io\Poll\StreamPollHandle as an alternative to PollHandle. No such
class exists: Io\Poll\Context::add() takes an Io\Poll\Handle, and 8.6 ships no
handle for a plain PHP stream. Both are corrected, along with the note that the
reactor now takes every future type.
The guide and the reference both stated that the driver has no callback and no
event loop integration. That has not been true since the completion descriptor
landed, and a reader following either page would conclude the feature does not
exist.

Adds guide/event-loops.md, which covers the getResource()/isReady() primitive
and its contract, stream_select() without a framework, the adapter package, the
shared reactor and its batching, native Io\Poll on 8.6, and coroutine-aware
get() under a native Swoole build. Adds reference/async.md for Reactor, Poll and
PollHandle. Corrects the two stale sections and wires both pages into the
sidebar.
Replace the integral macros in InetUtil.c with an enum, and read the
address fields with strtoul() and strtol() instead of sscanf() and
atoi(). The tokenizer only emits validated digits, so the values are
unchanged.

Remove the redundant casts around export_twos_complement() and around
the tuple index lookup.

Drop the gnu::nonnull attribute from the FutureUtil declarations. The
driver returns a null future on an allocation failure, and both
functions report that as a PHP exception, so the attribute let the
optimizer delete the guard.

Zero-initialize the zvals in TypeFactory.c. ZVAL_UNDEF only writes the
type tag, so the value union stayed indeterminate in zvals that the
code copies and returns by value.
php_scylladb_format_decimal() asserted that the scale is not negative.
A positive exponent makes the scale negative, so new Decimal('1e3')
aborted a debug build and produced a wrong string in a release build.

Print a negative scale in scientific notation, which is the rule that
BigDecimal.toString() uses. ScyllaDB stores the CQL literal 1E+3 as the
same pair, an unscaled 1 with a scale of -3, and the round trip through
the wire keeps both fields.

Size the output buffer after the digit count is known. The old
allocation added the scale to the length, so a negative scale made the
buffer too small for the digits.

Copy only the fractional digits when an exponent follows them. The old
length also copied the 'e', so '1.5e2' failed to parse.

Parse the exponent with strtol() and check the range. sscanf() with %d
reports a read failure as EOF, which the old test accepted, and it
overflows without a report. This also clears a clang-tidy finding.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 72 out of 73 changed files in this pull request and generated 1 comment.

Comment thread src/Numbers/NumberParser.c
strtol() stops at the first character it cannot use and skips leading
whitespace, so "1e2foo" and "1e 2" parsed as 1e2. Require the exponent
token to start with a digit or a minus sign, and to end at the end of
the string.

Also stop the clang-tidy job from linting Poll.c and PollHandle.c: they
need main/php_poll.h, which only PHP 8.6 ships, and the job builds
against PHP 8.5. Run the PHP 8.6 Io\\Poll job on demand only until 8.6
is stable.
@CodeLieutenant
CodeLieutenant merged commit e4a46fd into trunk Aug 8, 2026
35 checks passed
@CodeLieutenant
CodeLieutenant deleted the claude/scylladb-php-async-support-904e8f branch August 8, 2026 16:56
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