PHP mysqli Prepared Statement with anonymous join - php

I am currently frustrated with preparing one statement - culprit is not preparing it properly, but staying inline with my current function to bin a query with a variable number of parameters. It works in all cases, but this query gives me an unsolveable problem (in one single query) for mysql:
SELECT studios.name,
studios.phone,
locations.zip_code,
locations.location_name,
addresses.street_name,
addresses.stree_nr,
persons.first_name,
persons.last_name,
p.distance_unit
* DEGREES(ACOS(COS(RADIANS(p.latpoint))
* COS(RADIANS(addresses.geo_lat))
* COS(RADIANS(p.longpoint) - RADIANS(addresses.geo_long ))
+ SIN(RADIANS(p.latpoint))
* SIN(RADIANS(addresses.geo_lat)))) AS distance
FROM studios
JOIN ( /* these are the query parameters */
SELECT ? AS latpoint, ? AS longpoint,
? AS radius, 111.045 AS distance_unit
) AS p ON 1=1
CROSS JOIN addresses
ON studios.address = addresses.id
CROSS JOIN locations
ON addresses.location = locations.id
CROSS JOIN persons
ON studios.owner = persons.id
CROSS JOIN studio_types
ON studios.studio_type = studio_types.id
WHERE addresses.geo_lat
BETWEEN p.latpoint - (p.radius / p.distance_unit)
AND p.latpoint + (p.radius / p.distance_unit)
AND addresses.geo_long
BETWEEN p.longpoint - (p.radius / (p.distance_unit * COS(RADIANS(p.latpoint))))
AND p.longpoint + (p.radius / (p.distance_unit * COS(RADIANS(p.latpoint))))
ORDER BY distance
LIMIT 25
The query itself is fully functional - here is a gist of it:
SELECT *
FROM studios
JOIN ( SELECT ? AS latpoint, ? AS longpoint,
? AS radius, 111.045 AS distance_unit
) AS p ON 1=1
now binding it is pretty straight forward:
$query = $this->conn->prepare($query);
if(!$query) throw new Exception($this->conn->error);
$query->bind_params("ddd", $val1, $val2, $val2);
$query->execute();
$query->get_result();
The error that occures by preparing already happens in the first line: Unknown column 'p.latpoint' in 'field list'
Now this is a huge problem. Going around that would probably work with preparing statements with SET #variable = ? - but thats not a preferred situation, as it would break up my current scheme of dispatching my requests.
Is there any oversight or is this simply not possible, due to undefined joins while preparing?
Edit: third method would involve replacing the join and placing values - this would make a very confusuing amount of bind parameters - which would be hard to maintain later.

So, I finally come around for anyone who seeks the same answer like me and I gotta extend where I left with it:
Imagine a class that does all the heavy lifting for you, minimizing issues provides you a simple interface between you and your database. There is a method that takes a statement string, type binding and like this:
protected function __dispatch($query, $types, $arguments) {
error_reporting(0);
$query = $this->conn->prepare($query);
if(!$query) throw new Exception($this->conn->error);
if($types !== "") call_user_func_array(array(&$query, 'bind_param'),
array_merge(array($types), $arguments));
$query->execute();
return $query->get_result();
}
Since placeholders cannot possibly any identifiers (thanks Jon), I can however bind SQL variables just fine. Then I am going to execute something like this:
SELECT *
FROM studios
JOIN ( SELECT #lat AS latpoint, #lon AS longpoint,
#dist AS radius, 111.045 AS distance_unit
) AS p ON 1=1
This gives the possibility to bind variables before - and then use it in the statement, just like this:
public function search_in_range($lat, $long, $distance) {
$this->__dispatch("SET #lat = ?", "d", array(&$lat));
$this->__dispatch("SET #lon = ?", "d", array(&$long));
$this->__dispatch("SET #dist = ?", "d", array(&$distance));
// And finally:
return $this->__dispatch($query, $types, [])->fetch_all();
}
This does the trick to create to have a very simple join with an anonymous set of data and use it in a more complicated statement later on.

Related

PHP Symfony DQL - Too few parameters: the query defines 1 parameters but you only bound 0

