peculiar behaviour of dynamically generated mysqli prepared statement - php
What started out yesterday as the beginnings of an answer for, or improvements to, code found in this question has now become something of an enigma. I cannot figure why the dynamically generated query produces different results to the equivilent when run in the mysqli client or even when written out in php as a standard prepared statement ( losing the dynamic element )
For anyone wishing to experiment I included the table schema and dummy records.
The dynamic query is built based upon existence of certain variables ( these would, in the original question, have been POST variables ) but here are static - though they can be negated by commenting them out which will in turn affect the WHERE clause that is generated.
<style>
body, body *{font-size:0.85rem;}
h1,h2{font-size:0.95rem;text-decoration:underline}
pre{white-space: pre-wrap;word-wrap: break-word;}
</style>
<?php
/*
to create a suitable table and populate with dummy data
create table `volopp` (
`id` int(10) unsigned not null auto_increment,
`taskname` varchar(50) not null default '0',
`industryname` varchar(50) not null default '0',
`country` varchar(50) not null default '0',
`orgname` varchar(50) not null default '0',
`photo` varchar(50) not null default '0',
primary key (`id`)
)
engine=innodb;
insert into `volopp` (`id`, `taskname`, `industryname`, `description`, `country`, `orgname`, `photo`, `status`) values
(1, 'squirrel juggling', 'outdoor pursuits', 'squirrels are members of the family sciuridae, a family that includes small or medium-size rodents. the squirrel family includes tree squirrels, ground squirrels, chipmunks, marmots, flying squirrels, and prairie dogs amongst other rodents.', 'scotland', 'squirrel jugglers association', 'squirrel.jpg', 1),
(2, 'hedgehog pickling', 'food and drink', 'a hedgehog is any of the spiny mammals of the subfamily erinaceinae, in the eulipotyphlan family erinaceidae. there are seventeen species of hedgehog in five genera found through parts of europe, asia, and africa, and in new zealand by introduction.', 'england', 'hog heaven association', 'hedgehog.jpg', 0),
(3, 'haggis hunting', 'outdoor pursuits', 'wild haggis is a creature of scottish folklore, said to be native to the scottish highlands. it is comically claimed to be the source of haggis, a traditional scottish dish that is in fact made from the innards of sheep.', 'scotland', 'campbell\'s haggis hunt', 'lesser-haggis.jpg', 1),
(4, 'nessie netting', 'outdoor pursuits', 'the loch ness monster or nessie is said to be a creature that inhabits loch ness in the scottish highlands. it is often described as large in size with a long neck and one or more humps protruding from the water.', 'scotland', 'nessienets.org.uk', 'loch-ness.jpg', 0),
(5, 'dwarf tossing', 'outdoor pursuits', 'dwarf-tossing, also called midget-tossing, is a pub/bar attraction in which people with dwarfism, wearing special padded clothing or velcro costumes, are thrown onto mattresses or at velcro-coated walls. participants compete to throw the person with dwarfism the farthest.', 'scotland', 'highlandgames.scot', 'highland-games.jpg', 1),
(6, 'stickleback stuffing', 'indoor pursuits', 'sticklebacks are small, elongated fishes that reach a maximum length of about 18 cm (7 inches). the members of the family are characterized by a row of 2 to 16 spines on the back, which are positioned in front of a soft-rayed dorsal fin.', 'wales', 'stickleback.org', 'spinysucker.jpg', 0),
(7, 'squirrel suckling', 'historic pastimes', 'tree rats', 'ireland', 'weirdness abounds', 'treerat.gif', 0);
For reference
+----+----------------------+-------------------+--------------------+----------+-------------------------------+--------------------+--------+
| id | taskname | industryname | description | country | orgname | photo | status |
+----+----------------------+-------------------+--------------------+----------+-------------------------------+--------------------+--------+
| 1 | squirrel juggling | outdoor pursuits | Squirrels are m... | scotland | Squirrel Jugglers Association | squirrel.jpg | 1 |
| 2 | Hedgehog Pickling | food and drink | A hedgehog is a... | england | Hog Heaven Association | hedgehog.jpg | 0 |
| 3 | Haggis Hunting | outdoor pursuits | Wild haggis is ... | scotland | Campbell's Haggis Hunt | lesser-haggis.jpg | 1 |
| 4 | Nessie Netting | outdoor pursuits | The Loch Ness M... | scotland | NessieNets.org.uk | Loch-Ness.jpg | 0 |
| 5 | Dwarf Tossing | outdoor pursuits | Dwarf-tossing, ... | scotland | highlandgames.scot | highland-games.jpg | 1 |
| 6 | Stickleback Stuffing | indoor pursuits | Sticklebacks ar... | wales | stickleback.org | spinyfucker.jpg | 0 |
| 7 | squirrel suckling | historic pastimes | tree rats... | ireland | weirdness abounds | treerat.gif | 0 |
+----+----------------------+-------------------+--------------------+----------+-------------------------------+--------------------+--------+
*/
/*
utility functions to try to assist debugging
*/
function pre( $data, $header ){
$title = $header ? sprintf( '<h1>%s</h1>', $header ) : '';
printf( '<pre>%s%s</pre>', $title, print_r($data,true) );
}
function preview_mysqli( $sql, $args ){
$index=0;
for( $i=0; $i < strlen( $sql ); $i++ ){
$char = substr( $sql, $i, 1 );
if( $char == '?' ) {
$param = $args[ $index ];
if( !strstr( $param, '"' ) )$param=sprintf( '"%s"', $param );
$sql = substr_replace( $sql, $param, $i, 1 );
$index++;
}
}
return preg_replace( '#(\t)#', ' ', $sql );
}
function varname( $var ){
foreach( $GLOBALS as $name => $value )if( $value === $var ) return $name;
return false;
}
/*
This writes out the equivilent php code one would use
to run the query manually without generating on the fly.
This produces different results but is, I'm sure, essentially
the same query being executed.
*/
function preview_mysqli_statement( $sql, $args ){
$types=array();
foreach( $args as $arg )$types[]=type( $arg );
$vars=array();
foreach( $args as $arg )$vars[]=sprintf( '$%s', varname( $arg ) );
return preg_replace('#\t#',' ',sprintf('
$sql="%s";
$stmt=$db->prepare( $sql );
$stmt->bind_param( "%s", %s );
$res=$stmt->execute();
if( $res ){
$res=$stmt->get_result();
while( $rs=$res->fetch_object() ){
pre( $rs, "-- record --" );
}
}',
$sql,
implode( '', $types ),
implode( ', ', $vars )
));
}
/*
utility to return string for the variable type
used when creating the statement binding
*/
function type($arg){
switch( gettype( $arg ) ){
case 'integer':
case 'int': return 'i';
case 'float':
case 'double': return 'd';
default: return 's';
}
}
/* standard db connection */
$dbhost = '127.0.0.1';
$dbuser = 'root';
$dbpwd = 'xxx';
$dbname = 'xxx';
$db = new mysqli( $dbhost, $dbuser, $dbpwd, $dbname );
/* parameters used to generate where clause. Comment out to negate and modify query */
$search_taskname='squirrel';
$search_industry='outdoor pursuits';
$search_country='scotland';
$search_orgname='association';
$search_description='folklore';
/* placeholders */
$where=array();
$args=array();
$types=array();
/* Clause joining verb : AND | OR */
$joiner='or';
/* create where clause conditions */
if( !empty( $search_taskname ) ){
$where[]='`taskname` like ?';
$search_taskname="%{$search_taskname}%";
$args[]=$search_taskname;
}
if( !empty( $search_industry ) ){
$where[]='`industryname`=?';
$args[]=$search_industry;
}
if( !empty( $search_country ) ){
$where[]='`country`=?';
$args[]=$search_country;
}
if( !empty( $search_orgname ) ){
$where[]='`orgname` like ?';
$search_orgname="%{$search_orgname}%";
$args[]=$search_orgname;
}
if( !empty( $search_description ) ){
$where[]='`description` like ?';
$search_description="%{$search_description}%";
$args[]=$search_description;
}
if( isset( $search_status ) ){
$where[]='`status`=?';
$args[]=$search_status;
}
if( count( $where )==0 )exit('bad foo: no "WHERE" clause parameters');
/* construct the full SQL statement with full where clauses */
$sql=sprintf('select id,taskname,industryname,country,orgname,photo,status
from `volopp`
%s;',
count( $where ) > 0 ? sprintf( 'where %s', implode( sprintf( ' %s ', $joiner ), $where ) ) : ''
);
$params=&$args;
$types=array();
foreach( $params as $param )$types[]=type( $param );
$types=implode( '', $types );
/* debug statements */
pre( preg_replace( '#\t#', ' ', $sql ), 'The generated sql statement to be passed to `prepare` method.' );
pre( preview_mysqli( $sql, $params ), 'Effective query: Can be run in mysql client' );
pre( preview_mysqli_statement( $sql, $params ), 'Generated php code to emulate the whole process. [copy]' );
$stmt=$db->prepare( $sql );
if( $stmt ){
$vars=array();
$vars[]=&$types;
foreach( $params as $param ) $vars[]=&$param;
$callback=array( $stmt, 'bind_param' );
call_user_func_array( $callback, $vars );
$result = $stmt->execute();
if( $result ){
$result=$stmt->get_result();
$i=1;
while( $rs=$result->fetch_object() ){
pre( $rs, sprintf(' --> Live record %d', $i ) );
$i++;
}
$stmt->free_result();
$stmt->close();
}
}
/*
if you copy the generated php code from debug output
and run... it should run and produce different
results.
*/
?>
The above screenshot shows 1 record returned when script is run with default parameters.
Running the generated PHP code version, as below, yields the same results as when run in the mysql client ( see above screenshot ).
<?php
$dbhost = '127.0.0.1';
$dbuser = 'root';
$dbpwd = 'xxx';
$dbname = 'xxx';
$db = new mysqli( $dbhost, $dbuser, $dbpwd, $dbname );
function pre( $data, $header ){
$title = $header ? sprintf( '<h1>%s</h1>', $header ) : '';
printf( '<pre>%s%s</pre>', $title, print_r($data,true) );
}
$search_taskname='squirrel';
$search_industry='outdoor pursuits';
$search_country='scotland';
$search_orgname='association';
$search_description='folklore';
/* clauses that use `like` */
$search_taskname="%{$search_taskname}%";
$search_orgname="%{$search_orgname}%";
$search_description="%{$search_description}%";
$sql="select id,taskname,industryname,country,orgname,photo,status
from `volopp`
where `taskname` like ? or `industryname`=? or `country`=? or `orgname` like ? or `description` like ?;";
$stmt=$db->prepare( $sql );
$stmt->bind_param( "sssss", $search_taskname, $search_industry, $search_country, $search_orgname, $search_description );
$res=$stmt->execute();
if( $res ){
$res=$stmt->get_result();
while( $rs=$res->fetch_object() ){
pre( $rs, "-- record --" );
}
}
?>
Sorry for the long post. If anyone can figure out what is going on I'd be happy to hear. No doubt I have missed something obvious and will feel stupid someone points out a glaring oops.
Your problem is in the segment...
if( $stmt ){
$vars=array();
$vars[]=&$types;
foreach( $params as $param ) $vars[]=&$param;
$callback=array( $stmt, 'bind_param' );
print_r($vars);
call_user_func_array( $callback, $vars );
The output of which is...
Array
(
[0] => sssss
[1] => %folklore%
[2] => %folklore%
[3] => %folklore%
[4] => %folklore%
[5] => %folklore%
)
To make this call easier, I would tend to use splat anyway...
if( $stmt ){
$stmt->bind_param($types, ...$params);
which gives you the output you want (I think).
Related
Extracting data through Wordpress database table
I am trying to extract data from a Wordpress database so I can get it into a new database, but having a few issues figuring things out. This is just a small sample of how some of the Wordpress tables are like. wp_posts id | post_type | post_status | ---------------------------------- 1 | portfolio | published | ... other data wp_postmeta id | post_id | meta_key | meta_value | ----------------------------------------------------------------------- 1 | 1 | item_submission_title | Some title | ... other data 2 | 1 | item_technology | a:1:{i:0;s:4:"2372";} | 3 | 1 | item_description | Some description | wp_terms id | term_id | name | -------------------------------- 1 | 2372 | Some name | ... other data So essentially, I have to get all id's from wp_posts where post_type is portfolio and post_status is publish. Using these id's I then need to get the meta_value for the 3 meta_key's shown in my wp_postmeta table. The item_technology meta_value is serialized, so I need to then unserialize it. I then need to get its id (2372) and use this to obtain further data from the wp_terms table. There is a lot more I need to do, but achieving the above will give me the understanding of how best to do this. I have a feeling I can somehow use joins here, but not sure. At the moment, my attempt is very ineffecient and incomplete. This is what I currently have. $conn = Database::getInstance(); $ids = getIDs($conn); $dataArray = array(); foreach ($ids as $row) { $data = getData($conn, $row['id']); $dataArray[] = $data; } function getIDs($conn) { $query = "SELECT `id` FROM `wp_posts` WHERE `post_type` = \"portfolio\" and `post_status` = \"publish\""; $sql = $conn->dbc->prepare($query); $sql->execute(); $row = $sql->fetchAll(); return $row; } function getData($conn, $id) { $query = "SELECT `meta_value` FROM `wp_postmeta` WHERE `post_id` = $id AND `meta_key` = \"item_submission_title\""; $sql = $conn->dbc->prepare($query); $sql->execute(); $row = $sql->fetchAll(); return $row; } What would be the best way to achieve what I am after? Thanks
Considering that you have a WordPress Database, I'll assume you also have a WordPress install attached to it. If not, you should, in order to make sure you get the data you want in the manner you want without having to recreate code that deals with the multitude of WP specific "idiosyncrasies". So the first thing you'll do is "bootstrap" WordPress so you can obtain the DB connection and all of the WordPress functionality in one east step. So let's say you create a file in the WordPress root directory... <?php // This includes gives us all the WordPress functionality require_once( dirname(__FILE__) . '/wp-load.php' ); // Set parameters to gather posts $args = array( 'posts_per_page' => -1, 'offset' => 0, 'orderby' => 'date', 'order' => 'DESC', 'post_type' => 'portfolio', 'post_status' => 'publish', 'suppress_filters' => true ); // Retrieve matching posts $posts_array = get_posts( $args ); // Loop through posts to get Meta values foreach ( $posts_array as $post ) { $item_submission_title = get_post_meta( $post->ID, 'item_submission_title', true ); $item_technology = maybe_unserialize( get_post_meta( $post->ID, 'item_technology', true ) ); $item_description = get_post_meta( $post->ID, 'item_description', true ); // Do something with this information ... } Of course, you might now need to get it into the other DB. WordPress can also accommodate that with a simple and powerful interface... $otherDB = new wpdb( 'username', 'password', 'database', 'localhost' ); $table = 'other_table'; $data = array( 'item_submission_title' => $item_submission_title, 'item_technology' => $item_technology, 'item_description' => $item_description, ); $otherDB->insert( $table, $data ); More functions of the WPDB class can be found here: https://codex.wordpress.org/Class_Reference/wpdb
how to update a text field in mysql
in my wordpress blog i have a table consist of users and their products seperated by ";" for example : mytable ---------------------- userid | products ---------------------- 1 | camera 2 | books;keyboard;computer 3 | mouse;printer 4 | scanner now in my script the user will add a new product so i need to check if that user has more than 3 products because in this case i will do nothing and will not add this product for him but if he has less than 3 products i need to add the new product to his products seperated by ";" something like : $userid = 3; $newproduct = "ball" if (pruduct field for that user has more than 2 ";" ) { do nothing }else{ $wpdb->update( 'mytable', array( 'product' => concat(product, ";", $newproduct)), array( 'userid ' => $userid ), array( '%s' ), array( '%d' ) ); so the result in this example would be : mytable ---------------------- userid | products ---------------------- 1 | camera 2 | books;keyboard;computer 3 | mouse;printer;ball 4 | scanner
Despite the data normalization arguments, which are incredibly valid, if for some reason you cannot correct that and must work with what exists. Select the products column as a string and use the PHP explode() and implode() functions. You'd end up with something like this: $current_products = explode(';', $user['products']); // $current_products is now an array of the user's products. if !(count($current_products) >= 3) { $current_products[] = $new_product; } $user_products = implode(';', $current_products); // Insert $user_products into your table via UPDATE The implode and explode functions convert strings to and from arrays based on given delimiters.
In WordPress, the right way to do this is with the user_meta table. With respect, your proposed way is not a good way. You can set these values into the user_meta table with code like this. $meta_key = '_my_products'; /* be sure to choose a unique key prefix */ add_user_meta( $userid, $meta_key, 'football', false ); add_user_meta( $userid, $meta_key, 'basketball', false ); You can get them back with code like this $vals = get_user_meta( $userid, $meta_key, false ); foreach ($vals as $value) { echo $i; } There are other functions for updating and deleting user_meta items. They work well. Read this. https://codex.wordpress.org/Function_Reference/get_user_meta
Wordpress Form php code submitting blank values
This code is supposed to check to see if a user is logged in when posting an input, if so, it then checks to see if the ItemID and User ID already have an entry in the table, and if so, it updates that entry rather than creating a duplicate. This is the output I need from it: +--------+-------+----------+ | ItemID | Price | user | +--------+-------+----------+ | 1 | 20 | 27 | +--------+-------+----------+ This is what I'm getting: +--------+-------+----------+ | ItemID | Price | user | +--------+-------+----------+ | 0 | 0 | 27 | +--------+-------+----------+ Here is the full function if needed : http://pastebin.com/W0UM68UT if ( is_user_logged_in() ) { $hf_user = get_current_user_id(); $hf_username = $hf_user->user_login; global $quanid; $inputValue = isset($_POST[$quanid]); global $newdb; $newdb = new wpdb( 'user', 'pass', 'db', 'localhost' ); $retval = $newdb->get_results("SELECT * FROM $table WHERE ItemID = '$quanid' AND user = '$hf_username' "); if($retval > 0) { //If user exists, update $newdb->replace($table,array( 'ItemID' => '$quanid', 'Price' => $inputValue, 'user' => $hf_username ) ); } else { global $table; global $newdb; $newdb->insert( $table, array( 'ItemID' => $quanid, 'Price' => $inputValue, 'user' => $hf_username ) ); } } else { global $error; $error = "Error: You must be logged in to submit prices"; return; } }
Please don't use globals... How to avoid using PHP global objects? Change your SELECT statement to a count for better performance: SELECT count( 1 ) FROM $table WHERE ItemID = '$quanid' AND user = '$hf_username' On to your question: It appears your global $quanid; and isset($_POST[$quanid]); return unexpected values so you should see where they're set. Try using: var_dump right below these two lines: global $quanid; $inputValue = isset($_POST[$quanid]); var_dump( $quanid ); var_dump( $inputValue ); var_dump( $_POST); die;
postgresql to_tsquery() with PDO's prepared statement
If write directly $sql = "... to_tsquery( 'word_1 | word_2' )"; Works, but how to use this syntax with PDO's prepared statement? What is right syntax for this: $sql = " ... to_tsquery( :word_1 | :word_2 ) "; //this is incorrect $sth = $db->prepare( $sql ); $sth->execute( array( ':word_1'=>"word_1", ':word_2'=>"word_2" ) );
Like so: denis=# select to_tsquery('(foo | bar) & baz'); to_tsquery --------------------------- ( 'foo' | 'bar' ) & 'baz' (1 row) denis=# select (to_tsquery('foo') || to_tsquery('bar')) && to_tsquery('baz'); ?column? --------------------------- ( 'foo' | 'bar' ) & 'baz' (1 row)
A general single sql query
I have a table like this: id | roll_no | name --------------------- 1 | 111 | Naveed 2 | 222 | Adil 3 | 333 | Ali If I have data like this: $fields = array( "id" , "roll_no" ) and $values = array( "1,111", "2,222" ); It means I have to write a sql query to get records from table where (id != 1 and roll_no != 111) and (id != 2 and roll_no != 222). It means 3rd record will be fetched. If I have data like this: $fields = array( "id" ) and $values = array( "2", "3" ); It means I have to write a sql query to get records from table where (id != 2) and (id != 3). It means 1st record will be fetched. Q: How to write a general single query using php to get data from table using above two data arrays. Thanks
select * from dummy where concat_ws (',', id, roll_no) not in ('1,111', '2,222') Complete solution: $tableName = "test"; $fields = array( "id" , "roll_no" ); $values = array( "1,111", "2,222" ); $fieldsStr = implode(',', $fields); $valuesStr = implode("','", $values); $sql = "SELECT * FROM $tableName WHERE concat_ws(',', $fieldsStr ) NOT IN ( '$valuesStr' )";
You will probably always have to explode the Array in PHP and pass the values as a string into the query (sprintf) so you probably can, and should, do all in PHP. One thing that catches my eye is that you are always using ID's. Are the ID's a unique or primary field? If so just forget about the roll_no as your query will be faster using just ID's.
Complete solution with the help of accepted answer. $tableName = "test"; $fields = array( "id" , "roll_no" ); $values = array( "1,111", "2,222" ); $fieldsStr = implode(',', $fields); $valuesStr = implode("','", $values); // Get all records from remote table $sql = "SELECT * FROM $tableName WHERE concat_ws(',', $fieldsStr ) NOT IN ( '$valuesStr' )";