Skip to content

Troubleshooting

The exact error messages ZenDB throws, what each one means, and the fix. Plus deprecation warnings, connection problems, behavioral gotchas, and how to see the SQL a call actually ran. Headings quote the error text so you can find them by search.

Full message includes the quoted text and the fix, for example: Quotes not allowed in template. Replace 'John' with :paramName and add: [ ':paramName' => 'John' ]

What happened: The SQL template contains a quote character (single or double). Templates are code, values are data; the guard rejects quotes so a value can never be concatenated into the SQL.

// Throws - quoted value in template
DB::select('users', "name = 'John'");

Fix: Move the value into a placeholder:

DB::select('users', "name = ?", 'John');
// SELECT * FROM `users` WHERE name = 'John'

Full message includes the number and the fix, for example: Standalone number in template. Replace 21 with :n21 and add: [ ':n21' => 21 ]

What happened: The SQL template contains a literal number. A literal number in a template is indistinguishable from user input that was concatenated in, so the guard rejects it.

// Throws - literal number in template
DB::select('users', "age > 21");

Fix: Use a placeholder:

DB::select('users', "age > ?", 21);
// SELECT * FROM `users` WHERE age > 21

Exception: a trailing LIMIT 10 is allowed and runs exactly as written. MySQL’s LIMIT only accepts literal integers, so the guard strips a trailing LIMIT # from the copy it checks; anything that isn’t a plain trailing LIMIT # still throws.

Hitting this on CREATE TABLE or ALTER TABLE? Type lengths like VARCHAR(255) are standalone numbers too. Run schema statements through DB::$mysqli->query(), which is plain mysqli with no template guard.

“Max 3 positional arguments allowed. For more, use named placeholders: [':name' => $value]”

Section titled ““Max 3 positional arguments allowed. For more, use named placeholders: [':name' => $value]””

What happened: More than 3 values were passed for ? placeholders. Positional placeholders stop at three because matching each ? to its value by counting positions is where mistakes start.

// Throws - four positional values
DB::select('users', "a = ? AND b = ? AND c = ? AND d = ?", 1, 2, 3, 4);

Fix: Switch to named placeholders:

DB::select('users', "a = :a AND b = :b AND c = :c AND d = :d", [
':a' => 1,
':b' => 2,
':c' => 3,
':d' => 4,
]);

“Can’t mix positional (?) and named (:param) placeholders. Use one style consistently.”

Section titled ““Can’t mix positional (?) and named (:param) placeholders. Use one style consistently.””

What happened: One call was given both positional values and named parameters: a params array holding plain values for ? alongside :name keys.

// Throws - positional value and named parameter in one call
DB::select('users', "status = ? AND city = :city", ['active', ':city' => 'Vancouver']);

Fix: Pick one style for the whole query:

DB::select('users', "status = :status AND city = :city", [
':status' => 'active',
':city' => 'Vancouver',
]);

“Missing value for ? parameter at position N”

Section titled ““Missing value for ? parameter at position N””

What happened: The template has more ? placeholders than values.

// Throws - 2 placeholders, 1 value
DB::select('users', "name = ? AND city = ?", 'Alice');

Fix: Pass one value per placeholder:

DB::select('users', "name = ? AND city = ?", 'Alice', 'Vancouver');

What happened: The template references a named placeholder that isn’t in the params array.

// Throws - :city not in the array
DB::select('users', "name = :name AND city = :city", [':name' => 'Alice']);

Fix: Add the missing key:

DB::select('users', "name = :name AND city = :city", [
':name' => 'Alice',
':city' => 'Vancouver',
]);

“Arrays not allowed with positional ? placeholders (ambiguous). Use named placeholder instead: ':paramName' => […]”

Section titled ““Arrays not allowed with positional ? placeholders (ambiguous). Use named placeholder instead: ':paramName' => […]””

What happened: An array value reached a ? placeholder. With named placeholders an array expands to a comma-separated list (for IN); with ? there is no way to tell whether you meant one value or a list, so it throws.

Fix: Use a named placeholder for the list:

DB::select('users', "id IN (:ids)", [':ids' => [1, 2, 3]]);
// SELECT * FROM `users` WHERE id IN (1,2,3)

“This method doesn’t support LIMIT or OFFSET”

Section titled ““This method doesn’t support LIMIT or OFFSET””

What happened: The template passed to selectOne(), queryOne(), or count() contains LIMIT or OFFSET; selectOne() and queryOne() append LIMIT 1 themselves, and count() returns a single number, so a caller-supplied LIMIT conflicts with both.

// Throws - selectOne() adds its own LIMIT 1
DB::selectOne('users', "status = ? LIMIT 5", 'active');

Fix: Use select() (or query()) when you want your own LIMIT, or query(...)->first() when you want one row from a query these methods reject:

