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.
Error Messages
Section titled “Error Messages”“Quotes not allowed in template”
Section titled ““Quotes not allowed in template””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 templateDB::select('users', "name = 'John'");Fix: Move the value into a placeholder:
DB::select('users', "name = ?", 'John');// SELECT * FROM `users` WHERE name = 'John'“Standalone number in template”
Section titled ““Standalone number in template””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 templateDB::select('users', "age > 21");Fix: Use a placeholder:
DB::select('users', "age > ?", 21);// SELECT * FROM `users` WHERE age > 21Exception: 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 valuesDB::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 callDB::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 valueDB::select('users', "name = ? AND city = ?", 'Alice');Fix: Pass one value per placeholder:
DB::select('users', "name = ? AND city = ?", 'Alice', 'Vancouver');“Missing value for ':name' parameter”
Section titled ““Missing value for ':name' parameter””What happened: The template references a named placeholder that isn’t in the params array.
// Throws - :city not in the arrayDB::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 1DB::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 WHEREDB::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 reversedDB::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` = 5Deprecation Warnings
Section titled “Deprecation Warnings”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 versionDB::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 ignoredDB::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)Connection Problems
Section titled “Connection Problems”“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',]);SSL Connection Errors
Section titled “SSL Connection Errors”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]);Gotchas
Section titled “Gotchas”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 NULLDB::select('users', ['deletedAt' => null]);// SELECT * FROM `users` WHERE `deletedAt` IS NULL
// Placeholder: literal NULL, "= NULL" matches no rowsDB::select('users', "deletedAt = ?", null);// SELECT * FROM `users` WHERE deletedAt = NULLWith string templates, write deletedAt IS NULL yourself or use the array
form.
NULL and Empty Arrays in IN Lists
Section titled “NULL and Empty Arrays in IN Lists”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 tickedDB::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 excludeDB::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 rowsThat 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 selectedDB::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 stringBooleans Convert to TRUE and FALSE
Section titled “Booleans Convert to TRUE and FALSE”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` = TRUEDebugging
Section titled “Debugging”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.
Inspecting Results - print_r()
Section titled “Inspecting Results - print_r()”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.
Behavior Reference
Section titled “Behavior Reference”For version-by-version behavior across MySQL and MariaDB (type conversions, edge cases, driver differences), see the CI-generated db-behavior-matrix.md.