mysqli prepared regular expression or other way - php

So I need to select data from a MySQL table by looking into one field and see if it has a certain word in it and do a whole lot of other things in the where clause which is dynamically generated.
In the old days with the old mysql extension I would do this:
select [bunch of stuff] left join [bunch of stuff] where
`field` rlike "(?=.*word1)(?=.*word2)(?=.*word3)..."
and [more where..] order by [order stuff]
Now of course I use mysqli and a prepared statement...
select [bunch of stuff] left join [bunch of stuff] where
match(`field`) against(?,?,?...)
and [more where..] order by [order stuff]
Unfortunately I got a InnoDB table which means I don't have full text search which would bring me to chain some like statement together like so:
select [bunch of stuff] left join [bunch of stuff] where
`field` like concat("%",?,"%") or `field` like concat("%",?,"%") ...
and [more where..] order by [order stuff]
But this would mean it breaks the "and" chain I have going here and would need to repeat [more where..] in every "or".... This has got to be wrong and I have been staring at this for too long now.
Any ideas?

You can build your query with array:
$sql="SELECT...WHERE ";
$cond=array();
$bind=array();
$subCond=array()
foreach($searchQuery as $txt) //assumed you have stored search query
{
$subCond[]="`field` LIKE concat('%',?,'%')";
$bind[]=$txt;
}
$cond[]="(".implode(" OR ",$subCond).")";
/*...continue to build conditions...*/
$sql.=implode(" AND ",$cond);
$sql.=" SORT BY ...";
$stmt=$mysqli->prepare($sql);
call_user_func_array(array($stmt,"bind_param"),array_merge(array(str_repeat("s",count($bind))),$cond));
$stmt->execute();
Noted that the above code was not tested, and may raise warning (possible due to the pass by reference issue), but it gives you the idea.
Also check out this comment for a variable-number-variable-binding solution.

Related

Phalcon PhP: how to bind parameters in SQL query inside a Controller

I'm trying to run a raw SQL query, inside a Controller class, and I'm having trouble to figure out why I can't bind more than one parameter. Take a look at the snippet below:
$sql = "select
u.id,
u.name,
u.email,
r.name as role
from user u
inner join role r on r.id = u.role_id
left join user_group_user ugu on ugu.user_id = u.id and ugu.user_group_id = ".$user_group_id."
where (u.name like :search or u.email like :search or r.name like :search)
and ugu.user_id is null and u.id not in( :notIn )
order by u.name, r.name";
$users = $this->db->query($sql, ['search' => '%'.$search.'%', 'notIn'=>$notInStr ])->fetchAll();
The $notIn variable has a value like 1,2,3. If I do the same thing only with the parameter :search the query works. When I try with both parameters (:search and :notIn) the query returns but the :notIn seems to not have effect in the query. It looks like it is not being bound.
How can I run this query considering both parameters?
Thanks for any help
UPDATE:
The binding is actually working but it is binding the :notIn as a string, so the executed query is .... not in ('1,2,3')
I have solved my problem just making sure that everything is a number and just concatenating in the query: ...u.id not in( ".$notInStr." )
Standard pdo library which phalcon is using and which phalcon db is wrapping doesn't accept array as bound parameter. You would need to use phalcon models.
Make use of Phalcon\Mvc\Model\Query\Builder. There is a inWhere method, that accepts array as parameter.
Remember to never use not filtered data that You get from end-user, like You just did. This makes Yours application vulnerable to SQLInjection attacks.

DBAL cardinality violation error