i am trying to put together a SELECT in symfony repository project but for some reason getting error message in the subject. Been working on it for a few days but cannot find a solution with different variations of the code. This is the code
$qb3 = $this->getEntityManager()->createQueryBuilder();
$qb3-> select('check.userid')
-> from('App\Entity\YY_Table', 'check')
-> where('DATE_FORMAT(now(), \'%e-%b-%Y\') - DATE_FORMAT(check.datecreated, \'%e-%b-%Y\') <=360');
$qb2 = $this->getEntityManager()->createQueryBuilder();
$qb2-> select('((:numemails1 / COUNT(subs.id)) * 10)')
-> from('App\Entity\XX_Table', 'subs')
-> where($qb2->expr()->notIn('subs.id', $qb3->getDQL()))
-> setParameter('numemails1', $numemails);
$rand_num = $qb2->getQuery()->getResult();
$qb1 = $this->getEntityManager()->createQueryBuilder();
$qb1-> select('subss.id')
-> from('App\Entity\XX_Table', 'subss')
-> where('RAND() < :sqbresult')
-> orderBy('RAND()')
-> setMaxResults($numemails)
-> setParameter('sqbresult', $rand_num);
/*primary query to select users for future campaigns*/
$qb = $this->getEntityManager()->createQueryBuilder();
$qb -> select('s')
-> from('App\Entity\XX_Table', 's')
-> where($qb->expr()->In('s.id', $qb1->getDQL()));
//-> where($expr->not($expr->exists($qb1->getDQL())));
return $qb ->getQuery() ->getResult();
and I am trying to get the alternative of .sql code below to select random entities from DB that passed some basic criteria
SELECT
g.* FROM XX_table g
JOIN
(SELECT
id
FROM
XX_table
WHERE
RAND() < (SELECT
((60000 / COUNT(*)) * 10) as rand_num
FROM
XX_table
WHERE
id NOT IN (SELECT userID as id FROM YY_table emst WHERE CURDATE() - emst.datecreated <=360)
)
ORDER BY RAND()
LIMIT 60000) AS z ON z.id= g.id
I have checked answers on here:
Too few parameters: the query defines 1 parameters but you only bound 0
and here
https://github.com/stwe/DatatablesBundle/issues/685
and feel like solution is somewhere close but cannot get to it
The short answer is that you need to actually call:
$qb->setParameter('sqbresult', $rand_num)
before the last line. In fact, there is no reason to call it on $qb1, as this will basically get discarded.
The reason for this is that, in your code above, you are basically just using $qb1 as a DQL generation mechanism. Any parameters set on this are not passed on when the call to getDQL is made, only the string value of the DQL at that point.
If you were to var_dump($qb->getDQL()) just before the end, you would see something like:
SELECT s FROM App\Entity\XX_Table s WHERE s.id IN (
SELECT subss.id FROM App\Entity\XX_Table subss
WHERE RAND() < :sqbresult
ORDER BY RAND() ASC
)
showing that :sqbresult still remains in the DQL and thus needs to have a parameter set.

MySQLi: Select price range if empty inputs

