I've included a DB-fiddle, and you can adjust the input parameter accordingly. This returns how I would expect it to, and differs from the results I am seeing in PDO.
I have the following minified table-view and query:
CREATE TABLE `tagged` {
`tag` SMALLINT(5) UNSIGNED NOT NULL
}
Table has an assortment of values, but you can use 1-10 for tags in the DB:
INSERT INTO tagged (tag) VALUES (1), (2), (3), (4), (5), (6), (7), (8), (9), (10)
query:
SELECT tagged.tag,
(#t := :tag),
#t AS temp_var,
(#t IS NULL OR FIND_IN_SET(tagged.tag, #t) > 0) AS is_match
FROM tagged
HAVING is_match = 1
LIMIT 150
This seems well and good when run in a client, command line, jdbc, etc. If I put in an input of '' or NULL, I get all results. Similarly an input of '1' yields only tags of 1, and an input of '1,4' would retrieve all tags with 1 or 4.
The way the query restricts these results is via the is_match = 1 in the HAVING clause. When run with PDO, the parameter seems to bind correctly but it completely ignores the condition in the clause:
Array
(
[0] => stdClass Object
(
[tag] => 3
[(#t := ?)] => 1,4
[temp_var] => 1,4
[is_match] => 0 ## should not have been returned
)
[1] => stdClass Object
(
[tag] => 4
[(#t := ?)] => 1,4
[temp_var] => 1,4
[is_match] => 1
)
PHP code used to run this (simplified):
$conn = /* pdo connection object */;
$stmt = $conn->prepare(DB::queryOf('test')); //uses our above query from a file
$stmt->bindValue(':tag', $args['tag'], PDO::PARAM_STR); //hardcode binding '1,4'
$stmt->execute(); //also tried plain #execute($args)
return $stmt->fetchAll(PDO::FETCH_OBJ);
Is there something I'm missing? I am binding a direct string parameter, and it seems the temporary variable is there and set correctly. Why is PDO returning the results for elements where is_match = 0?
I believe this behavior is dependent on the RDBMS being used.
In the absence of the GROUP BY clause, it seems that in some circumstances, the entire result can be considered as "one group". Because one row in the results satisfies the HAVING condition, all shall pass.
Additional reading:
Use of HAVING without GROUP BY in SQL queries
HAVING without GROUP BY
p.s. I don't think the > 0 is necessary.
I suppose I'd write your query something like this:
SELECT tag,
#t := '1,4' AS temp_var,
1 AS is_match
FROM tagged
WHERE #t IS NULL OR FIND_IN_SET(tag, #t)
LIMIT 150;
Related
I have a very simple query, not sure what I am doing wrong here.
My DB call is not receiving an insert id as I would expect it to.
Table:
Stored Procedure:
CREATE DEFINER=`root`#`localhost` PROCEDURE `addCustomerProduct`(IN in_customerID INT, in_productID INT)
BEGIN
INSERT INTO order_customer_product (customerID, productID, retailAmountAtPurchase, faceValue)
SELECT
in_customerID,
in_productID,
p.retail,
p.faceValue
FROM
products as p
WHERE
p.productID = in_productID;
END
PHP:
public function addProduct($data, $userID)
{
// Do we already have a pending order for this user?
$orderID = $this->doesOrderExist($userID);
// We had no order, lets create one
if (!$orderID) {
$orderID = $this->createOrder($userID);
}
/**
* Insert the customer product.
* This relates a denomination to a customer.
*/
$customerProductID = $this->addCustomerProduct($data);
// Add this customer product to the order
$this->addProductToOrder(array("customerProductID" => $customerProductID, "orderID" => $orderID));
// Return
return $customerProductID;
}
/**
* Description: Add a customer product / reward
* Page: client/add_reward
*/
public function addCustomerProduct($data){
$procedure = "CALL addCustomerProduct(?,?)";
$result = $this->db->query($procedure, $data);
return $this->db->insert_id();
}
The line with the issue is: $customerProductID = $this->addCustomerProduct($data);.
A new record is being inserted into the table and the table has a PK/AI. Data goes in fine but 0 is returned as the $customerProductID.
Will an insert from select statement not return an insert ID perhaps?
Update For #Ravi-
Update 2:
I created a separate method and hard coded the query and data being sent.
It adds the records fine, AI goes up, 0 is returned as the last id.
public function test(){
$procedure = "CALL addCustomerProduct(?,?)";
$result = $this->db->query($procedure, array("customerID" => 1, "productID" => 20));
echo $this->db->insert_id();
}
Also restarted the MySQL server to make sure there wasn't anything weird going on there.
Also, updated the SP to just insert random data into the table without using a select.
CREATE DEFINER=`root`#`localhost` PROCEDURE `addCustomerProduct`(IN in_customerID INT, in_productID INT)
BEGIN
INSERT INTO order_customer_product (customerID, productID, retailAmountAtPurchase, faceValue)
VALUES(8,2,'4.55',25);
END
Update 3:
Right after the insert, I am printing out the last query that was ran as well as the result. You will notice that there is 1 affected row (the insert is happening) but the insert_id is still 0.
CALL addCustomerProduct('8','33')
CI_DB_mysqli_result Object
(
[conn_id] => mysqli Object
(
[affected_rows] => 1
[client_info] => mysqlnd 5.0.12-dev - 20150407 - $Id: b396954eeb2d1d9ed7902b8bae237b287f21ad9e $
[client_version] => 50012
[connect_errno] => 0
[connect_error] =>
[errno] => 0
[error] =>
[error_list] => Array
(
)
[field_count] => 0
[host_info] => Localhost via UNIX socket
[info] =>
[insert_id] => 0
[server_info] => 5.6.35
[server_version] => 50635
[stat] => Uptime: 1637 Threads: 3 Questions: 508 Slow queries: 0 Opens: 113 Flush tables: 1 Open tables: 106 Queries per second avg: 0.310
[sqlstate] => 00000
[protocol_version] => 10
[thread_id] => 25
[warning_count] => 0
)
[result_id] => 1
[result_array] => Array
(
)
[result_object] => Array
(
)
[custom_result_object] => Array
(
)
[current_row] => 0
[num_rows] =>
[row_data] =>
)
Update 4:
From some of the research I have done, unless you use the mysqli method such as $this->db->insert(), it won't provide a last insert id back to you.
I am going to try and figure out Ravi's suggestion but it seems that code igniter doesn't allow the example that was shown. At least I know now that I am not crazy and its just not normal behavior unless you use the ``insert` method vs a stored procedure.
This answer may explain why your existing code doesn't work. To quote:
CodeIgniter's insert_id() will only return an ID of an insert().
Unless you are executing something like $this->db->insert('table', $data); before calling the function it will not be able to return an ID.
MySQL's LAST_INSERT_ID(); should help you here (assuming you have permission to alter the stored procedure definition). Change it to:
CREATE DEFINER=`root`#`localhost` PROCEDURE `addCustomerProduct`(
IN in_customerID INT, in_productID INT, OUT out_customerProductID INT)
BEGIN
INSERT INTO order_customer_product (
customerID, productID, retailAmountAtPurchase, faceValue)
VALUES(8,2,'4.55',25);
SELECT LAST_INSERT_ID() INTO out_customerProductID;
END
Then use something like the following to get the output parameter value:
public function addCustomerProduct($data) {
$procedure = "CALL addCustomerProduct("
. $this->db->escape($data["customerID"]).", "
. $this->db->escape($data["productID"]).", "
. "#customerProductID);"
$this->db->query($procedure);
$query = $this->db->query("SELECT #customerProductID AS customerProductID");
if($query->num_rows() > 0)
return $query->result()->customerProductID;
else
return NULL;
}
If the above doesn't work, try adding a $this->db->trans_start(); and $this->db->trans_complete(); before and after the stored procedure call to ensure the transaction is committed.
Ideally, following line should work
$this->db->insert_id;
But, I'm not sure why is not working, so I would suggest a workaround as following, recompile your procedure with additional parameter out_lastId, which will return last inserted id
CREATE DEFINER=`root`#`localhost` PROCEDURE `addCustomerProduct`(IN in_customerID INT, in_productID INT, OUT out_lastId INT)
And, after insert set the value with last inserted id.
SET out_lastId = LAST_INSERT_ID();
==Updated==
$this->db->multi_query( "CALL addCustomerProduct($data, #id);SELECT #id as id" );
$db->next_result(); // flush the null RS from the call
$rs=$this->db->store_result(); // get the RS containing the id
echo $rs->fetch_object()->id, "\n";
$rs->free();
Why
insert_id() will only workes with Query Builder and Queries only. SP's are used to call with $this->db->query() but it won't retuns data insert_id().
How
Before the End of SP add SELECT MAX(id) FROM order_customer_product;. So this will return the last ID to your code.
Suggestion
As I see there is an Insert query to DB. If I use similar case will use normal Query Builder or/and will warp it with Codeigniter Transactions(My answer on another question).
What is the best way to merge two mysqli query's as one query
TABLE SERVER_JOINS
ID DEFAULT SERVER_ID MEMBER_ID
---------------------------------------
1 0 1 57
2 0 52 57
3 0 22 57
4 1 35 57
Only one row must have default as 1
By clicking on a link i want to change the default value
mysqli_query($database->connection,"UPDATE `server_joins` SET
`default` = '0' WHERE `default` = '1' AND `member_id` = '$session->u_id'");
mysqli_query($database->connection,"UPDATE `server_joins` SET
`default` = '1' WHERE `server_id`= '$id' AND `member_id` = '$session->u_id'");
I think you want to schieve something like that:
mysqli_query($database->connection,"UPDATE `server_joins`
SET `default` = '1'
WHERE `default`= '0' AND `server_id`= '$id' AND `member_id` = '$session->u_id'");
Use an if() function or a conditional case expression in the set clause depending on the value of the server field to decide whether default is to be set to 0 or 1.
UPDATE `server_joins`
SET `default` = if(`server_id`= $id, 1, 0)
WHERE `member_id` = $session->u_id
Couple of notes:
Your code is likely to be vulnerable to sql injection attack. Consider using prepared statements with parameters.
If you have numeric values in a field, then do not pass the values as string. Mysql has to convert the values on the flight.
I have a stored procedure that I am trying to call from my php. Here is the stored procedure:
BEGIN
DECLARE done INT DEFAULT FALSE;
declare phone_temp VARCHAR(20) default '';
declare phone_cur cursor for SELECT DISTINCT sentNum FROM Queue;
declare continue handler for not found set done = true;
#create temp table
create temporary table if not exists temp_return AS SELECT * FROM Queue LIMIT 0;
#empty if exists
delete from temp_return;
open phone_cur;
phone_loop: LOOP
fetch phone_cur into phone_temp;
if done then
leave phone_loop;
end if;
insert into temp_return SELECT * FROM Queue WHERE num2=phone_temp LIMIT 2;
insert into temp_return SELECT * FROM Queue WHERE num1=phone_temp LIMIT 1;
end loop phone_loop;
close phone_cur;
select * from temp_return;
drop table if exists temp_return;
END
Directly in mysql workbench, calling it works. In php, it does not work. Here is my php:
function grabFromSmsQueue(){
global $myStmt, $conn;
if(isset($myStmt)){
$myStmt -> execute();
}
else{
$query = "CALL myStoredProc();";
$myStmt = $conn->stmt_init();
$myStmt -> prepare($query);
$myStmt -> execute();
}
$result = $myStmt -> get_result();
//print_r ($result);
$info = [];
if(isset($result)){
while($data = $result->fetch_assoc()){
$info[] = $data;
}
}
return $info;
}
Connecting like this, I get the following error
The localhost page isn’t working
localhost didn’t send any data.
ERR_EMPTY_RESPONSE
I traced my problem back to an issue with $data = $result->fetch_assoc(), because when I comment that out and put in the print_r I get something actually returned, which is mysqli_result Object ( [current_field] => 0 [field_count] => 9 [lengths] => [num_rows] => 0 [type] => 1 ). I have drawn the conclusion that it is not working because [num_rows] => 0.
Now, going back to my stored procedure, I took out all mentions of a cursor and replaced it with a hard-coded value, and it worked in both workbench and php. I have already verified that the user connecting through php has permission, that the connection is open, and that the same code can execute other stored procedures (ones that do not include cursors). Does this mean that I can not use cursors in stored procedures that are being called by php? Are there alternatives to cursors? Am I missing something in my php syntax to deal with cursors?
Based on discussions in chat for 3 groupings, and this provided SQLFiddle for test data (not much data there).
Due to testing data with a sliding window of where now() is in relation to that data, the following variable was used to "freeze" now(). Simply to facilitate testing and verification of output.
So, ditch that ultimately and change the 4 references in the code that use it (note that Group 3 uses it twice).
The now() variable:
select #theNow:=now();
-- REM OUT the following line. It is used only for testing (as now will chg, your data won't)
select #theNow:='2016-06-23 14:00:00';
The Query:
select id,sentNum,message,sentTime,startAtTime,sentByTime,msgType,theGrp from
( select id,sentNum,message,sentTime,startAtTime,sentByTime,msgType,theGrp,
if(sentNum!=#lastSentNum,greatest(#sentNumChg:=1,0),least(#sentNumChg:=0,1)) as dummy1,
if(theGrp!=#lastGrp,greatest(#grpChg:=1,0),least(#grpChg:=0,1)) as dummy2,
if(#sentNumChg=1 or #grpChg=1,#seqNum:=1,#seqNum:=#seqNum+1) as seqNum,
#lastSentNum:=sentNum as setLast01,
#lastGrp:=theGrp as setLast02
from
( -- GROUP 1: sentByTime<=now(), INVITE
select `id`, `sentNum`, `message`, `sentTime`, `startAtTime`, `sentByTime`, `msgType`, 1 as theGrp
from SmsQueue
where sentByTime<=#theNow and msgType='invite'
UNION ALL
-- GROUP 2 startAtTime<=now(), BROADCAST
select `id`, `sentNum`, `message`, `sentTime`, `startAtTime`, `sentByTime`, `msgType`, 2 as theGrp
from SmsQueue
where startAtTime<=#theNow and msgType='broadcast'
UNION ALL
-- GROUP 3: sentByTime>now() && startAtTime<=now(), INVITE
select `id`, `sentNum`, `message`, `sentTime`, `startAtTime`, `sentByTime`, `msgType`, 3 as theGrp
from SmsQueue
where sentByTime>#theNow and startAtTime<=#theNow and msgType='invite'
) d1
cross join (select #sentNumChg:=0,#grpChg:=0,#lastSentNum:='',#lastGrp:=0,#seqNum:=0) as xParams
order by sentNum,theGrp,sentByTime,id -- id is the tie-break
) d2
where (theGrp=1 and seqNum<3) or (theGrp=2 and seqNum=1) or (theGrp=3 and seqNum=1)
order by sentNum,theGrp;
Output (my client tool is text challenged at the moment):
See my general comments at the top of this answer of mine for advanced variable usage.
I have a MSSQL table users:
CREATE TABLE users (
ID int IDENTITY(1,1) NOT NULL,
firstname nvarchar(20) NOT NULL,
lastname nvarchar(20) NOT NULL,
dir bit NOT NULL,
cc nvarchar(15),
readyacc bit NOT NULL,
region nvarchar(50),
org nvarchar(50),
suborg nvarchar(50),
section nvarchar(50),
title nvarchar(50),
floor tinyint,
wkstn nvarchar(50),
fc nvarchar(15)
);
And I'm trying to update an existing entry with the prepared query:
UPDATE users SET ? = ? WHERE ID=?;
With my parameters as:
Array ( [0] => title [1] => Teleco [2] => 1 )
But it seems as though if the string length is greater than 5 it gives me the error "String or binary data would be truncated.". Eg, Telec works but Teleco does not. When I try the same query in the SQL Management Studio it gives me no errors.
Am I just missing something obvious? Please help
Seeing someone else decided to post an answer, transcribing my comment to an answer.
That's just it, you can't bind tables/columns with UPDATE users SET ? = ? WHERE ID=?;.
Use a safelist if anything.
You can do this though $var="x"; by assigning a pre-defined variable "ahead of time".
Then doing UPDATE users SET '$var' = ? WHERE ID=?;
You see, tables/columns require a hard coded value or a "lookahead" in order to know what it's supposed to use as far as table/column names go, before binding begins. This all happens "after" the query, therefore a "lookahead" can be in the form of a variable.
FYI: The same applies for MySQL and is not specific to MSSQL.
Your query, when the parameters are passed in, it equivalent to this:
UPDATE users SET 'title' = 'Teleco' WHERE ID='1';
Which would not work if you tried running it in the management studio. The error message you're getting is erroneous. As the comment by Fred says, you need to have a safe(white) list of columns that can be updated:
$safe_cols = ['dir', 'cc', 'title'];
if(in_array($col, $safe_cols, true))
{
$stmt = $db->prepare('UPDATE users SET ' . $col . ' = ? WHERE id = ?');
// bind params and execute
}
I would like to perform a find('all') query on a table in a CakePHP application with the following conditions:
1- The primary key value is equal 17
AND
2- The value of another column my_column_id in the table is the MAX of all the values of the column my_column_id but less than the value of my_column_id of the datum whose primary key value is equal to 17
AND
3- The value of another column my_column_id in the table is the MIN of all the values of the column my_column_id but greater than the value of my_column_id of the datum whose primary key value is equal to 17
So the above query should give me 3 results.
Is it possible to get all this in one single query? or do I need three separate queries?
I tried something like:
$results = $this->Model->find('all', array(
'conditions' => array(
'MAX(Model.my_column_id) <' => 23,
'Mddel.id' => 17,
'MIN(Model.my_column_id) >' => 23
)
));
23 represents the value of my_column_id of the datum whose primary key is equal to 17
But that gives me the following error: SQL Error: 1111: Invalid use of group function
Any help please.
Often times when I have complex queries I'll just write out the SQL. I can't speak for the speed and I'm not sure if it's frowned upon, but you can try giving this a shot.
$this->Model->query('
SELECT MAX(table.column),
MIN(table.column)
FROM table
WHERE table.id = '$id'
');
Also, if this controller is outside of the model make sure to load the correct model (place it above the query)
$this->loadModel('Model');
You can't use MySQL aggregate functions (MAX, MIN etc) in your where clause.
I think the easiest way will be to explicitly include the GROUP BY and use HAVING, something like this:
$this->Model->find('all', array(
'group' => 'Model.some_field HAVING MAX(Model.some_field) > 23
AND MIN(some_field) < 23'
));
(although that doesn't make sense, I don't think!)*
That's something like what you want.
Toby
Update
[*] By which I mean, I don't think it makes sense to be querying both greater than and less than a value!
You can do it by passing conditions array inside find.
Example
$this->Model->find('all',array
(
'conditions' => array
(
'Model.field' => 17,
'MAX(Model.field) < ' => 17,
'MIN(Model.field) > ' => 17
)
))
OP Comment Response
SQL Error: 1305: FUNCTION NAX does not exist
Solution: Remove all spaces between the function and the parenthesis or set sql_mode="IGNORE_SPACE";
This is causing the error:
SELECT MAX (id) as something FROM Example
This will work correctly:
SELECT MAX(id) as something FROM Example