Skip to content

Bind integer parameters as strings on MySQL connections - #16

Merged
adhikjoshi merged 3 commits into
mainfrom
perf/string-int-bindings
Aug 20, 2026
Merged

adhikjoshi merged 3 commits into
mainfrom
perf/string-int-bindings

Conversation

@adhikjoshi

@adhikjoshi adhikjoshi commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Problem

MySQL 9.0.1 re-prepares a statement server-side on every execute that carries an integer-typed binary-protocol parameter. Production: 226 Com_stmt_reprepare/s — 54% of all statement executes — each one a full re-parse + re-plan of the statement on the DB server, in the same prepared-statement subsystem that produced the 1461 max_prepared_stmt_count outage.

Isolated with a clean-room PDO probe (same-DC VPS, 0.14ms RTT, EMULATE_PREPARES=false):

bind type reprepares per query
PDO::PARAM_INT 3000/3000 350–401µs
PDO::PARAM_STR (same value) 0/3000 322–326µs

~15–50µs of server CPU per int-bound query, scaling with statement size. mysql-connector-python with int params does not trigger it — this is specific to how mysqlnd types parameters. MySQL 8.0.36 with the identical stack: zero reprepares.

Fix

MySqlStringBindingConnection overrides bindValues() to send PHP ints as decimal strings with PARAM_STR (otherwise byte-identical to the framework's implementation), registered via Connection::resolverFor('mysql', …). Escape hatch: OCTANE_MYSQL_STRING_BINDINGS=false (also 0/off/no).

A string parameter is converted to the column type with full precision at execute time — verified exact for adjacent bigints beyond 2^53, where a quoted literal would compare as DOUBLE. E2E on Octane + Swoole: an 18-probe battery (inserts, whereIn, whereIntegerInRaw, paginate, limit/offset, aggregates, joins, update/delete counts, PHP_INT_MAX roundtrip) is byte-identical main vs branch; against prod 9.0.1 the per-request reprepare probe goes 1-per-int-query → 0.

Semantic footnote: an int bound against a varchar column previously compared numerically ('05' matched 5, index defeated) and now compares as a string (exact, indexed). Every id-like varchar/char column in modelslab_prod and gpulab_prod was scanned for non-canonical numeric values: zero found.

Tests

  • Strict-typed unit tests on the override (mutation-tested: reverting stringification errors both), resolver registration test (mutation-tested: removing registration fails it).
  • Full suite: 174 green.
  • Adversarially reviewed; the gate-spelling bug and a wrong comment mechanism it found are fixed in the second commit.

🤖 Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

MySQL 9.0.x re-prepares a statement on every execute that carries a
PARAM_INT parameter; mysqlnd silently retries, adding two round trips
per int-bound query. Measured at 226 reprepares/s (54% of executes) in
production. Binding the same values as PARAM_STR produces identical
plans and results with zero reprepares.

Opt out with OCTANE_MYSQL_STRING_BINDINGS=false.
- The gate now honors '0'/'off'/'no' via FILTER_VALIDATE_BOOL, not just
  boolean false - operators disable features with '0' during incidents.
- The class comment claimed string parameters behave 'exactly like a
  quoted literal'; parameters are exact where literals compare as DOUBLE
  (adjacent-bigint probe beyond 2^53). Rewritten so nobody extends the
  literal reasoning into interpolated SQL.
- The unit test mixed positional and named placeholders in one
  statement, which real PDO rejects; split into two statements.
@adhikjoshi
adhikjoshi merged commit ade4873 into main Aug 20, 2026
3 checks passed
@adhikjoshi
adhikjoshi deleted the perf/string-int-bindings branch August 20, 2026 16:47
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