I have 2 variables to define a price range for a query. The problem I'm trying to solve is when these are not set in which case I want to show all rows (from 1, if the lower boundary is null, and to max(price) if the upper boundary is null).
I've tried with ifnull, but without success.
$priceFrom = $_POST['priceFrom'];
$priceTo = $_POST['priceTo'];
if(is_null($priceFrom) || is_null($priceTo)){
$priceFrom = 0;
$priceTo = 0;
}
$mass = array();
foreach($data as $current){
$sql = "SELECT p.price,
p.type,
p.area,
p.floor,
p.construction,
p.id as propertyID,
CONCAT(u.name, ' ',u.family) as bname,
p.type as ptype,
n.name as neighborhoodName,
CONCAT(o.name,' ',o.surname,' ',o.family) as fullName
FROM `property` p
LEFT JOIN `neighbour` n ON p.neighbour = n.id
RIGHT JOIN `owners` o ON p.owner = o.id
LEFT JOIN users u ON p.broker = u.id
WHERE `neighbour`= '$current'
AND `price` BETWEEN ifnull('$priceFrom', '1') AND ifnull('$priceTo','2000000')
";}
SQL INJECTION
^ Please Google that! Your code is seriously vulnerable! Your data can be stolen or deleted...
You have to sanitize your inputs at least with mysqli_real_escape_string()
Even better would be to take proper countermeasures to SQL injection and use prepared statements and parametrized queries! (as shown in the code below)
I think the best approach would be to handle the logic by altering the query based on the values of the variables:
$sql = "SELECT p.price,
p.type,
p.area,
p.floor,
p.construction,
p.id as propertyID,
CONCAT(u.name, ' ',u.family) as bname,
p.type as ptype,
n.name as neighborhoodName,
CONCAT(o.name,' ',o.surname,' ',o.family) as fullName
FROM `property` p
LEFT JOIN `neighbour` n ON p.neighbour = n.id
RIGHT JOIN `owners` o ON p.owner = o.id
LEFT JOIN users u ON p.broker = u.id
WHERE `neighbour`= :current "; //note: ending white space is recommended
//lower boundary clause -- if variable null - no restriction
if(!is_null($priceFrom){
sql = sql . " AND `price` >= :priceFrom "; // note: whitespace at end and beginning recommended
}
//upper boundary -- better than to set it to an arbitrary "high" value
if(!is_null($priceTo)){
sql = sql . " AND `price` <= :priceTo "; // note: whitespace at end and beginning recommended
}
This approach allows for any upper value: if there is a serious inflation, a different currency, or suddenly the code will be used to sell housese and there will be products with prices > 200000, you don't need to go out and change a lot of code to make it show...
The parameters need to be bound when executing the query of course:
$stmt = $dbConnection->prepare(sql);
$stmt->bind_param('current', $current);
if(!is_null($priceFrom)){
$stmt->bind_param('priceFrom', $priceFrom);
}
if(!is_null($priceTo)){
$stmt->bind_param('priceTo', $priceTo);
}
//execute and process in same way
$stmt->execute();
Also note: from your code it seems you are issuing queries in a loop. That is bad practice. If the data on which you loop comes
from the DB --> use a JOIN
from an array or other place of the code --> better use an IN clause for the elements
to fetch all data with one query. This helps a lot both in organizing and maintaining the code and results generally in better performance for the most cases.

Perform an Inner Join in Zend PHP Framework

I want to perform an inner join on two table.
Table A -
item_id
item_title
varX
Table B -
item_id
varY
someVar
This is how I've done this using a RAW SQL query.
$sql = 'SELECT tableA.item_id, tableY.item_title AS Name, 5 * varX + 5 * count(*) AS myScore
FROM tableA
INNER JOIN tableY ON tableA.item_id=tableY.item_id
WHERE someVar=\'8\'
GROUP BY item_id
ORDER BY myScore DESC
LIMIT 10';
$stmt = $this->_db->prepare($sql);
$stmt->execute();
$result = $stmt->fetchAll();
Now I want to do this using a Zend Query.
This is what I've written -
$data = $this->_db->select()
->from(array('tablA'=>'tableA'), array('item_id', 'item_title'), 'myScore'=>'(5*'tableA'.'varX') + 5*count(*)')
->joinInner(array('tablB'=>'tableB'), 'tablA'.'item_id' = 'tablB'.'item_id')
->where('someVar = 8')
->GROUP('item_id')
->order('myScore DESC')
->limit(10);
$dataResult = $this->_db->fetchAll($data);
But I get this error -
syntax error, unexpected '=>' (T_DOUBLE_ARROW), expecting ',' or ')'
in line ->from(array('tablA'=>'tableA'), array('item_id', 'item_title'), 'myScore'=>'(5'tableA'.'varX') + 5*count()')
Not sure what to do do here as I've read the official documentation but still can't figure this out. Any help is appreciated!
The quotes were used in wrong way in your code. Also, tt seems like, you have used the unnecessary third parameter for 'myScore' field. It should be placed in the second parameterTry the following:
...
$data = $this->_db->select()
->from(array('tablA'=>'tableA'), array('item_id', 'item_title', 'myScore'=>'(5 * tableA.varX) + 5*count(*)'))
->joinInner(array('tablB'=>'tableB'), 'tablA.item_id = tablB.item_id')
->where('someVar = 8')
->group('tablA.item_id')
->order('myScore DESC')
->limit(10);

Doctrine native query with mysql user variables

This is my native query:
$query = <<<Query
SET #num = 0;
SET #type = 0;
SELECT md.*, od.id AS order_detail_id, od.erc AS od_ec,
#num := if(#type = od.id, #num + 1, 1) AS row_number,
#type := od.id AS dummy
FROM bar md
LEFT JOIN foobar mb ON md.mbi = mb.id
LEFT JOIN barfoo od ON od.mbid = mb.id
WHERE od.id IN ($where)
AND (od.status = $statusQueued OR od.status = $statusProcessing)
GROUP BY md.id
HAVING row_number <= od.erc
ORDER BY md.qt ASC
LIMIT 0, 1000
Query;
$nativeQuery = $this->_em->createNativeQuery($query, $rsm);
When I execute it, PDO gets upset and throws me an error:
[PDOException] SQLSTATE[HY000]: General error
No more informations.
However when I get rid of first two lines (with "SET") the query runs (however returns wrong results, as the variables must be initialised in this case). How can I force PDO to run this correctly? (when run via PMA it works just fine)
It seems that most elegant solution is to leverage MySQL alternative initialisation syntax:
$query = <<<Query
SELECT md.*, od.id AS order_detail_id, od.erc AS od_ec,
#num := if(#type = od.id, #num + 1, 1) AS row_number,
#type := od.id AS dummy
FROM (SELECT #num := 0, #type := 0) init, bar md
LEFT JOIN foobar mb ON md.mbi = mb.id
LEFT JOIN barfoo od ON od.mbid = mb.id
WHERE od.id IN ($where)
AND (od.status = $statusQueued OR od.status = $statusProcessing)
GROUP BY md.id
HAVING row_number <= od.erc
ORDER BY md.qt ASC
LIMIT 0, 1000
Query;
I ran into the same kind of issue trying to run several queries in one call (in my case it was about using SQL_CALC_FOUND_ROWS and its buddy FOUND_ROWS()).
Doctrine can't handle this. I don't know the official reason, but in my opinion it's probably a security concern (as the EM's calls are then open to basic query injection).
Still in my opinion, the most elegant way of dealing with this is using the EM's transactional mode:
//fetch PDO-ish adapter
$conn = $em->getConnection();
//open an atomic sequence
$conn->beginTransaction();
//do your stuff
$conn->executeQuery('SET #...');
$stmt = $conn->executeQuery('SELECT stuff using #vars');
//commit the atomic sequence
$conn->commit();
//fetch the result, could be useful ;)
$data = $conn->fetchAll();
Hope this helps! (and works in your case...)

pdo prepared statements do not work, but an ordinary query does. What am I missing?

For some reason I cannot get this to work:
/**
* $_COOKIE['MyShoppingList'] is a serialized array with intergers.
*/
if($_COOKIE['MyShoppingList']){
foreach(unserialize($_COOKIE['MyShoppingList']) as $recipe){
$recipes_ids .= $recipe.',';
}
$sql_WHERE_NOT_IN = 'WHERE r.id NOT IN (:ids)';
}
$qry_recipes = $dbh->prepare('
SELECT r.id drink_id, r.name drink_name
FROM recipes r
'.$sql_WHERE_NOT_IN.'
');
if($_COOKIE['MyShoppingList']){
$qry_recipes->execute(array(':ids'=>rtrim($recipes_ids,','))); // I've verified that this is in fact a string with all the intergers sepparated with a comma.
} else {
$qry_recipes->execute();
}
This does work like a charm:
if($_COOKIE['MyShoppingList']){
/* the $recipes_id is the same as before */
$sql_WHERE_NOT_IN = 'WHERE r.id NOT IN ('.rtrim($recipes_ids,',').')';
}
$qry_recipes = $dbh->query('
SELECT r.id drink_id, r.name drink_name
FROM recipes r
'.$sql_WHERE_NOT_IN.'
');
The only difference is that the former is using prepared statements, and the latter is a pure query.
What happens is that it looks like the former, prepared, is not detecting the $recipes_ids-string..
Is there something about the $recipes_ids I'm overlooking?
rtrim(...) string (13) "12,1,2,3,9,10" // it's like this in both scenarios
I've tried bindParam() as well, but that resulted in this error message:
"Strict Standards: Only variables should be passed by reference"
I'm not sure what that means, but it might be telling me what I should be doing..
So please let me know..
Also; I've tried putting rtrim($recipes_ids,',') into a variable before sending it to the prepared query - but with no luck..
You cannot bind multiple values to a single named parameter in, for example, the IN() clause of an SQL statement.
Try this way:
/**
* $_COOKIE['MyShoppingList'] is a serialized array with intergers.
*/
$recipes_ids = array();
if($_COOKIE['MyShoppingList']){
foreach(unserialize($_COOKIE['MyShoppingList']) as $recipe){
$recipes_ids[] = $recipe;
}
$sql_WHERE_NOT_IN = 'WHERE r.id NOT IN (' . str_repeat('?, ', count($recipe_ids) - 1) . '?)';
}
$qry_recipes = $dbh->prepare('
SELECT r.id drink_id, r.name drink_name
FROM recipes r
'.$sql_WHERE_NOT_IN.'
');
if($_COOKIE['MyShoppingList']){
$qry_recipes->execute($recipe_ids);
} else {
$qry_recipes->execute();
}

Categories