I am getting the 'Cardinality Violation' error, for the following SQL:
Doctrine\DBAL\Exception\DriverException: An exception occurred while executing
SELECT p.* FROM mod_products_products p
LEFT JOIN mod_products_products_categories c_link ON c_link.product_id = p.id
LEFT JOIN mod_products_brands b ON p.brand_id = b.id
LEFT JOIN mod_products_groups vg ON p.variation_id = vg.id
LEFT JOIN mod_products_categories c ON c_link.category_id = c.id
LEFT JOIN mod_products_group_options vg_o ON vg_o.group_id = vg.id
LEFT JOIN mod_products_group_values vg_o_v ON vg_o_v.option_id = vg_o.id
WHERE (p.name LIKE (?, ?)) AND (p.parent_id = 0) AND (vg_o.disabled=0)
GROUP BY p.id ORDER BY p.name ASC
LIMIT 18446744073709551615 OFFSET 0
with params ["%big%", "%light%"]: SQLSTATE[21000]: Cardinality violation: 1241 Operand should contain 1 column(s).
The error only occurs if there is more than one value defined in the parameter list for WHERE (p.name LIKE (?, ?)).
I am using executeQuery(), and passing the array as Connection::PARAM_STR_ARRAY. In the original statement I am defining the trouble point as:
$builder->andWhere('p.name LIKE (:partial_names)');
It seems it doesn't like getting an array passed as partial_names. Any ideas on what is causing this, and how to avoid it?
MySQL LIKE is a "string comparison function" and as such compares one string to another, using "simple pattern matching".
If you check the SQL standard, you'll notice that the BNF grammar for LIKE accepts only "character-like" and "octet-like" arguments, both of which are essentially what we'd call strings. (There is some detail around the fact that LIKE performs a binary, character-for-character match on the RHS, which is different than how = operates: foo LIKE 'bar' and foo='bar' may produce different results.)
All this means you can't do LIKE ('a', 'b') because the columnar expression ('a', 'b') is not string-like. Or in geeky standard language, it's cardinality (2) differs from the expected cardinality (1). However, you can do this in MySQL and SQLite (maybe other engines):
WHERE foo LIKE ('%bar')
because the cardinality of the RHS is 1 (there is one column), which is what LIKE expects.
You're wanting something effectively similar to foo LIKE IN ('a', 'b'), but that doesn't exist either (for the SQL standard reason mentioned above). This Q&A shows some workarounds for that behavior, REGEXP based being the accepted answer.
So, to get around this error, you need to rewrite your query to use multiple LIKE, or a REGEXP, or maybe even something like FIND_IN_SET.
Change
(p.name LIKE (?, ?))
to
(p.name LIKE ? OR p.name LIKE ?)
and
["%big%", "%light%"]
to
"%big%", "%light%"

PHP PDO too slow on SELECT with some joins

