PHP PDO SQL Server Null result causes exception - php

I have a PDO class so I can easily switch between database types.
//*** Function: createQuery, Purpose: Create the query object ***
function createQuery($theQuery)
{
$theQuery = str_replace('`', "'", $theQuery);
$this->query = $theQuery;
$this->sth = $this->pdo->prepare($theQuery, array(PDO::ATTR_CURSOR
=> PDO::CURSOR_FWDONLY));
}
//*** Function: query, Purpose: Execute the PDO ***
function query($paramArray = [], $column = null)
{
try
{
if(!empty($paramArray))
foreach ($paramArray as $key => &$val)
{ $this->sth->bindParam($key, $val);}
$this->sth->execute();
}
catch(PDOException $e) { $this->error_catcher( $e->getMessage());}
if($column != null)
{
$column = ($this->sth->columnCount() < $column) ? 0 : $column;
return $this->sth->fetchAll( PDO::FETCH_COLUMN, $column );
}
else
return $this->sth->fetchAll();
}
and it all works spiffingly well. However, sometimes, if the result is NULL the code throws an exception
here is a typical query to build a menu :
SELECT TOP (100) PERCENT caption, id
FROM dbo.com_menuitems
WHERE (suite = 'chrimbotu') AND (parentid = 0) AND (enabled = 1) AND (id IN
(
SELECT parentid AS id FROM dbo.com_menuitems AS com_menuitems_1
WHERE (suite = 'chrimbotu') AND (perms <= - 1)GROUP BY parentid
HAVING (parentid > 0))
)
ORDER BY parentid, vieworder, caption
and i load the query (from another script) like this :
function getTopLevel($mySQL)
{
$this->_myConn->createQuery($mySQL);
$result = $this->_myConn->query() or die (DumpSQL($mySQL).__FUNCTION__ .' Query failed.');
$this->_topLevel = $result[0]['perms'];
}
If I run the query with no 'chrimbotu' in the database, SSMS says [NULL, NULL] my script carks and throws the helpful error "Query failed".
If I add 'chrimbotu' to the database, but give it a perms of 9, SSMS says [NULL, NULL] but my script is happy.
Both queries return a NULL, but is one more "NULLER" than the other?
FYI: 0 is the lowest permission, < 0 is "everybody" stuff

Related

PHP 7.4.18 update: Mysqlnd -- Calling stmt_store_result after fetch now throws an error

