Dynamic query with PDO and FIND_IN_SET - php

I'm using PDO to build my queries which rely on FIND_IN_SET to select certain rows. The elements of the set are passed via POST to the server and the query constructed like this:
SELECT * FROM table WHERE
(FIND_IN_SET('param1', column) > 0 OR FIND_IN_SET('param2', column) > 0)
That's, in a basic way, how the queries are constructed. Because the number of parameters to find in the set is dynamic, the FIND_IN_SET part of the query is made dynamically:
$q = implode("', column)>0 OR FIND_IN_SET('", $array);
And then applied to the query, which looks like this:
SELECT * FROM table WHERE
AND (FIND_IN_SET(:q, e.event_type)>0);
And the query eventually executed as:
$countResult->execute(array(':q' => $q);
Note that $q may take the form of:
"param1', column)>0 OR FIND_IN_SET('param2', column)>0 OR FIND_IN_SET('param3"
If I don't use PDO, the query executes correctly, but when using PDO, the query works when there is only one element in the array, but doesn't return any results when the array contains more than one element.
Is the execute method doing something to the parameter possibly?

You can use array_map() to create a FIND_IN_SET() expression from each array element, then implode() the results with ' OR ' as glue:
function fis($c) {
return function($k) {return "FIND_IN_SET(:$k, $c)";}
}
$arr = array(
'p1' => 'param1',
'p2' => 'param2'
);
$qry = $dbh->prepare('
SELECT *
FROM table
WHERE ('.implode(' OR ', array_map(fis('column'), array_keys($arr))).')
');
$qry->execute($arr);

Related

How to correctly use multiple SqlSelect set/add where functions

I'm piecing together an SQL query using SilverStripes SQLSelect class with some optional (frontend) filters.
When attempting to use addWhere() with parameters the query has no results. E.g:
$sql->useConjunction();
$sql->addWhere(['SiteTree.ClassName' => 'ResourcePage']);
if (self::$Filters['Tags'])
{
$sql->addLeftJoin('ResourcePage_Tags', 'ResourcePage_Tags.ResourcePageID = SiteTree.ID');
$sql->addWhere(['ResourcePage_Tags.ResourceTagID IN (?)' => implode(',', self::$Filters['Tags'])]);
}
if (self::$Filters['Types'])
{
$sql->addLeftJoin('ResourcePage_Types', 'ResourcePage_Types.ResourcePageID = SiteTree.ID');
$sql->addWhere(['ResourcePage_Types.ResourceTypeID IN (?)' => implode(',', self::$Filters['Types'])]);
}
If I treat it as a string, I do get the results (like I expect testing with manual SQL). E.g:
$sql->useConjunction();
$sql->addWhere("SiteTree.ClassName = 'ResourcePage'");
if (self::$Filters['Tags'])
{
$sql->addLeftJoin('ResourcePage_Tags', 'ResourcePage_Tags.ResourcePageID = SiteTree.ID');
$sql->addWhere('ResourcePage_Tags.ResourceTagID IN ('.implode(',', self::$Filters['Tags']).')');
}
if (self::$Filters['Types'])
{
$sql->addLeftJoin('ResourcePage_Types', 'ResourcePage_Types.ResourcePageID = SiteTree.ID');
$sql->addWhere('ResourcePage_Types.ResourceTypeID IN ('.implode(',', self::$Filters['Types']).')');
}
The generated (simplified) query found using $sql->__toString() is:
SELECT
DISTINCT SiteTree.*
FROM
SiteTree
LEFT JOIN
"ResourcePage_Tags" ON ResourcePage_Tags.ResourcePageID = SiteTree.ID
LEFT JOIN
"ResourcePage_Types" ON ResourcePage_Types.ResourcePageID = SiteTree.ID
WHERE
(SiteTree.ClassName = ?)
AND (ResourcePage_Tags.ResourceTagID IN (?))
AND (ResourcePage_Types.ResourceTypeID IN (?))
The parameters portion of the above output is an array:
'ResourcePage',
1 => '38'
2 => '5,4'
The omitted parts of the query is some MATCH AGAINST and LIMIT ORDER BY clauses that are always there.
What have I done wrong to get two different behaviours between string and the parameterised version?
The first example you had which was using in (?) and then passing an array of values will not execute correctly because the assumption is that ? is one value when using the prepare. Which means you are now querying for one ResourceTypeID that is equal to array_value1,array_value2. You therefore need to have a placeholder (?) per item in the self::$Filters['Tags'] array.
The framework does have a helper method to achieve this like so:
$placeholders = DB::placeholders(self::$Filters['Tags'])
$query->addWhere([
"ResourcePage_Types.ResourceTypeID IN ($placeholders)" => self::$Filters['Tags']
]);

Dynamically creating OR conditions by passing an array to a query in MySQL PHP

I am trying to create OR condition dynamically using an array. Given an array, of course names $courses = array('Eng, 'Deu', 'Bio', 'Chemi') I want to have a SQL query that uses the values of the array in its AND clause with OR conditions like:
SELECT *
FROM classe
/* The OR conditions should be created in AND clause using array */
WHERE class = 'EFG' AND (course = 'Eng' OR course = 'Deu' OR course = 'Bio')
I trying to do it in PHP MySQL.
Any help would be really appreciated.
Thanks in Advance.
Instead of so many OR clauses, you can simply use IN(..):
SELECT *
FROM classe
WHERE class = 'EFG' AND course IN ('Eng' ,'Deu', 'Bio')
In the PHP code, you can use implode() function to convert the array into a comma separated string, and use it in the query string generation.
The IN clause will be easier to use than ORs. If you are using PDO you can take advantage of its execute binding and build the placeholders dynamically then just pass your array to it.
$courses = array('Eng', 'Deu', 'Bio', 'Chemi');
$placeholders = rtrim(str_repeat('?, ', count($courses)), ', ');
$query = "select * from table WHERE class = 'EFG' AND course in ({$placeholders})";
$stmt = $pdo->prepare($query);
$stmt->execute($courses);
Demo: https://3v4l.org/jcFSv (PDO bit non functional)

Sanitise query in SQL and PHP using in_array()

Using MySQL and PHP, a typical (PDO) query looks like this:
// prepare the query
$q = $DB->prepare("SELECT * FROM table_name WHERE property = :value");
// run the query
$q->execute(array(':value'=>$value));
This is safe from SQL injection, as the property value is treated separately to the query.
However if I want to use the same code to write a query that might retrieve a different field or different grouping, you cannot use the prepare/execute method alone, as you cannot use PDO parameters for fields (see here).
Can you simply use in_array() to check a field name, like this:
// return false if the field is not recognised
if(! in_array($field_name, array('field1','field2','field3')) return false
// run the query
$q = $DB->query("SELECT * FROM table_name ORDER BY " . $field_name);
Is there a safer / quicker way?
Already seems quite fast and secure. Maybe add backticks around the field name in the query.
To speed it up slightly you can use an assoc array and just check if the index exists instead of searching the contents of an array.
$fields = array('field1' => null, 'field2' => null, 'field3' => null);
if (!array_key_exists($field_name, $fields)) return false;
furthermore isset is faster than array_key_exists
if (!isset($fields[$field_name])) return false;
function benchmarks

PDO params not passed but sprintf is

Unless I am missing something very obvious, I would expect the values of $data1 and $data2 to be the same?? But for some reason when I run this scenario twice (its run once each function call so I'm calling the function twice) it produces different results.
Call 1: PDO = Blank, Sprintf = 3 rows returned
Call 2: PDO = 1 row, Sprintf = 4 rows (which includes the PDO row)
Can someone tell me what I'm missing or why on earth these might return different results?
$sql = "SELECT smacc.account as Smid,sappr.*,CONCAT('$domain/',filepath,new_filename) as Image
FROM `{$dp}table`.`territories` pt
JOIN `{$dp}table`.`approvals` sappr ON pt.approvalID = sappr.ID
JOIN `{$dp}table`.`sm_accounts` smacc ON pt.ID = smacc.posted_territory_id
LEFT JOIN `{$dp}table`.`uploaded_images` upimg ON pt.imageID = upimg.ID
WHERE postID = %s AND countryID = %s AND smacc.account IN (%s) AND languageID = %s";
echo sprintf($sql,$postID,$countryID,implode(',',$accs),$langID);
$qry1 = $db->prepare(str_replace('%s','?',$sql));
$qry1->execute(array($postID,$countryID,implode(',',$accs),$langID));
$data1 = $qry1->fetchAll();
print'<pre><h1>PDO</h1>';print_r($data1);print'</pre>';
$qry2 = $db->query(sprintf($sql,$postID,$countryID,implode(',',$accs),$langID));
$data2 = $qry2->fetchAll();
print'<pre><h1>Sprintf</h1>';print_r($data2);print'</pre><hr />';
The root of the problem is the implode(',',$accs) function.
While you are using sprintf() it will generate a coma separated list and that list will be injected into the query string.
The result will be something like this:
smacc.account IN (1,2,3,4,5)
When you are binding the same list with PDO, it handles it as one value (a string: '1,2,3,4,5'). The "result" will be something like this:
smacc.account IN ('1,2,3,4,5')
Note the apostrophes! -> The queries are not identical.
In short, when you are using PDO and binding parameters, you have to bind each value individually (you can not pass lists as a string).
You can generate the query based on the input array like this:
$query = ... 'IN (?' . str_repeat(', ?', count($accs)-1) . ')' ...
// or
$query = ... 'IN (' . substr(str_repeat('?,', count($accs)), 0, -1) . ')'
This will add a bindable parameter position for each input value in the array. Now you can bind the parameters individually.
$params = array_merge(array($postID, $countryID), $accs, array($langID));
$qry1->execute($params);
Yes as Kris has mentioned the issue with this is the IN part of the query. Example 5 on the following link helps fix this: http://php.net/manual/en/pdostatement.execute.php. I tried using bindParam() but that didn't seem to work so will use Example 5 instead.

Change the return format on PHP's PDO Select

I am writing my own PDO wrapper to make my life easier and a fair amount safer.
A standard query looks like:
$user = $db->select('users')
->eq('twitter_id', $twitter_id)
->limit(1)
->prepare()
->exec();
Generates this query:
SELECT * FROM users WHERE twitter_id = :twitter_id LIMIT 1
This works perfectly fine as I, currently, want it. Where I am running into a problem is when I have a query to return multiple rows.
My apps stores some dynamic settings that I want to grab and use in one pass and I can do that by running a query like:
$share_datas = $db->select('settings', 'setting, value')
->prepare()
->exec();
Which generates:
SELECT setting, value FROM settings
Which returns:
Array
(
[0] => Array
(
[setting] => since_id
[value] => 17124357332
)
[1] => Array
(
[setting] => last_dm
[value] => 1271237111
)
)
The function prepare() puts the pieces together for the query and the function exec() binds the params and returns the array.
function exec()
{
// echo 'vars: <pre>'.print_r($this->sql_vars, true).'</pre>';
$stmt = $this->dbh->prepare($this->sql_last_query);
foreach($this->sql_vars as $key => $val)
{
if('date_time' === $key) continue;
$bind = $stmt->bindValue($key, $val);
}
$stmt->execute();
$this->sql_vars = array();
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
Now to the question: Is there a way that I can change exec() or the query itself so that it can return an array that returns like below and avoids any additional loops?
Array
(
[since_id] => 17124357332
[last_dm] => 1271237111
)
No problem with some simple array functions.
$in = $db->exec();
$out = array();
foreach( $in as $row )
$out[ $row['setting'] ] = $row['value'];
If you need a more general function, you'll have to describe the transformation clearer.
The answer is likely going to be either:
Creating multiple versions of your exec method with different return behavior, or
Having exec simply perform the execution and store the statement handle, then have fetching the data be a separate method.
I've found the following convenience methods handy, in addition to your current array of hashes:
Query "one": The first column in the first row as a scalar (for things like SELECT COUNT(*))
Query "list": The first column of all rows as an indexed array (for things like SELECT id FROM ...))
Query "pairs": The first two columns of all rows as a hash (for your current problem)
Query "insert id": The last generated row id as a scalar (autoincrement in MySQL, sequence in Postgres, etc)
These are all occasionally convenient things that PDO (and most other database adapters) simply don't have built-in flags to handle.

Categories