I'm having a performace problem with the execution of a select in PHP PDO.
Using a script available here at stackoverflow (Simplest way to profile a PHP script), I identified where the problem IS, but I have not found a solution.
My select that is the problem is:
SELECT REDACAO.ID_REDACAO AS ID_REDACAO,
DATE_FORMAT(REDACAO.DATA,'%d/%m/%Y') AS DATAE,
ALUNO.ID_ALUNO AS ID_ALUNO,
(SELECT IFNULL((DATEDIFF(DATE_ADD((SELECT MAX(DATA) FROM REDACAO WHERE ID_ALUNO = ALUNO.ID_ALUNO AND ID_REDACAO NOT IN (SELECT ID_REDACAO FROM CORRECAO)), INTERVAL 7 DAY), now())),NULL) as DATA FROM REDACAO LIMIT 1) AS ULTIMA,
ALUNO.NOME as ALUNO,
REDACAO.ID_TEMA AS ID_TEMA,
TEMA.TITULO as TEMA,
TEMA.MOTIVACIONAIS AS MOTIVACIONAIS,
REDACAO.TEXTO AS TEXTO,
REDACAO.ID_STATUS AS STATUS,
B.NOTA as NOTA,
B.RCORRIGIDA AS CORRIGIDA,
B.NOTA1,
B.COMENTARIO1,
B.NOTA2,
B.COMENTARIO2,
B.NOTA3,
B.COMENTARIO3,
B.NOTA4,
B.COMENTARIO4,
B.NOTA5,
B.COMENTARIO5,
B.COMENTARIO6,
C.COMENTARIO AS COMENTARIO
FROM REDACAO
LEFT OUTER JOIN (SELECT SUM(CORRECAO.C1+CORRECAO.C2+CORRECAO.C3+CORRECAO.C4+CORRECAO.C5) AS NOTA, RCORRIGIDA AS RCORRIGIDA, CORRECAO.C1 as NOTA1, CORRECAO.COM1 as COMENTARIO1, CORRECAO.C2 as NOTA2, CORRECAO.COM2 as COMENTARIO2, CORRECAO.C3 as NOTA3, CORRECAO.COM3 as COMENTARIO3, CORRECAO.C4 as NOTA4, CORRECAO.COM4 as COMENTARIO4, CORRECAO.C5 as NOTA5, CORRECAO.COM5 as COMENTARIO5, CORRECAO.COMGERAL AS COMENTARIO6, CORRECAO.ID_REDACAO FROM CORRECAO GROUP BY CORRECAO.ID_REDACAO) B
ON B.ID_REDACAO = REDACAO.ID_REDACAO
JOIN ALUNO ON ALUNO.ID_ALUNO = REDACAO.ID_ALUNO
JOIN TEMA ON TEMA.ID_TEMA = REDACAO.ID_TEMA
LEFT OUTER JOIN (SELECT (COUNT(COMENTARIO.ID_COMENTARIO)) AS COMENTARIO, COMENTARIO.ID_REDACAO FROM COMENTARIO GROUP BY COMENTARIO.ID_REDACAO) C
ON C.ID_REDACAO = REDACAO.ID_REDACAO
WHERE REDACAO.ID_PROFESSOR = $CodProfessor
and REDACAO.ID_STATUS != 6
ORDER BY (CASE WHEN REDACAO.ID_STATUS = 4 THEN 1 ELSE 0 END) DESC
I'm using (PDO :: FETCH_ASSOC) to get the data. Some columns respond in less than 1 second and others in more than 20 seconds.
Any idea what could be the problem and how to solve it?
Your query contains following that will slow it down:
many joins
many subselects
select without where
functions like COUNT, isnull, datediff, sum.(some of these may cancel an index)
case when
order by
group by
Depending on your indexes, on how the tables are joined, and on how big are the tables, this will eventually get very slower.
Try using 'explain' command, and simplify the query if possible.
explain output
a good video about explain
PDO is not at fault; the query is complex. And there may be missing indexes.
Turn this into a LEFT JOIN (because IN (SELECT...) optimizes poorly.
AND ID_REDACAO NOT IN ( SELECT ID_REDACAO FROM CORRECAO)
Upgrade to 5.6; it has some improvements.
You have two JOIN ( SELECT ... ). Before 5.6 that would be optimized terribly. Move one of them out into a temp table, to which you add a suitable index.
In one of the subqueries, GROUP BY CORRECAO.ID_REDACAO seems to be unnecessary.
These indexes (or PRIMARY KEYs) are needed:
CORRECAO: (ID_REDACAO)
REDACAO: (ID_REDACAO), (ID_PROFESSOR)
ALUNO: (ID_ALUNO)
TEMA: (ID_TEMA)
COMENTARIO: (ID_REDACAO, ID_COMENTARIO) ("compound index")
If those suggestions do not help enough, come back with SHOW CREATE TABLE for each table.

PHP+MSSQL JOIN two tables outputs nothing - "No tuples"

$conn=odbc_connect('mydatabase','','');
$sql="SELECT
Orders.OrderID,
Orders.OrderDate,
\"Order Details\".OrderID,
\"Order Details\".UnitPrice,
\"Order Details\".Quantity,
\"Order Details\".Discount,
FROM
\"Order Details\"
INNER JOIN
Orders
ON \"Order Details\".OrderID = Orders.OrderID";
$rs=odbc_exec($conn,$sql) or die("<p>".odbc_errormsg());
while (($row = odbc_fetch_array($rs)) !== false)
When I try to output the results of $rs nothing is returned. If I try to access fields from either Orders or \"Order Details\" separately it works fine but if I try to JOIN the two tables it outputs nothing.
Is this the correct way to SELECT a field from a table that has a space in its name when using MSSQL? It seems to work when I try "SELECT * FROM \"Order Details\"" but when I try to join the tables I have to specify "\Order Details\".OrderID and I think that might be where it's getting confused.
Apache error log returns a strange "odbc_fetch_array(): No tuples available at this result" message, but I'm not positive this is the actual problem as I've seen this error message pop up randomly for unrelated reasons. Still, I don't quite understand it so thought I should mention it.
Thanks!
Instead of using "/", use brackets.
"SELECT Orders.OrderID, [Order Details].UnitPrice
FROM [Order Details] INNER JOIN Orders ON [Order Details].OrderID = Orders.OrderID"

nesting conditions with DQL

Now that I've read all the DQL docs I still have some doubts, I'm
trying to do some nested condictions in my DQL however playing around
with DQL I can't seem to be able to archive them
To make myself more clear:
I have this DQL query
$q = Doctrine_Query::create()
->select('c.nombre,c.webpage')
->addSelect('COUNT(m.marca_id) as total_marcas')
->from('Corporativos c')
->leftJoin('c.Marcas m')
->groupBy('c.corporativo_id')
->where('ISNULL(c.deleted_at)')
->orwhere('c.nombre LIKE :nombre', array(':nombre'=>'%'.$srch))
->orWhere('c.nombre LIKE :nombre', array(':nombre'=>'%'.$srch.'%'))
->orWhere('c.nombre LIKE :nombre', array(':nombre'=>$srch.'%'))
->orderBy('c.nombre ASC')
->limit(0,20);
now this generates the following MySQL query:
SELECT c.corporativo_id AS c__corporativo_id, c.nombre AS c__nombre,
c.webpage AS c__webpage, COUNT(m.marca_id) AS m__0 FROM corporativos c
LEFT JOIN marcas m ON c.corporativo_id = m.corporativo_id WHERE
(ISNULL(c.deleted_at) OR c.nombre LIKE :nombre OR c.nombre
LIKE :nombre OR c.nombre LIKE :nombre) GROUP BY c.corporativo_id ORDER
BY c.nombre ASC
However I'm getting a set of results where either deleted_at is null
or the other conditions is completed, I'd like to make the
isnull(deleted_at) obligatory, if we were talking in terms of SQL the
query would look like this:
SELECT c.corporativo_id AS c__corporativo_id, c.nombre AS c__nombre,
c.webpage AS c__webpage, COUNT(m.marca_id) AS m__0 FROM corporativos c
LEFT JOIN marcas m ON c.corporativo_id = m.corporativo_id WHERE
(ISNULL(c.deleted_at) AND (c.nombre LIKE :nombre OR c.nombre
LIKE :nombre OR c.nombre LIKE :nombre)) GROUP BY c.corporativo_id
ORDER BY c.nombre ASC
you can see that I just changed the first OR statement for an AND and
added a couple of parenthesis to group the LIKE conditions.
Is it posible to archivie this in DQL using the same ->where()
notation avoiding writing down the whole condition ?
thanks :)
Not sure how recently this change was made, but for anyone like me coming to this question looking for answers long after this question was asked, the query builder now has ways to accomplish nested AND/OR logic: andX() / orX() functions.
You can use Doctrine_Query::andWhere, but i think this will factor your statement the wrong way, so you could build your query like
->where('c.nombre LIKE :nombre', array(':nombre'=>'%'.$srch))
->andWhere('ISNULL(c.deleted_at)')
->orWhere('c.nombre LIKE :nombre', array(':nombre'=>'%'.$srch.'%'))
->andWhere('ISNULL(c.deleted_at)')
->orWhere('c.nombre LIKE :nombre', array(':nombre'=>$srch.'%'))
->andWhere('ISNULL(c.deleted_at)')
which is kind of creepy.
Here is another solution with modifies Doctrine to be capable of custom bracketing: http://danielfamily.com/techblog/?p=37

Categories