DB::select('users', "status = ? LIMIT 5", 'active');
// SELECT * FROM `users` WHERE status = 'active' LIMIT 5
DB::query("SELECT * FROM ::users WHERE status = ? ORDER BY id DESC LIMIT 5", 'active')->first();

selectOne() and queryOne() also reject templates ending in a -- or # comment or a ;, because the appended LIMIT 1 would be swallowed by the comment (silent full-table scan) or fail to parse after the semicolon. The messages name the fix; query(...)->first() is the escape for those too.

“UPDATE requires a WHERE condition to prevent accidental bulk UPDATE”

Section titled ““UPDATE requires a WHERE condition to prevent accidental bulk UPDATE””

Also thrown as DELETE requires a WHERE condition to prevent accidental bulk DELETE.

What happened: update() or delete() was called with an empty WHERE, or a string starting with ORDER, LIMIT, OFFSET, or FOR (which would apply to every row).

// Throws - empty WHERE
DB::update('users', ['status' => 'inactive'], []);

Fix: Add a WHERE condition. To intentionally affect every row, pass the literal string "TRUE"; that is the designed escape:

DB::update('users', ['status' => 'inactive'], ['id' => 42]);
// UPDATE `users` SET `status` = 'inactive' WHERE `id` = 42
DB::update('users', ['status' => 'inactive'], "TRUE");
// UPDATE `users` SET `status` = 'inactive' WHERE TRUE

“Suspicious SET clause: only updating ‘num’. Did you reverse the arguments? Signature is: update($table, $values, $whereEtc)”

Section titled ““Suspicious SET clause: only updating ‘num’. Did you reverse the arguments? Signature is: update($table, $values, $whereEtc)””

What happened: The UPDATE sets exactly one column and it’s named num, id, or ID. That almost always means the values and WHERE arguments are swapped, an easy mix-up since both are arrays.

// Throws - arguments reversed
DB::update('users', ['num' => 5], ['status' => 'active']);

Fix: Values first, WHERE second:

DB::update('users', ['status' => 'active'], ['num' => 5]);
// UPDATE `users` SET `status` = 'active' WHERE `num` = 5

These keep working for now but will throw in a future version. Deprecated calls raise E_USER_DEPRECATED (with the calling file and line appended) quietly: PHP’s error log stays clean, but a set_error_handler callback receives them - CMS Builder records them in its Developer Log - and your IDE and static analysis flag deprecated calls in code.

“Positional values in an array are deprecated. Pass up to 3 values directly for ? placeholders, or use named placeholders: [':name' => $value]”

Section titled ““Positional values in an array are deprecated. Pass up to 3 values directly for ? placeholders, or use named placeholders: [':name' => $value]””

What happened: Values for ? placeholders were wrapped in an array.

// Deprecated - still runs, will throw in a future version
DB::select('users', "name = ? AND city = ?", ['Alice', 'Vancouver']);

Fix: Pass up to 3 values directly, or use named placeholders:

DB::select('users', "name = ? AND city = ?", 'Alice', 'Vancouver');

“Query has N positional (?) placeholder(s) but M values were passed. Unused positional values are deprecated and will throw in a future version. For IN() lists use a named placeholder: ':ids' => […]”

Section titled ““Query has N positional (?) placeholder(s) but M values were passed. Unused positional values are deprecated and will throw in a future version. For IN() lists use a named placeholder: ':ids' => […]””

What happened: More positional values were passed than the template has ? placeholders. The extras are silently unused, which almost always means a bug; the classic case is trying to expand a list into a single ? placeholder:

// Deprecated - runs as IN (1), values 2 and 3 are ignored
DB::select('users', "id IN (?)", 1, 2, 3);

Fix: For lists, use a named placeholder, which expands arrays:

DB::select('users', "id IN (:ids)", [':ids' => [1, 2, 3]]);
// SELECT * FROM `users` WHERE id IN (1,2,3)

“Couldn’t connect to server, check database server is running and connection settings are correct.”

Section titled ““Couldn’t connect to server, check database server is running and connection settings are correct.””

The message continues with the driver detail, for example MySQL Error(2002): Connection refused.

What happened: MySQL error 2002: nothing accepted the connection. The server isn’t running, or the hostname or port doesn’t point at it.

Fix: Confirm the MySQL server is running, then check hostname (and the port, if not the default 3306: 'hostname' => 'localhost:3307'). On WSL, see the next entry.

“‘localhost’ uses Unix sockets. To connect to Windows MySQL from WSL, use ‘127.0.0.1’ or ‘localhost:3306’ with WSL mirrored networking.”

Section titled ““‘localhost’ uses Unix sockets. To connect to Windows MySQL from WSL, use ‘127.0.0.1’ or ‘localhost:3306’ with WSL mirrored networking.””

The message ends with MySQL Error(2002): No such file or directory.

What happened: On Windows Subsystem for Linux, the hostname localhost connects through a Unix socket, which doesn’t exist when MySQL runs on the Windows host. ZenDB detects this combination and adds this hint to the error.