We have a bunch of websites using a shared MySQLi class.
PHP 7.4 was working with these and no errors were seen to be generated.
Now, PHP Bug #80837 has been fixed with release of PHP 7.4.18
The problem is that this fix means all our websites are throwing this fatal error and I can't see how to fix the error itself.
The Exception:
[06-May-2021 08:04:19 UTC] PHP Fatal error: Uncaught mysqli_sql_exception: Commands out of sync; you can't run this command
now in /usr/local/lib/php/databaseClass.php:169 Stack trace:
#0 /usr/local/lib/php/databaseClass.php(169): mysqli_stmt->store_result()
#1 /usr/local/lib/php/databaseClass.php(897): Database->fetchResult(Object(mysqli_stmt), true)
#2 /home/account/public_html/.../file.php(45): DatabaseObject->getSelect('SELECT (bad_log...', Array, true)
#3 /home/katemawdsley/public_html/start.php(10): include('/home/account...')
#4 {main} thrown in databaseClass.php on line 169
The Database Class 'fetchResult' method:
/***
* For use with MySQLi->dbiLink Object.
* Returns the result as an array of KEY=>VALUE pairs from the Database.
* #param $result mysqli_result object.
* #return array|mixed
***/
private function fetchResult($result)
{
if ($result instanceof \mysqli_stmt) {
$result->store_result(); // This is line 169
$variables = [];
$data = [];
$meta = $result->result_metadata();
//error_log(print_r(debug_backtrace(),true));
while ($field = $meta->fetch_field()) {
$variables[] = &$data[$field->name]; // pass by reference
}
\call_user_func_array([$result, 'bind_result'], $variables);
$i = 0;
while ($result->fetch()) {
$resultsArray[$i] = [];
foreach ($data as $k => $v) {
$resultsArray[$i][$k] = $v;
if ($this->numericIndexes) {
$resultsArray[$i][] = $v;
}
}
unset($k, $v);
$i++;
}
} elseif ($result instanceof \mysqli_result) {
$rowNumber = 0;
while ($row = $result->fetch_assoc()) {
$resultsArray[$rowNumber] = $row;
if ($this->numericIndexes) {
foreach ($row as $numerical) {
$resultsArray[$rowNumber][] = $numerical;
}
unset($numerical);
}
$rowNumber++;
}
$i = 0;
unset($row, $rowNumber);
}
return $resultsArray;
}
The Database Class 'fetchResult' method called from a 'select' method:
/***
* Function for retrieving SQL query data from the Database
* #param $sql string the SQL select query
* #param null $data string|array the data to check in the query with
* #return array|bool|mixed
***/
public function select($sql, $data = null){
/***
* Now prepare the SQL
***/
try {
$query = $this->databaseObject->prepare($sql);
/****
* $query is prepared here....
***/
...
$query->execute();
}
catch (\mysqli_sql_exception $ex) {
$this->exceptionalError($ex);
return false;
}
catch (\Exception $ex) {
$this->exceptionalError($ex);
return false;
}
if (\mb_strtoupper($reduce, "UTF-8") !== "NORETURN") {
if ($query->field_count > 0 ) {
$query->store_result();
}
$this->rowsFound[] = $query->num_rows;
$output = $this->fetchResult($query); // Call to fetch result method
if ($query->field_count > 0) {
$query->free_result();
}
$query->close();
unset($query);
return $output;
}
/***
* NORETURN value so no result to return.
***/
$successValue = $this->rowsAffected[] = $query->affected_rows;
$query->close();
unset($query);
return $successValue;
}
What is the correct work around for how to construct this fetchResult method in light of the update?
(As you can tell we were completely unaware of this previous issue and it didn't appear on any error logs so was a surprise)
You are calling store_result() twice. You can't do that. Once the results have been buffered in PHP, the connection line is free - there are no more results pending to be fetched. Calling store_result() the second time will throw "Out of sync" error.
You call it first here:
if (\mb_strtoupper($reduce, "UTF-8") !== "NORETURN") {
if ($query->field_count > 0 ) {
$query->store_result();
}
and then here
private function fetchResult($result)
{
if ($result instanceof \mysqli_stmt) {
$result->store_result(); // This is line 169
Remove one usage and you should be fine.
I would recommend working with mysqli_result instead. When you call get_result it will buffer the results and give you a familiar object that you can work with. This could simplify your abstraction class significantly.
Here is how I would refactor this class:
public function select($sql, $data = null)
{
/***
* Now prepare the SQL
***/
try {
$query = $this->databaseObject->prepare($sql);
/****
* $query is prepared here....
***/
$query->execute();
} catch (\mysqli_sql_exception $ex) {
$this->exceptionalError($ex);
return false;
} catch (\Exception $ex) {
$this->exceptionalError($ex);
return false;
}
$result = $query->get_result();
if (\mb_strtoupper($reduce, "UTF-8") !== "NORETURN") {
$this->rowsFound[] = $result->num_rows;
if ($result) {
return $this->fetchResult($result); // Call to fetch result method
}
}
/***
* NORETURN value so no result to return.
***/
$successValue = $this->rowsAffected[] = $query->affected_rows;
return $successValue;
}
private function fetchResult(mysqli_result $result)
{
return $result->fetch_all($this->numericIndexes ? MYSQLI_BOTH : MYSQLI_ASSOC);
}

PHP Improve performance to execute multiple queries while reading a file with thousand lines

I'm trying to build a script where I need to read a txt file and execute some process with the lines on the file. For example, I need to check if the ID exists, if the information has updated, if yes, then update the current table, if no, then insert a new row on another temporary table to be manually checked later.
These files may contain more than 20,30 thousand lines.
When I just read the file and print some dummie content from the lines, it takes up to 40-50ms. However, when I need to connect to the database to do all those verifications, it stops before the end due to the timeout.
This is what I'm doing so far:
$handle = fopen($path, "r") or die("Couldn't get handle");
if ($handle) {
while (!feof($handle)) {
$buffer = fgets($handle, 4096);
$segment = explode('|', $buffer);
if ( strlen($segment[0]) > 6 ) {
$param = [':code' => intval($segment[0])];
$codeObj = Sql::exec("SELECT value FROM product WHERE code = :code", $param);
if ( !$codeObj ) {
$param = [
':code' => $segment[0],
':name' => $segment[1],
':value' => $segment[2],
];
Sql::exec("INSERT INTO product_tmp (code, name, value) VALUES (:code, :name, :value)", $param);
} else {
if ( $codeObj->value !== $segment[2] ) {
$param = [
':code' => $segment[0],
':value' => $segment[2],
];
Sql::exec("UPDATE product SET value = :value WHERE code = :code", $param);
}
}
}
}
fclose($handle);
}
And this is my Sql Class to connect with PDO and execute the query:
public static function exec($sql, $param = null) {
try {
$conn = new PDO('mysql:charset=utf8mb4;host= '....'); // I've just deleted the information to connect to the database (password, user, etc.)
$q = $conn->prepare($sql);
if ( isset($param) ) {
foreach ($param as $key => $value) {
$$key = $value;
$q->bindParam($key, $$key);
}
}
$q->execute();
$response = $q->fetchAll();
if ( count($response) ) return $response;
return false;
} catch(PDOException $e) {
return 'ERROR: ' . $e->getMessage();
}
}
As you can see, each query I do through Sql::exec(), is openning a new connection. I don't know if this may be the cause of such a delay on the process, because when I don't do any Sql query, the script run within ms.
Or what other part of the code may be causing this problem?
First of all, make your function like this,
to avoid multiple connects and also o get rid of useless code.
public static function getPDO() {
if (!static::$conn) {
static::$conn = new PDO('mysql:charset=utf8mb4;host= ....');
}
return static::$conn;
}
public static function exec($sql, $param = null) {
$q = static::getPDO()->prepare($sql);
$q->execute($param);
return $q;
}
then create unique index for the code field
then use a single INSERT ... ON DUPLICATE KEY UPDATE query instead of your thrree queries
you may also want to wrap your inserts in a transaction, it may speed up the inserts up to 70 times.

SilverStripe SQL boolean value syntax error

I trying to return an sql query with the code below. For some reason the boolean values I'm trying to filter by ('ShowOnHomePage' and 'Published') are throwing the following syntax error due to the question mark placeholder.
You have an error in your SQL syntax; check the manual that corresponds to >your MySQL server version for the right syntax to use near '?) AND ("DummyContentOne"."ShowOnHomePage" = ?)) "DummyContentOne" UNION ALL (S' at line 5
public function getSiteContent() {
if($this->_contentList!=false) {
return $this->_contentList;
}
$request = (Controller::has_curr() ? Controller::curr()->getRequest() : null);
$start = round(($request ? intval($request->getVar('start')) : 0));
$DummyContentOne = DummyContentOne::get()->filter('Published', true)->filter('ShowOnHomePage', true)->dataQuery()->query()->setOrderBy(null);
$DummyContentTwo = DummyContentTwo::get()->filter('Published', true)->filter('ShowOnHomePage', true)->dataQuery()->query()->setOrderBy(null);
$this->fixQueryColumns($DummyContentOne, $DummyContentTwo);
$db = DB::query(
'SELECT * '.
'FROM ('.$DummyContentOne->sql().') "DummyContentOne" '.
'UNION ALL ('.$DummyContentTwo->sql().') '.
'ORDER BY "Date" DESC, "Created" DESC '.
'LIMIT '.$this->config()->item_count.' OFFSET '.$start
);
// Merge into an array list
$list = new ArrayList();
foreach($db as $row) {
$className=$row['ClassName'];
$list->push(new $className($row, false, DataModel::inst()));
}
// Calculate the total number of items
$totalItems = $DummyContentOne->count() + $DummyContentTwo->count();
// Convert to a paginated list
$pagedList=ActionPaginatedList::create($list, $this->Link('more-content'), $request)
->setPageLength($this->config()->item_count)
->setPageStart($start)
->setTotalItems($totalItems)
->setLimitItems(false);
return $pagedList;
}
...
private function fixQueryColumns() {
$queries = func_get_args();
$columns = array();
// Debug::show($queries);
// Get all of the columns from each query
foreach($queries as $query) {
$columns = array_merge($columns, array_keys($query->getSelect()));
}
...
Any ideas what would cause this?
I'm using SS3.2
Thanks

PHP / MySQLi on Windows: inserts into a MySQL innoDB Table increases the AUTO_INCREMENT column by 2

When I am doing a simple insert via the PHP mysqli API with prepared statements on Windows within a PHP process, the defined AUTO_INCREMENT column is increased by 2 instead of 1:
INSERT INTO `table` (`name`) VALUES (?)
It get increased by 1 when doing multiple inserts (one by one in separate transactions) within one PHP process.
It always get increased by 1 when I use the same SQL query via phpmyadmin.
There are no other INSERT or UPDATE statements before or after the mentioned INSERT. Only a SHOW and some SELECT statements before.
I cannot find the cause for this problem. What can be the causes for such a behaviour?
Here the main code parts:
<?php
class DB
{
private function __construct($host, $username, $password, $schema, $port, $socket)
{
if(is_null(self::$DB))
{
self::$DB = new \MySQLi((string) $host, (string) $username, (string) $password, (string) $schema, (int) $port, (string) $socket);
self::$DB->set_charset('utf8');
}
}
// [...]
public function __destruct()
{
if(!is_null(self::$DB))
self::$DB->close();
}
// [...]
public static function connect($host = '', $username = '', $password = '', $schema = '', $port = 0, $socket = '')
{
if(is_null(self::$instance))
{
$MD = new \MySQLi_driver();
$MD->report_mode = MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT;
// [...]
self::$instance = new self($host, $username, $password, $schema, $port, $socket);
}
return self::$instance;
}
// [...]
public static function __callStatic($name, $args)
{
self::connect();
switch(true)
{
case in_array($name, array('insert', 'select', 'update', 'delete', 'show', 'describe', 'explain')):
$query = isset($args[0]) ? (string) $args[0] : '';
$vals = isset($args[1]) ? $args[1] : array();
$select = isset($args[2]) ? trim((string) $args[2]) : 'array';
$empty = isset($args[3]) ? $args[3] : array();
$types = isset($args[4]) ? trim((string) $args[4]) : '';
return self::dml(in_array($name, array('show', 'describe', 'explain')) ? 'select' : $name, $query, $vals, $select, $empty, $types);
break;
}
//[...]
}
// [...]
public static function dml($type, $query, $vals = array(), $select = 'array', $empty = array(), $types = '')
{
// [...]
if(!empty($vals) || mb_strpos($query,'?') !== false)
{
if(!$stmt = self::$DB->prepare($query))
throw new DBException('Failed to prepare statement '.htmlspecialchars($query).PHP_EOL.self::$DB->error.' ('.self::$DB->sqlstate.').');
$args = array();
if(empty($types))
{
foreach($vals as &$val)
{
$t = gettype($val);
if($t == 'string' || $t == 'NULL')
$types.= 's';
elseif($t == 'integer' || $t == 'boolean')
$types.= 'i';
elseif($t == 'double' || $t == 'float')
$types.= 'd';
else
throw new DBException('Its not possible to automatically assign a value of type '.$t.'. Please specify the corresponding types manually.');
$args[] = $val;
}
}
else
{
foreach($vals as &$val)
$args[] = $val;
}
array_unshift($args, $types);
$RC = new \ReflectionClass($stmt);
$RM = $RC->getMethod('bind_param');
if(!$RM->invokeArgs($stmt, $args))
throw new DBException('Failed to bind params.'.PHP_EOL.self::$DB->error.' ('.self::$DB->sqlstate.').');
if(!$stmt->execute())
throw new DBException('Failed to execute Statement.'.PHP_EOL.self::$DB->error.' ('.self::$DB->sqlstate.').');
if($type == 'select')
{
// [...]
}
else
{
$return = $type == 'insert' && self::$DB->insert_id > 0 ? self::$DB->insert_id : self::$DB->affected_rows;
$stmt->close();
return $return;
}
}
else
{
// [...]
}
}
}
?>
AND:
echo DB::insert("INSERT INTO `table` (`name`) VALUES (?)", ["test"]);
The Query-Log shows 2 inserts. This causes the additional increment. But, when i put an echo into the corresponding DB::dml()-method, its only outputted once, thus called once. The mysql-query-log:
151026 12:54:49 3 Connect XXX#localhost on XXX
3 Query SET NAMES utf8
3 Query SELECT DATABASE() AS `schema`
3 Query SHOW GRANTS FOR CURRENT_USER
3 Prepare INSERT INTO `table` (`name`) VALUES (?)
3 Execute INSERT INTO `table` (`name`) VALUES ('testinsert22')
3 Close stmt
3 Quit
4 Connect XXX#localhost on XXX
4 Query SET NAMES utf8
4 Query SELECT DATABASE() AS `schema`
4 Query SHOW GRANTS FOR CURRENT_USER
4 Prepare INSERT INTO `table` (`name`) VALUES (?)
4 Execute INSERT INTO `table` (`name`) VALUES ('testinsert22')
4 Close stmt
4 Quit
Ok. I finally got it.
The problem is: I redirect all requests to my index.php and do all the rest with PHP. I tested some classes behaviour with simple outputs. One of the tests lead to the problem above.
I checked the apache access log and noticed repeated favicon.ico requests. These requests - automatically produced by major browsers (see also How to prevent favicon.ico requests?) - also got to the index.php - and looks like some redirect to the browser?
normally this is all handled in my application framework, but i disabled it while i tested.

Undefined offset: 0 in first PHP app

I'm currently building my first PHP application. I want to read in bus times from a csv file and give back ti users the next bus from their residence to our university. It is my first try with PHP, so be gentle:
<?php
// configuration
require("../includes/config.php");
if (!empty($_SESSION["id"]))
{
//lookup database entries for house
$users = query("SELECT * FROM users WHERE id = ?", $_SESSION["id"]);
if ($users === false)
{
apologize("Sorry, there was an error");
}
//lookup database entries for house
$residences = query("SELECT * FROM residences WHERE id = ?", $users[0]["residence"]);
if ($residences === false)
{
apologize("Sorry, there was an error");
}
//if user specified a residence in his profile
if($residences[0]["id"] != 0)
{
$times = array();
//if there is no bus today, in this case sat and sun
if(date( "w", $timestamp) == 0 || date( "w", $timestamp) == 6)
{
$times[0] = "There is no bus today";
}
//load the busplan for his residence
else
{
//open file and load in array if time is higher than date("His");
$timesList = file_get_contents($users[0]["residence"] . ".csv");
$nextbuses = explode(',', $timesList);
$hoursMins = date("Gi");
$num = 0;
for($i = 0; $i < count($nextbuses); $i++)
{
if($hoursMins < $nextbuses[$i])
{
$times[$num] = $nextbuses[$i];
$num++;
}
}
}
render("shuttle_show.php", ["title" => "Next Shuttle from your residence.", "times" => $times]);
}
}
This uses the function query:
function query(/* $sql [, ... ] */)
{
// SQL statement
$sql = func_get_arg(0);
// parameters, if any
$parameters = array_slice(func_get_args(), 1);
// try to connect to database
static $handle;
if (!isset($handle))
{
try
{
// connect to database
$handle = new PDO("mysql:dbname=" . DATABASE . ";host=" . SERVER, USERNAME, PASSWORD);
// ensure that PDO::prepare returns false when passed invalid SQL
$handle->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
}
catch (Exception $e)
{
// trigger (big, orange) error
trigger_error($e->getMessage(), E_USER_ERROR);
exit;
}
}
// prepare SQL statement
$statement = $handle->prepare($sql);
if ($statement === false)
{
// trigger (big, orange) error
trigger_error($handle->errorInfo()[2], E_USER_ERROR);
exit;
}
// execute SQL statement
$results = $statement->execute($parameters);
// return result set's rows, if any
if ($results !== false)
{
return $statement->fetchAll(PDO::FETCH_ASSOC);
}
else
{
return false;
}
}
the other functions it uses are not relevant I guess. Now I cant seem to find why this keeps producing:
Notice: Undefined offset: 0 in /Applications/MAMP/htdocs/html/shuttle.php on line 16
Notice: Undefined offset: 0 in /Applications/MAMP/htdocs/html/shuttle.php on line 22
The relevant lines are
$residences = query("SELECT * FROM residences WHERE id = ?", $users[0]["residence"]);
and
if($residences[0]["id"] != 0)
Would appreciate some help! :)
Edit:
I transferred the same file to my linux system and without any changes I get a vardump. If I use the same vardump on MAMP on my Mac the array is empty. Now I get for:
var_dump($users);
array (size=1)
0 =>
array (size=5)
'id' => int 12
'username' => string 'frechdaxx' (length=9)
'mail' => string '*************#gmail.com' (length=23)
'hash' => string '$1$qr5axj4C$BET5zZGJza2DcHI8eD8fV0' (length=34)
'residence' => int 9573
Why is this a problem at all? the function query worked with the exact same syntax before when I accessed the user table and as we can see it gives back all other values correctly.
Why the difference between my environments? The array is empty on my MAMP server, but works on Linux. However, other code examples with the query function work perfectly on both environments
Why the big int of 9573? The value in the table is 2.
This is simply that $users[0]["residence"] doesn't exists (is not set). You should check it before attempting to use it. As you can see, it's only a notice, not a error. This means that in some case scenarios, the variable can be unset and PHP won't complain THAT much, but this is definitely not good as a general rule.
I would change it to:
$users = query("SELECT * FROM users WHERE id = ?", $_SESSION["id"]);
if (empty($users[0]["residence"]))
{
apologize("Sorry, there was an error");
}
Furthermore, that only "fixes" it. If you want to go to the root of the problem, it's in the query() function that is called in $users = query("SELECT * FROM users WHERE id = ?", $_SESSION["id"]);:
// execute SQL statement
$results = $statement->execute($parameters);
// return result set's rows, if any
if ($results !== false)
{
$fetched = $statement->fetchAll(PDO::FETCH_ASSOC);
// Check that ther was actually something fetched
if(!empty($fetched))
{
return $fetched;
}
}
/* You don't need to return false, null is the default returned value.
* If you *want* it, then delete the else. */
}
From the documentation, fetchall returns an empty array if there's nothing, so the function might return an empty array.
$users = query("SELECT * FROM users WHERE id = ?", $_SESSION["id"]);
if ($users === false) { }
You are strictly checking if query returned false, that only occurs when something was wrong with query or parameters. That does not check if any result was returned, and you are referring to the first returned record ($users[0]["residence"]) that is not present ($users is an empty array).
Since user id comes from $_SESSION["id"] it should be present and available in database, but there is possibility that somewhere in your code you are overwriting it with other id.

Categories