Commit dc11d3cf authored by konstantin@mysql.com's avatar konstantin@mysql.com

Proposed fix for Bug#4026 "Microseconds part of TIME/DATETIME types

is broken (prepared statements)": fixed date handling in many places 
of prepared statements code.
parent dabc0e77
......@@ -1667,6 +1667,27 @@ static int stmt_read_row_buffered(MYSQL_STMT *stmt, unsigned char **row);
static int stmt_read_row_no_data(MYSQL_STMT *stmt, unsigned char **row);
/*
Maximum sizes of MYSQL_TYPE_DATE, MYSQL_TYPE_TIME, MYSQL_TYPE_DATETIME
values stored in network buffer.
*/
/* 1 (length) + 2 (year) + 1 (month) + 1 (day) */
static const unsigned MAX_DATE_REP_LENGTH= 5;
/*
1 (length) + 1 (is negative) + 4 (day count) + 1 (hour)
+ 1 (minute) + 1 (seconds) + 4 (microseconds)
*/
static const unsigned MAX_TIME_REP_LENGTH= 13;
/*
1 (length) + 2 (year) + 1 (month) + 1 (day) +
1 (hour) + 1 (minute) + 1 (second) + 4 (microseconds)
*/
static const unsigned MAX_DATETIME_REP_LENGTH= 12;
/**************** Misc utility functions ****************************/
/*
......@@ -2030,6 +2051,30 @@ static unsigned int alloc_stmt_fields(MYSQL_STMT *stmt)
return stmt->field_count;
}
/*
Update result set columns metadata if it was sent again in
reply to COM_EXECUTE.
*/
static void update_stmt_fields(MYSQL_STMT *stmt)
{
MYSQL_FIELD *field= stmt->mysql->fields;
MYSQL_FIELD *field_end= field + stmt->field_count;
MYSQL_FIELD *stmt_field= stmt->fields;
DBUG_ASSERT(stmt->field_count == stmt->mysql->field_count);
for (; field < field_end; ++field, ++stmt_field)
{
stmt_field->charsetnr= field->charsetnr;
stmt_field->length = field->length;
stmt_field->type = field->type;
stmt_field->flags = field->flags;
stmt_field->decimals = field->decimals;
}
}
/*
Returns prepared statement metadata in the form of a result set.
......@@ -2166,7 +2211,7 @@ static void store_param_double(NET *net, MYSQL_BIND *param)
static void store_param_time(NET *net, MYSQL_BIND *param)
{
MYSQL_TIME *tm= (MYSQL_TIME *) param->buffer;
char buff[15], *pos;
char buff[MAX_TIME_REP_LENGTH], *pos;
uint length;
pos= buff+1;
......@@ -2177,19 +2222,19 @@ static void store_param_time(NET *net, MYSQL_BIND *param)
pos[7]= (uchar) tm->second;
int4store(pos+8, tm->second_part);
if (tm->second_part)
length= 11;
length= 12;
else if (tm->hour || tm->minute || tm->second || tm->day)
length= 8;
else
length= 0;
buff[0]= (char) length++;
buff[0]= (char) length++;
memcpy((char *)net->write_pos, buff, length);
net->write_pos+= length;
}
static void net_store_datetime(NET *net, MYSQL_TIME *tm)
{
char buff[12], *pos;
char buff[MAX_DATETIME_REP_LENGTH], *pos;
uint length;
pos= buff+1;
......@@ -2280,7 +2325,7 @@ static my_bool store_param(MYSQL_STMT *stmt, MYSQL_BIND *param)
Param->length should ALWAYS point to the correct length for the type
Either to the length pointer given by the user or param->buffer_length
*/
if ((my_realloc_str(net, 9 + *param->length)))
if ((my_realloc_str(net, *param->length)))
{
set_stmt_error(stmt, CR_OUT_OF_MEMORY, unknown_sqlstate);
DBUG_RETURN(1);
......@@ -2557,16 +2602,37 @@ int STDCALL mysql_stmt_execute(MYSQL_STMT *stmt)
*/
if (mysql->methods->stmt_execute(stmt))
DBUG_RETURN(1);
if (!stmt->field_count && mysql->field_count)
if (mysql->field_count)
{
/*
This is 'SHOW'/'EXPLAIN'-like query. Current implementation of
prepared statements can't send result set metadata for this queries
on prepare stage. Read it now.
*/
alloc_stmt_fields(stmt);
/* Server has sent result set metadata */
if (stmt->field_count == 0)
{
/*
This is 'SHOW'/'EXPLAIN'-like query. Current implementation of
prepared statements can't send result set metadata for these queries
on prepare stage. Read it now.
*/
alloc_stmt_fields(stmt);
}
else
{
/*
Update result set metadata if it for some reason changed between
prepare and execute, i.e.:
- in case of 'SELECT ?' we don't know column type unless data was
supplied to mysql_stmt_execute, so updated column type is sent
now.
- if data dictionary changed between prepare and execute, for
example a table used in the query was altered.
Note, that now (4.1.3) we always send metadata in reply to
COM_EXECUTE (even if it is not necessary), so either this or
previous always branch works.
TODO: send metadata only when it's really necessary and add a warning
'Metadata changed' when it's sent twice.
*/
update_stmt_fields(stmt);
}
}
stmt->state= MYSQL_STMT_EXECUTE_DONE;
if (stmt->field_count)
{
......@@ -2693,15 +2759,17 @@ my_bool STDCALL mysql_stmt_bind_param(MYSQL_STMT *stmt, MYSQL_BIND * bind)
param->store_param_func= store_param_double;
break;
case MYSQL_TYPE_TIME:
/* Buffer length ignored for DATE, TIME and DATETIME */
param->store_param_func= store_param_time;
param->buffer_length= MAX_TIME_REP_LENGTH;
break;
case MYSQL_TYPE_DATE:
param->store_param_func= store_param_date;
param->buffer_length= MAX_DATE_REP_LENGTH;
break;
case MYSQL_TYPE_DATETIME:
case MYSQL_TYPE_TIMESTAMP:
param->store_param_func= store_param_datetime;
param->buffer_length= MAX_DATETIME_REP_LENGTH;
break;
case MYSQL_TYPE_TINY_BLOB:
case MYSQL_TYPE_MEDIUM_BLOB:
......@@ -2859,17 +2927,17 @@ static uint read_binary_time(MYSQL_TIME *tm, uchar **pos)
set_zero_time(tm);
return 0;
}
to= *pos;
tm->second_part= (length > 8 ) ? (ulong) sint4korr(to+7): 0;
to= *pos;
tm->neg= (bool) to[0];
tm->day= (ulong) sint4korr(to+1);
tm->hour= (uint) to[5];
tm->minute= (uint) to[6];
tm->second= (uint) to[7];
tm->second_part= (length > 8) ? (ulong) sint4korr(to+8) : 0;
tm->year= tm->month= 0;
tm->neg= (bool)to[0];
return length;
}
......@@ -2878,16 +2946,20 @@ static uint read_binary_datetime(MYSQL_TIME *tm, uchar **pos)
{
uchar *to;
uint length;
if (!(length= net_field_length(pos)))
{
set_zero_time(tm);
return 0;
}
to= *pos;
tm->second_part= (length > 7 ) ? (ulong) sint4korr(to+7): 0;
to= *pos;
tm->neg= 0;
tm->year= (uint) sint2korr(to);
tm->month= (uint) to[2];
tm->day= (uint) to[3];
if (length > 4)
{
tm->hour= (uint) to[4];
......@@ -2896,11 +2968,7 @@ static uint read_binary_datetime(MYSQL_TIME *tm, uchar **pos)
}
else
tm->hour= tm->minute= tm->second= 0;
tm->year= (uint) sint2korr(to);
tm->month= (uint) to[2];
tm->day= (uint) to[3];
tm->neg= 0;
tm->second_part= (length > 7) ? (ulong) sint4korr(to+7) : 0;
return length;
}
......@@ -3159,7 +3227,7 @@ static void send_data_time(MYSQL_BIND *param, MYSQL_TIME ltime,
}
}
}
/* Fetch data to buffers */
......
......@@ -629,6 +629,7 @@ Item_param::Item_param(unsigned pos_in_query_arg) :
state(NO_VALUE),
item_result_type(STRING_RESULT),
item_type(STRING_ITEM),
param_type(MYSQL_TYPE_STRING),
pos_in_query(pos_in_query_arg),
set_param_func(default_set_param_func)
{
......@@ -808,6 +809,17 @@ bool Item_param::get_time(TIME *res)
}
bool Item_param::get_date(TIME *res, uint fuzzydate)
{
if (state == TIME_VALUE)
{
*res= value.time;
return 0;
}
return Item::get_date(res, fuzzydate);
}
double Item_param::val()
{
switch (state) {
......
......@@ -465,6 +465,16 @@ public:
/* Cached values for virtual methods to save us one switch. */
enum Item_result item_result_type;
enum Type item_type;
/*
Used when this item is used in a temporary table.
This is NOT placeholder metadata sent to client, as this value
is assigned after sending metadata (in setup_one_conversion_function).
For example in case of 'SELECT ?' you'll get MYSQL_TYPE_STRING both
in result set and placeholders metadata, no matter what type you will
supply for this placeholder in mysql_stmt_execute.
*/
enum enum_field_types param_type;
/*
Offset of placeholder inside statement text. Used to create
no-placeholders version of this statement for the binary log.
......@@ -475,12 +485,13 @@ public:
enum Item_result result_type () const { return item_result_type; }
enum Type type() const { return item_type; }
enum_field_types field_type() const { return MYSQL_TYPE_STRING; }
enum_field_types field_type() const { return param_type; }
double val();
longlong val_int();
String *val_str(String*);
bool get_time(TIME *tm);
bool get_date(TIME *tm, uint fuzzydate);
int save_in_field(Field *field, bool no_conversions);
void set_null();
......
......@@ -26,6 +26,8 @@
#include "mysql_priv.h"
#include <stdarg.h>
static const unsigned int PACKET_BUFFER_EXTRA_ALLOC= 1024;
#ifndef EMBEDDED_LIBRARY
bool Protocol::net_store_data(const char *from, uint length)
#else
......@@ -687,7 +689,7 @@ bool Protocol_simple::store_null()
#endif
char buff[1];
buff[0]= (char)251;
return packet->append(buff, sizeof(buff), PACKET_BUFFET_EXTRA_ALLOC);
return packet->append(buff, sizeof(buff), PACKET_BUFFER_EXTRA_ALLOC);
}
#endif
......@@ -990,7 +992,7 @@ bool Protocol_prep::store_tiny(longlong from)
char buff[1];
field_pos++;
buff[0]= (uchar) from;
return packet->append(buff, sizeof(buff), PACKET_BUFFET_EXTRA_ALLOC);
return packet->append(buff, sizeof(buff), PACKET_BUFFER_EXTRA_ALLOC);
}
......@@ -1002,7 +1004,7 @@ bool Protocol_prep::store_short(longlong from)
field_types[field_pos] == MYSQL_TYPE_YEAR);
#endif
field_pos++;
char *to= packet->prep_append(2, PACKET_BUFFET_EXTRA_ALLOC);
char *to= packet->prep_append(2, PACKET_BUFFER_EXTRA_ALLOC);
if (!to)
return 1;
int2store(to, (int) from);
......@@ -1018,7 +1020,7 @@ bool Protocol_prep::store_long(longlong from)
field_types[field_pos] == MYSQL_TYPE_LONG);
#endif
field_pos++;
char *to= packet->prep_append(4, PACKET_BUFFET_EXTRA_ALLOC);
char *to= packet->prep_append(4, PACKET_BUFFER_EXTRA_ALLOC);
if (!to)
return 1;
int4store(to, from);
......@@ -1033,7 +1035,7 @@ bool Protocol_prep::store_longlong(longlong from, bool unsigned_flag)
field_types[field_pos] == MYSQL_TYPE_LONGLONG);
#endif
field_pos++;
char *to= packet->prep_append(8, PACKET_BUFFET_EXTRA_ALLOC);
char *to= packet->prep_append(8, PACKET_BUFFER_EXTRA_ALLOC);
if (!to)
return 1;
int8store(to, from);
......@@ -1048,7 +1050,7 @@ bool Protocol_prep::store(float from, uint32 decimals, String *buffer)
field_types[field_pos] == MYSQL_TYPE_FLOAT);
#endif
field_pos++;
char *to= packet->prep_append(4, PACKET_BUFFET_EXTRA_ALLOC);
char *to= packet->prep_append(4, PACKET_BUFFER_EXTRA_ALLOC);
if (!to)
return 1;
float4store(to, from);
......@@ -1063,7 +1065,7 @@ bool Protocol_prep::store(double from, uint32 decimals, String *buffer)
field_types[field_pos] == MYSQL_TYPE_DOUBLE);
#endif
field_pos++;
char *to= packet->prep_append(8, PACKET_BUFFET_EXTRA_ALLOC);
char *to= packet->prep_append(8, PACKET_BUFFER_EXTRA_ALLOC);
if (!to)
return 1;
float8store(to, from);
......@@ -1112,7 +1114,7 @@ bool Protocol_prep::store(TIME *tm)
else
length=0;
buff[0]=(char) length; // Length is stored first
return packet->append(buff, length+1, PACKET_BUFFET_EXTRA_ALLOC);
return packet->append(buff, length+1, PACKET_BUFFER_EXTRA_ALLOC);
}
bool Protocol_prep::store_date(TIME *tm)
......@@ -1129,7 +1131,7 @@ bool Protocol_prep::store_time(TIME *tm)
DBUG_ASSERT(field_types == 0 ||
field_types[field_pos] == MYSQL_TYPE_TIME);
#endif
char buff[15],*pos;
char buff[13], *pos;
uint length;
field_pos++;
pos= buff+1;
......@@ -1140,13 +1142,13 @@ bool Protocol_prep::store_time(TIME *tm)
pos[7]= (uchar) tm->second;
int4store(pos+8, tm->second_part);
if (tm->second_part)
length=11;
length=12;
else if (tm->hour || tm->minute || tm->second || tm->day)
length=8;
else
length=0;
buff[0]=(char) length; // Length is stored first
return packet->append(buff, length+1, PACKET_BUFFET_EXTRA_ALLOC);
return packet->append(buff, length+1, PACKET_BUFFER_EXTRA_ALLOC);
}
#ifdef EMBEDDED_LIBRARY
......
......@@ -18,7 +18,6 @@
#pragma interface /* gcc class implementation */
#endif
#define PACKET_BUFFET_EXTRA_ALLOC 1024
class i_string;
class THD;
......
......@@ -332,20 +332,19 @@ static void set_param_time(Item_param *param, uchar **pos, ulong len)
{
uchar *to= *pos;
TIME tm;
/* TODO: why length is compared with 8 here? */
tm.second_part= (length > 8 ) ? (ulong) sint4korr(to+7): 0;
tm.neg= (bool) to[0];
day= (uint) sint4korr(to+1);
/*
Note, that though ranges of hour, minute and second are not checked
here we rely on them being < 256: otherwise
we'll get buffer overflow in make_{date,time} functions,
which are called when time value is converted to string.
*/
day= (uint) sint4korr(to+1);
tm.hour= (uint) to[5] + day * 24;
tm.minute= (uint) to[6];
tm.second= (uint) to[7];
tm.second_part= (length > 8) ? (ulong) sint4korr(to+8) : 0;
if (tm.hour > 838)
{
/* TODO: add warning 'Data truncated' here */
......@@ -354,7 +353,6 @@ static void set_param_time(Item_param *param, uchar **pos, ulong len)
tm.second= 59;
}
tm.day= tm.year= tm.month= 0;
tm.neg= (bool)to[0];
param->set_time(&tm, TIMESTAMP_TIME,
MAX_TIME_WIDTH * MY_CHARSET_BIN_MB_MAXLEN);
......@@ -365,14 +363,16 @@ static void set_param_time(Item_param *param, uchar **pos, ulong len)
static void set_param_datetime(Item_param *param, uchar **pos, ulong len)
{
uint length;
if ((length= get_param_length(pos, len)) >= 4)
{
uchar *to= *pos;
TIME tm;
tm.second_part= (length > 7 ) ? (ulong) sint4korr(to+7): 0;
tm.neg= 0;
tm.year= (uint) sint2korr(to);
tm.month= (uint) to[2];
tm.day= (uint) to[3];
/*
Note, that though ranges of hour, minute and second are not checked
here we rely on them being < 256: otherwise
......@@ -386,11 +386,8 @@ static void set_param_datetime(Item_param *param, uchar **pos, ulong len)
}
else
tm.hour= tm.minute= tm.second= 0;
tm.year= (uint) sint2korr(to);
tm.month= (uint) to[2];
tm.day= (uint) to[3];
tm.neg= 0;
tm.second_part= (length > 7) ? (ulong) sint4korr(to+7) : 0;
param->set_time(&tm, TIMESTAMP_DATETIME,
MAX_DATETIME_WIDTH * MY_CHARSET_BIN_MB_MAXLEN);
......@@ -585,6 +582,7 @@ static void setup_one_conversion_function(THD *thd, Item_param *param,
param->item_result_type= STRING_RESULT;
}
}
param->param_type= (enum enum_field_types) param_type;
}
#ifndef EMBEDDED_LIBRARY
......
......@@ -9802,6 +9802,73 @@ static void test_bug3796()
myquery(rc);
}
static void test_bug4026()
{
MYSQL_STMT *stmt;
MYSQL_BIND bind[2];
MYSQL_TIME time_in, time_out;
MYSQL_TIME datetime_in, datetime_out;
const char *stmt_text;
int rc;
myheader("test_bug4026");
/* Check that microseconds are inserted and selected successfully */
/* Create a statement handle and prepare it with select */
stmt= mysql_stmt_init(mysql);
stmt_text= "SELECT ?, ?";
rc= mysql_stmt_prepare(stmt, stmt_text, strlen(stmt_text));
check_execute(stmt, rc);
/* Bind input buffers */
bzero(bind, sizeof(bind));
bzero(&time_in, sizeof(time_in));
bzero(&time_out, sizeof(time_out));
bzero(&datetime_in, sizeof(datetime_in));
bzero(&datetime_out, sizeof(datetime_out));
bind[0].buffer_type= MYSQL_TYPE_TIME;
bind[0].buffer= (char*) &time_in;
bind[1].buffer_type= MYSQL_TYPE_DATETIME;
bind[1].buffer= (char*) &datetime_in;
time_in.hour= 23;
time_in.minute= 59;
time_in.second= 59;
time_in.second_part= 123456;
datetime_in= time_in;
datetime_in.year= 2003;
datetime_in.month= 12;
datetime_in.day= 31;
mysql_stmt_bind_param(stmt, bind);
/* Execute the select statement */
rc= mysql_stmt_execute(stmt);
check_execute(stmt, rc);
bind[0].buffer= (char*) &time_out;
bind[1].buffer= (char*) &datetime_out;
mysql_stmt_bind_result(stmt, bind);
rc= mysql_stmt_fetch(stmt);
assert(rc == 0);
printf("%d:%d:%d.%lu\n", time_out.hour, time_out.minute, time_out.second,
time_out.second_part);
printf("%d-%d-%d %d:%d:%d.%lu\n", datetime_out.year, datetime_out.month,
datetime_out.day, datetime_out.hour,
datetime_out.minute, datetime_out.second,
datetime_out.second_part);
assert(memcmp(&time_in, &time_out, sizeof(time_in)) == 0);
assert(memcmp(&datetime_in, &datetime_out, sizeof(datetime_in)) == 0);
mysql_stmt_close(stmt);
}
/*
Read and parse arguments and MySQL options from my.cnf
*/
......@@ -10094,6 +10161,7 @@ int main(int argc, char **argv)
(Bug #3686 */
test_ps_i18n(); /* test for i18n support in binary protocol */
test_bug3796(); /* test for select concat(?, <string>) */
test_bug4026(); /* test microseconds precision of time types */
/*
XXX: PLEASE RUN THIS PROGRAM UNDER VALGRIND AND VERIFY THAT YOUR TEST
DOESN'T CONTAIN WARNINGS/ERRORS BEFORE YOU PUSH.
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment