Extract WHERE components from MySQL string dynamically - php

Say I have the following string (forget that it is a MySQL statement):
SELECT * FROM users WHERE name = 'fred bloggs' AND age=21 AND address='Mountain View, CA 94043' LIMIT 1
I need a way to extract the field name and value in the WHERE clause so I have an array like:
Array
(
[name] => fred bloggs
[age] => 21
[address] => Mountain View, CA 94043
)
Remember this is a dynamic MySQL string so I can't hard-code name, age or address.
The problems I can foresee are:
Finding the field names, the function would have to know all valid operators so to match each field name (see array to use below)
Spaces are not guaranteed (e.g. age=21)
Finding the values within ' and ' but not breaking when a ' is within the string
Finding values not within ' and ' e.g. numbers or other field names (would need to be treated seperately)
Does anyone have any ideas how to do this either with regex or string functions?
Here are the operators if required:
$operators = array('+', '-', '*', '/', '^', '=', '<>', '!=', '<', '<=', '>', '>=', 'like', 'clike', 'slike', 'not', 'is', 'in', 'between', 'and', 'or');

You can try with this kind of tool :
http://code.google.com/p/php-sql-parser/
You will get a tree (an array) describing the query, this will help you to build the array you want.

You should create an expresssion parser for that, it cannot be achieved with regex . E.g., your resulting array would contain an expression tree with all node types you specified (OR, AND, IN, BETWEEN, etc.)
There's an example on the web: here

There are probably a few ways to approach this:
1) Look for metadata from the query. I know you can get info about the resultset but Im not sure about the original query. Its in there somewhere but might need to be teased out.
2) Use a prepared statement to build your query from pieces. Pass in the various where conditions in an array. Its kind of backwards (IE starting with the answer you want and then building the query) though.

Related

Cakephp3 case mysql statement is not creating the correct query

I'm trying to create a query that returns the sum of a column using a case (it has logged time and the format in either minutes or hours, if it's in hours, multiply by 60 to convert to minutes). I'm very close, however the query is not populating the ELSE part of the CASE.
The finder method is:
public function findWithTotalTime(Query $query, array $options)
{
$conversionCase = $query->newExpr()
->addCase(
$query->newExpr()->add(['Times.time' => 'hours']),
['Times.time*60', 'Times.time'],
['integer', 'integer']
);
return $query->join([
'table' => 'times',
'alias' => 'Times',
'type' => 'LEFT',
'conditions' => 'Times.category_id = Categories.id'
])->select([
'Categories.name',
'total' => $query->func()->sum($conversionCase)
])->group('Categories.name');
}
The resulting query is:
SELECT Categories.name AS `Categories__name`, (SUM((CASE WHEN
Times.time = :c0 THEN :c1 END))) AS `total` FROM categories Categories
LEFT JOIN times Times ON Times.category_id = Categories.id GROUP BY
Categories.name
It's missing the ELSE statement before the CASE end, which according to the API docs:
...the last $value is used as the ELSE value...
https://api.cakephp.org/3.3/class-Cake.Database.Expression.QueryExpression.html
I know there might be a better way to do this, but at this point I'd like to at least know how to do CASE statements properly using the built in QueryBuilder.
Both arguments must be arrays
Looks like there are some documenation issues in the Cookbook, and the API could maybe be a little more clear on that subject too. Both, the $conditions argument as well as the $values argument must be arrays in order for this to work.
Enforcing types ends up with casting values
Also you're passing the SQL expression wrong, including the wrong types, defining the types as integer will cause the data passed in $values to be casted to these types, which means that you will be left with 0s.
The syntax that you're using is useful when dealing with user input, which needs to be passed safely. In your case however you want to pass hardcoded identifiers, so what you have to do is to use the key => value syntax to pass the values as literals or identifiers. That would look something like:
'Times.time' => 'identifier'
However, unfortunately there seems to be a bug (or at least an undocumented limitation) which causes the else part to not recognize this syntax properly, so for now you'd have to use the manual way, that is by passing proper expression objects, which btw, you may should have done for the Times.time*60 anyways, as it would otherwise break in case automatic identifier quoting is being applied/required.
tl;dr, Example time
Here's a complete example with all forementioned techniques:
use Cake\Database\Expression\IdentifierExpression;
// ...
$conversionCase = $query
->newExpr()
->addCase(
[
$query->newExpr()->add(['Times.time' => 'hours'])
],
[
$query
->newExpr(new IdentifierExpression('Times.time'))
->add('60')
->tieWith('*'), // setConjunction() as of 3.4.0
new IdentifierExpression('Times.time')
],
);
If you were for sure that you'd never ever make use of automatic identifier quoting, then you could just pass the multiplication fragment as:
'Times.time * 60' => 'literal'
or:
$query->newExpr('Times.time * 60')
See also
Cookbook > Database Access & ORM > Query Builder > Case statements
Cookbook > Database Access & ORM > Query Builder > Using SQL Functions
API > \Cake\Database\Expression\QueryExpression::add()
API > \Cake\Database\Expression\QueryExpression::tieWith()

