Skip to content

Commit ad746e9

Browse files
committed
fix: stringify scalar columns to match stock wpdb's mysqli typing
The bridge returns native int/float, but mysqli returns every scalar column as a string (mysqlnd only types natively under MYSQLI_OPT_INT_AND_FLOAT_NATIVE, which wpdb never sets). Core and plugins strict-compare against those strings — wp-includes/blocks/ comments-title.php does '0' === get_comments_number(), so native ints made every comment-less post render a spurious "0 responses" heading. Found by byte-diffing rendered HTML against stock wpdb under real WordPress. Stringify non-NULL scalars during row hydration (floats via PHP's default float-to-string conversion; NULL stays null), fix the README claim that native types survive, and add a regression test asserting COUNT(*) comes back as string "0" with the exact comments-title strict compare, plus a non-integral float case.
1 parent aec5d4b commit ad746e9

3 files changed

Lines changed: 67 additions & 15 deletions

File tree

README.md

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -166,9 +166,17 @@ on the state this class populates.
166166
| mysqli pass-through (`$wpdb->dbh`, `mysqli_result` access) | **not supported**`$dbh` is always `null`; code that reaches into it for raw mysqli calls will not work |
167167
| `get_col_length()` | **degraded** — always returns `false` (no `SHOW FULL COLUMNS` interrogation), so core skips its PHP-side length truncation. SQLite does not enforce `VARCHAR(n)` lengths anyway |
168168

169-
Native types survive the bridge: integer columns arrive as PHP `int`,
170-
floats as `float`, `NULL` as `null` — same as mysqlnd with default
171-
settings.
169+
**Result typing matches stock wpdb: every scalar column is a string.**
170+
mysqli returns all scalars as strings unless
171+
`MYSQLI_OPT_INT_AND_FLOAT_NATIVE` is set — and wpdb never sets it — so
172+
core and plugins strict-compare against string values (core's
173+
comments-title block does `'0' === get_comments_number()`, for
174+
example). The bridge itself hands back native `int`/`float`, and the
175+
drop-in deliberately stringifies them during row hydration to preserve
176+
that contract; `NULL` stays `null`, exactly as under mysqli. Floats use
177+
PHP's default float-to-string conversion. Exposing the bridge's native
178+
typing could become an explicit opt-in in a future release — it is not
179+
implemented today.
172180

173181
---
174182

@@ -332,7 +340,9 @@ database via the embed SAPI. [ephpm#257](https://github.com/ephpm/ephpm/pull/257
332340
registered two host functions into PHP's global function table:
333341

334342
- `ephpm_db_query(string $sql, array $params = []): array` — rows as a
335-
list of associative arrays; int/float/null arrive as native PHP types.
343+
list of associative arrays; int/float/null arrive as native PHP types
344+
(which the drop-in stringifies during hydration to match stock wpdb —
345+
see [What is implemented](#what-is-implemented)).
336346
- `ephpm_db_execute(string $sql, array $params = []): array`
337347
`{affected_rows, last_insert_id}`.
338348

src/Db.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,23 @@ public function query($query)
218218
$num_rows = 0;
219219

220220
foreach ($this->bridgeRows ?? [] as $row) {
221+
/*
222+
* Match stock wpdb's typing contract exactly: mysqli
223+
* returns EVERY scalar column as a string (mysqlnd only
224+
* types natively under MYSQLI_OPT_INT_AND_FLOAT_NATIVE,
225+
* which wpdb never sets). The bridge hands us native
226+
* int/float, so stringify them here — core and plugins
227+
* strict-compare against string values (e.g.
228+
* `'0' === get_comments_number()` in the comments-title
229+
* block) and native types break those paths. NULL stays
230+
* null, as it does under mysqli. Floats use PHP's default
231+
* float-to-string conversion.
232+
*/
233+
foreach ($row as $col => $value) {
234+
if (null !== $value && !\is_string($value)) {
235+
$row[$col] = (string) $value;
236+
}
237+
}
221238
$this->last_result[$num_rows] = (object) $row;
222239
++$num_rows;
223240
}

tests/DbTest.php

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -120,17 +120,17 @@ public function testGetResultsObjectArrayAAndArrayN(): void
120120
$this->assertCount(1, $objects);
121121
$this->assertInstanceOf(\stdClass::class, $objects[0]);
122122
$this->assertSame('widget', $objects[0]->name);
123-
$this->assertSame(1, $objects[0]->id); // Native int, not "1".
124-
$this->assertSame(3, $objects[0]->qty);
125-
$this->assertSame(1.5, $objects[0]->price); // Native float.
126-
$this->assertNull($objects[0]->note); // SQL NULL -> null.
123+
$this->assertSame('1', $objects[0]->id); // String, like mysqli.
124+
$this->assertSame('3', $objects[0]->qty);
125+
$this->assertSame('1.5', $objects[0]->price); // String, like mysqli.
126+
$this->assertNull($objects[0]->note); // SQL NULL -> null.
127127
$this->assertSame(1, $db->num_rows);
128128

129129
$assoc = $db->get_results('SELECT id, name FROM wp_items', ARRAY_A);
130-
$this->assertSame([['id' => 1, 'name' => 'widget']], $assoc);
130+
$this->assertSame([['id' => '1', 'name' => 'widget']], $assoc);
131131

132132
$numeric = $db->get_results('SELECT id, name FROM wp_items', ARRAY_N);
133-
$this->assertSame([[1, 'widget']], $numeric);
133+
$this->assertSame([['1', 'widget']], $numeric);
134134
}
135135

136136
public function testGetRowGetVarGetCol(): void
@@ -144,9 +144,9 @@ public function testGetRowGetVarGetCol(): void
144144
$this->assertSame('widget', $row->name);
145145

146146
$rowA = $db->get_row('SELECT name, qty FROM wp_items ORDER BY id LIMIT 1', ARRAY_A);
147-
$this->assertSame(['name' => 'widget', 'qty' => 3], $rowA);
147+
$this->assertSame(['name' => 'widget', 'qty' => '3'], $rowA);
148148

149-
$this->assertSame(10, $db->get_var('SELECT SUM(qty) FROM wp_items'));
149+
$this->assertSame('10', $db->get_var('SELECT SUM(qty) FROM wp_items'));
150150
$this->assertSame(['widget', 'gadget'], $db->get_col('SELECT name FROM wp_items ORDER BY id'));
151151
$this->assertNull($db->get_var('SELECT name FROM wp_items WHERE qty = 999'));
152152
}
@@ -242,7 +242,32 @@ public function testPreparePlaceholders(): void
242242
$this->assertStringContainsString('`wp_items`', $sql);
243243
$this->assertStringContainsString("'it\\'s'", $sql);
244244

245-
$this->assertSame(5, $db->get_var($sql));
245+
$this->assertSame('5', $db->get_var($sql));
246+
}
247+
248+
/**
249+
* Regression: stock wpdb returns every scalar column as a string
250+
* (mysqli without MYSQLI_OPT_INT_AND_FLOAT_NATIVE, which wpdb never
251+
* sets). Core strict-compares against those strings — e.g. the
252+
* comments-title block does `'0' === get_comments_number()` — so
253+
* native int/float hydration renders wrong output. NULL must stay
254+
* null, exactly as under mysqli.
255+
*/
256+
public function testScalarColumnsComeBackAsStringsLikeStockWpdb(): void
257+
{
258+
$db = $this->makeDbWithTable();
259+
$db->insert('wp_items', ['name' => 'post']);
260+
261+
$count = $db->get_var(
262+
"SELECT COUNT(*) FROM wp_items WHERE name = 'no-such-comment'"
263+
);
264+
$this->assertSame('0', $count);
265+
$this->assertTrue('0' === $count); // The comments-title comparison.
266+
267+
// Non-integral float: PHP's default float-to-string conversion.
268+
$db->insert('wp_items', ['name' => 'priced', 'qty' => 7, 'price' => 2.25], ['%s', '%d', '%f']);
269+
$row = $db->get_row("SELECT qty, price, note FROM wp_items WHERE name = 'priced'", ARRAY_A);
270+
$this->assertSame(['qty' => '7', 'price' => '2.25', 'note' => null], $row);
246271
}
247272

248273
public function testPreparedLikeWithEscapedPercentLiteral(): void
@@ -302,12 +327,12 @@ public function testTransactionsFlowThroughAsSql(): void
302327
$db->query('BEGIN');
303328
$db->insert('wp_items', ['name' => 'temp']);
304329
$db->query('ROLLBACK');
305-
$this->assertSame(0, $db->get_var('SELECT COUNT(*) FROM wp_items'));
330+
$this->assertSame('0', $db->get_var('SELECT COUNT(*) FROM wp_items'));
306331

307332
$db->query('BEGIN');
308333
$db->insert('wp_items', ['name' => 'kept']);
309334
$db->query('COMMIT');
310-
$this->assertSame(1, $db->get_var('SELECT COUNT(*) FROM wp_items'));
335+
$this->assertSame('1', $db->get_var('SELECT COUNT(*) FROM wp_items'));
311336
}
312337

313338
public function testLeadingCommentRouting(): void

0 commit comments

Comments
 (0)