Fix: Either form in the message works; both force a TCP connection (localhost:3306 needs WSL mirrored networking to reach the Windows host):

DB::connect([
'hostname' => '127.0.0.1', // not 'localhost'
'username' => 'root',
'password' => '',
'database' => 'my_app',
]);

requireSSL defaults to false. If you enabled it and the server doesn’t support SSL, connecting fails with MySQL error 2006 and the message starts with Try disabling 'requireSSL' in database configuration. Do that, or enable SSL on the server.

“This program requires MySQL v5.7.32+ or compatible. This server has … vX.Y.Z installed.”

Section titled ““This program requires MySQL v5.7.32+ or compatible. This server has … vX.Y.Z installed.””

The message names the detected server product, so a MariaDB user sees This server has MariaDB v10.4.34 installed. ZenDB requires MySQL 5.7.32 or newer by default (a compatible server like MariaDB or Percona also passes, compared on its own version number). Upgrade the server, or lower the check with the versionRequired config setting if you’ve confirmed your older version works for your queries:

DB::connect([
'hostname' => '127.0.0.1',
'username' => 'root',
'password' => '',
'database' => 'my_app',
'versionRequired' => '5.7.0', // lower the minimum
]);

NULL Comparisons - WHERE Array vs Placeholder

Section titled “NULL Comparisons - WHERE Array vs Placeholder”

SQL’s = never matches NULL; that needs IS NULL. The WHERE array form converts null for you; a placeholder inserts a literal NULL and the comparison matches nothing:

// WHERE array: converts to IS NULL
DB::select('users', ['deletedAt' => null]);
// SELECT * FROM `users` WHERE `deletedAt` IS NULL
// Placeholder: literal NULL, "= NULL" matches no rows
DB::select('users', "deletedAt = ?", null);
// SELECT * FROM `users` WHERE deletedAt = NULL

With string templates, write deletedAt IS NULL yourself or use the array form.

Two related behaviors when an array expands into IN (...). First, null elements are skipped, because IN (1, NULL) would never match NULL rows anyway:

DB::select('users', "id IN (:ids)", [':ids' => [1, null, 3]]);
// SELECT * FROM `users` WHERE id IN (1,3)

Second, an empty array expands to SELECT 0 FROM (SELECT 0) empty_set WHERE 0, a subquery that returns zero rows: an empty set; IN of an empty set matches nothing and NOT IN of an empty set matches everything, so both directions do what an empty list should:

$wantedIds = []; // e.g., no checkboxes ticked
DB::select('users', "id IN (:ids)", [':ids' => $wantedIds]);
// SELECT * FROM `users` WHERE id IN (SELECT 0 FROM (SELECT 0) empty_set WHERE 0) - returns no rows
$excludeIds = []; // nothing to exclude
DB::select('users', "id NOT IN (:ids)", [':ids' => $excludeIds]);
// SELECT * FROM `users` WHERE id NOT IN (SELECT 0 FROM (SELECT 0) empty_set WHERE 0) - returns all rows

That subquery is built for IN and NOT IN. Put an array anywhere else and an empty one is a SQL syntax error naming empty_set:

$tags = []; // nothing selected
DB::queryOne("SELECT CONCAT_WS(:sep, :tags) AS csv", [':sep' => ',', ':tags' => $tags]);
// SQL syntax error near 'SELECT 0 FROM (SELECT 0) empty_set WHERE 0)'

Pick what an empty list should mean there and pass that instead:

DB::queryOne("SELECT CONCAT_WS(:sep, :tags) AS csv", [':sep' => ',', ':tags' => $tags ?: '']);
// SELECT CONCAT_WS(',', '') AS csv - returns an empty string

PHP booleans become the SQL keywords TRUE and FALSE (which MySQL stores as 1 and 0):

DB::insert('users', ['isAdmin' => true]);
// INSERT INTO `users` SET `isAdmin` = TRUE

Seeing the SQL That Just Ran - DB::$mysqli->lastQuery

Section titled “Seeing the SQL That Just Ran - DB::$mysqli->lastQuery”

ZenDB escapes values into the final SQL string before sending it, so the exact query is always available after any call:

DB::select('users', ['status' => 'active', 'city' => 'Vancouver']);
echo DB::$mysqli->lastQuery;
// SELECT * FROM `users` WHERE `status` = 'active' AND `city` = 'Vancouver'

This is the first thing to check when a query returns unexpected results: read the SQL that actually ran, then run it yourself in a MySQL client.

Results are collections, but print_r() shows their contents like plain arrays:

$users = DB::select('users', ['status' => 'active']);
print_r($users);

CMS Builder users: showme($users) prints the same thing with formatting and the calling line number.

For version-by-version behavior across MySQL and MariaDB (type conversions, edge cases, driver differences), see the CI-generated db-behavior-matrix.md.