%LIKE% retrieves eg. WOMEN DATA when calling MEN

$itemList = DB::table('items')
->orderBy('id', 'desc')
->where('item_status', 2)
->where(function($query) use($queryArr)
{
foreach($queryArr as $uID){
$query->whereRaw("tags LIKE '%$uID%'");
}
})->paginate(21);
I have been facing this issue since a long time. Problem when you do a LIKE search is it grabs the data of WOMEN when it's just eg MEN
Mainly because MEN is inside Women
I also tried the following but failed(This sort of grab a word) men without women data
$query->where('tags', 'LIKE', '%'.$uID.'%');
SELECT 'a word a' REGEXP '[[:<:]]word[[:>:]]';
How do i use that Word boundary query in laravel query builder
Tried this and still failed $query->whereRaw("tags LIKE REGEXP '[[:<:]]Men[[:>:]]'");
SQLSTATE[42000]: Syntax error or access violation: 1064 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 'REGEXP '[[:<:]]Men[[:>:]]')' at line 1 (SQL: select count(*) as aggregate from items where item_status = ? and (tags LIKE REGEXP '[[:<:]]Men[[:>:]]')) (Bindings: array ( 0 => 2, ))
I also understand some asked why not just created a proper way of handling these item's category. Well i think for now using Full Text Search is fine for me at most when it come to scaling i will use elastic search. true?
UPDATE
Apologies for not giving an example of tags
Bag,Shoes,Men,Wedges
Bag,Shoes,Men
Men,Shoes,Bag
If values are separated by commas, try to use following
WHERE `tags` REGEXP "(^|,)men(,|$)"
This will require to have comma or end of string around the word men.
Your REGEXP solution is throwing the syntax error because of the extra LIKE keyword. The correct syntax is simply WHERE column REGEXP 'pattern'.
If you find this hard to remember, try using RLIKE instead, which is a synonym for MySQL's REGEXP. There is less chance you will accidentally write WHERE column LIKE RLIKE ... in your query because it is more obviously redundant.
The pattern itself should work fine. Personally, I dislike using those word boundary classes in MySQL; since you know your tags are comma delimited and space padded, [ ,]Man[ ,] would function just as well. The word boundary class breaks at punctuation other than underscores, so you could run into trouble if you have tags that are hyphenated, for instance.
If you want to use multiple word tags with spaces, either of the previous patterns is buggy. In that case I would try to stick with commas as your delimiter, get rid of the whitespace padding and use anchors instead, as suggested in one of the other answers: (^|,)Man($|,)
In your query you said : Find anything that contains the tag 'MEN' => '%MEN%', that's why it shows: WOMEN AND MEN, so to answer to your question you should use starts with instead of contains, like this :
$query->where('tags', 'LIKE', $uID.'%');

mysql postcode search

Evening All.
I have a mysql database for a property website. There is a search form where people can enter a location or postcode in the same field.
Part of the SQL is
PostCode LIKE '$Loc%
Put my problem is some people enter a post code like this : "l236yt" and some with a space like this "l23 6yt".
The database contains the postcodes with the space in them so how can make it work with or without the space ??
Any help will be greatly appreciated
thanks baz
Assuming the values in your database are without space, just sanitize the user value to a value without space:
$val = str_replace(' ', '', $val);
You could convert the string into an array of characters, and search for spaces, if you don't find any, you can then proceed to insert a space (the space will always be after the third character in a 6 character postcode, and after the fourth character in a 7 character postcode, so you can use regex to do this quite simply. I believe there are also postcodes which are 5 characters long, and these will be like A1 1AA, so after the second character you'll find the space.
I can't help with precise code due to my lack of knowledge of the language, but good luck!
Expects a valid postcode as input (with or without spaces), and breaks it down into constituent parts as well as returning it neatly formatted with a space in the appropriate place
function parsePostcode($postcode) {
$postcode = preg_replace('/\s*/','',strtoupper($postcode));
$sector = substr($postcode,0,-2);
$outcode = $district = substr($sector,0,-1);
list($area) = sscanf($district,'%[A-Z]');
$incode = substr($postcode,-3);
return array(
'postcode' => $postcode,
'formatted' => $outcode.' '.$incode,
'area' => $area,
'district' => $district,
'sector' => $sector,
'outcode' => $outcode,
'incode' => $incode,
);
}
Your first priority is to remove the LIKE condition from your query. Using LIKE conditions forces MySQL to evaluate every row in your table and is very inefficient. Try to avoid using a LIKE unless absolutely necessary. In order to change this part of your query, you will need to replace it with:
PostCode = "$Loc"
This presents you with two options:
1) sanitise your input. Postcodes follow a well known format so it is possible to convert the value that someone enters into something you expect. You can then search on. As $Loc would match exactly what you have in the database, it would be very fast to find in your database (provided you have indexed the field of course!).
2) overload the database with multiple values to represent the same postcode. This would mean that you would put both "l236yt" and "l23 6yt" in the database and handle them as if they are different values. This also helps when you want to search on just the first part of the postcode, such as "l23", but would only work if you have a one-to-many relationship between postcodes and locations.

How to do OR search

I want to search for a partial first and last name match - for example in sql
f_name LIKE J% OR l_name LIKE S%
would match John Smith or John Henry or Harry Smith .
I am assuming I may need to use the "$or" operator,
I have this so far that I believe is doing the LIKE % part properly, but I believe it is doing an "AND" search (meaning it searches for f_name LIKE J% AND l_name LIKE S% so it would only match John Smith):
$name1="J";
$name2="S";
$cursor = $this->clientCollection->find(array('f_name' => new MongoRegex('/^'.$name1.'/i'), 'l_name' => new MongoRegex('/^'.$name2.'/i') ));
Note: This will match containing as in %J%
MongoRegex('/'.$name1.'/i')
This will match starts with (notice the added ^) as in J%
MongoRegex('/^'.$name1.'/i')
$or takes an array of clauses, so you basically just need to wrap another array around your current query:
array('$or' => array(
array('f_name' => new MongoRegex('/'.$name1.'/i')),
array('l_name' => new MongoRegex('/'.$name2.'/i'))
));
Edit: the previous example missed an inner set of array() calls.
The original, wrong, example that I posted looked like this:
array('$or' => array(
'f_name' => new MongoRegex('/'.$name1.'/i'),
'l_name' => new MongoRegex('/'.$name2.'/i')
));
This is a valid query, but not a useful one. Essentially, the f_name and l_name query parts are still ANDed together, so the $or part is useless (it's only getting passed one query, so it's the same as just running that query by itself).
As for the alternative you mentioned in your comment, that one doesn't work because the outermost array in a query has to be an associative array. The confusion arises because Mongo's query syntax is JSON-like and uses a mixture of objects and arrays, but both of those structures are represented in PHP by arrays. The PHP Mongo driver basically converts PHP associative arrays to JSON objects ({ ... }), and "normal" PHP arrays to JSON arrays ([ ... ]).
The practical upshot is that "normal" PHP arrays are generally only valid when inside an associative array, like when specifying multiple values for a field. The following example from the PHP Mongo manual shows a valid usage of a "normal" array in a query:
$cursor = $collection->find(array("awards" => array('$in' => array("gold", "copper"))));

How can I avoid separating the search query to search a MySQL database?

When a user tries to search for people on our system, we've noticed that they often type a full name for their search.
How can we enable full name searching on our database when first name and last name are stored in different columns?
A working but neither fast nor reliable solution would be:
SELECT foo
FROM bar
WHERE CONCAT(firstname, ' ', lastname) = $search_name
(Not sure about MySQL CONCAT syntax atm, you might need to concat twice)
There is a high chance that this won't use any indexes, slowing down the search a lot.
A better solution would be to just split the name:
$names = explode(' ', $search_name);
SELECT foo
FROM bar
WHERE firstname = $names[0]
AND lastname = $names[1]
Even better:
Provide two input boxes and label them first and lastname so users will enter the search correctly.
Even better - If you are using CakePHP 1.3 then simply create a 'full_name' virtual field. Do your query against that field.
http://book.cakephp.org/view/1609/Creating-virtual-fields
http://book.cakephp.org/view/1610/Using-virtual-fields
class User extends AppModel {
...
var $virtualFields = array(
'full_name' => 'CONCAT( User.first_name, " ", User.last_name )'
);
...
}
Since Cake treats virtual fields as regular fields for the primary model you can simply do a find like so.
$User->find( 'all', array(
'conditions' => array(
'User.full_name' => $search_name
...
),
...
);
You could even throw in some MySQL LIKE goodness in the conditions
$User->find( 'all', array(
'conditions' => array(
'User.full_name LIKE' => '%' . $search_name . '%',
...
),
...
);
This syntax means the end-user can put part of the first, part of the last or the whole name into the search field and still get results without worrying about if the code is doing finds on first, last or both.
Your 99% case is probably going to be this:
Split the string on the first whitespace
Assume the first part is the first name, and the 2nd part is the last name
Depending on the number of users in your system, and the... "creativeness" of their names, you may get a few false negatives from this approach.
You could also consider providing separate fields to the user.
You could set up a single fulltext index on first_name and last_name. Then, just include a test for MATCH (first_name, last_name) AGAINST ($query). Or if you have more than just that field to search against, add those to the index as well.
Let SQL do the work for you.
How do you search the people in your DB?
If you have a SQL query you can do it somehow like that:
Separate the search text into parameters. so you have firstname and lastname in 2 separate parameters. (Stackoverflow: How to split a string in php)
then you can use the different parameters to build a query that fits your needs:
select *
from People
where
(firstname LIKE param1 AND lastname LIKE param2) OR
(firstname LIKE param2 AND lastname LIKE param1) OR ...

Categories