为
一、问题说明
一朋友线上用的mysql5.6.17,sql_mode配的STRICT_TRANS_TABLES,这个配置的具体含义就不在这里说明了,这个是比较严格的模式;
有一天发生一个奇怪的问题,为了简化说明,用以下表结构进行模拟:
CREATE TABLE `user1` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(10) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
上面有个表user1有个name字段,定义长度只有10。
具体执行的就是下面2 SQL,其中第一个是失败的,但第二个是成功的
INSERT INTO `user1`(name) VALUES('123451234512')
INSERT INTO `user1`(name) VALUES('1234512345 ')
其中第一条sql语句长度超过10了,并且没有多余的空格;
第2条则特殊一些,总长度超过10,并且尾部是空格,即去掉空格后总长度不超过10。
先手动执行下看下结果:
可以看到sql1失败报太长了,sql2执行成功,但只有一个警告。
二、源码分析
在mysql_insert函数上打断点:
while ((values= its++))
{
if (fields.elements || !value_count)
{
restore_record(table,s->default_values); // Get empty record
/*
Check whether default values of the fields not specified in column list
are correct or not.
*/
if (validate_default_values_of_unset_fields(thd, table))
{
error= 1;
break;
}
if (fill_record_n_invoke_before_triggers(thd, fields, *values, 0,
table->triggers,
TRG_EVENT_INSERT))
{
if (values_list.elements != 1 && ! thd->is_error())
{
info.stats.records++;
continue;
}
/*
TODO: set thd->abort_on_warning if values_list.elements == 1
and check that all items return warning in case of problem with
storing field.
*/
error=1;
break;
}
}
比较关键的是函数fill_record_n_invoke_before_triggers,跟进去一直到Field_varstring类的store函数;
mysql对于每种数据类型抽象一个类,varchar对应的是Field_varstring:
type_conversion_status Field_varstring::store(const char *from,uint length,
const CHARSET_INFO *cs)
{
ASSERT_COLUMN_MARKED_FOR_WRITE;
uint copy_length;
const char *well_formed_error_pos;
const char *cannot_convert_error_pos;
const char *from_end_pos;
copy_length= well_formed_copy_nchars(field_charset,
(char*) ptr + length_bytes,
field_length,
cs, from, length,
field_length / field_charset->mbmaxlen,
&well_formed_error_pos,
&cannot_convert_error_pos,
&from_end_pos);
if (length_bytes == 1)
*ptr= (uchar) copy_length;
else
int2store(ptr, copy_length);
return check_string_copy_error(well_formed_error_pos,
cannot_convert_error_pos, from_end_pos,
from + length, true, cs);
}
这里可以看from就是我们要插入的内容:
因为类型是varchar(10),所以只拷贝10个字符,重点看函数check_string_copy_error:
type_conversion_status
Field_longstr::check_string_copy_error(const char *well_formed_error_pos,
const char *cannot_convert_error_pos,
const char *from_end_pos,
const char *end,
bool count_spaces,
const CHARSET_INFO *cs) const
{
const char *pos;
char tmp[32];
THD *thd= table->in_use;
if (!(pos= well_formed_error_pos) &&
!(pos= cannot_convert_error_pos))
return report_if_important_data(from_end_pos, end, count_spaces);
convert_to_printable(tmp, sizeof(tmp), pos, (end - pos), cs, 6);
push_warning_printf(thd,
Sql_condition::WARN_LEVEL_WARN,
ER_TRUNCATED_WRONG_VALUE_FOR_FIELD,
ER(ER_TRUNCATED_WRONG_VALUE_FOR_FIELD),
"string", tmp, field_name,
thd->get_stmt_da()->current_row_for_warning());
return TYPE_WARN_TRUNCATED;
}
再跟进report_if_important_data函数:
type_conversion_status
Field_longstr::report_if_important_data(const char *pstr, const char *end,
bool count_spaces) const
{
if ((pstr < end) && table->in_use->count_cuted_fields)
{
if (test_if_important_data(field_charset, pstr, end))
{
if (table->in_use->abort_on_warning)
set_warning(Sql_condition::WARN_LEVEL_WARN, ER_DATA_TOO_LONG, 1);
else
set_warning(Sql_condition::WARN_LEVEL_WARN, WARN_DATA_TRUNCATED, 1);
return TYPE_WARN_TRUNCATED;
}
else if (count_spaces)
{ /* If we lost only spaces then produce a NOTE, not a WARNING */
set_warning(Sql_condition::WARN_LEVEL_NOTE, WARN_DATA_TRUNCATED, 1);
return TYPE_NOTE_TRUNCATED;
}
}
return TYPE_OK;
}
这里pstr是<end,因为前面讲了只拷贝10个字符,再看test_if_important_data函数:
static bool
test_if_important_data(const CHARSET_INFO *cs, const char *str,
const char *strend)
{
if (cs != &my_charset_bin)
str+= cs->cset->scan(cs, str, strend, MY_SEQ_SPACES);
return (str < strend);
}
这里scan最终对应的是my_scan_8bit函数:
size_t my_scan_8bit(const CHARSET_INFO *cs, const char *str, const char *end,
int sq)
{
const char *str0= str;
switch (sq)
{
case MY_SEQ_INTTAIL:
if (*str == '.')
{
for(str++ ; str != end && *str == '0' ; str++);
return (size_t) (str - str0);
}
return 0;
//进入这个逻辑
case MY_SEQ_SPACES:
for ( ; str < end ; str++)
{
if (!my_isspace(cs,*str))
break;
}
return (size_t) (str - str0);
default:
return 0;
}
}
因为传递的是MY_SEQ_SPACES,所以这里会判断my_isspace是否空格,如果是由跳过,因此尾部是空格由会跳过,即认为不会超过长度。
因此test_if_important_data会返回失败,设置相应告警,因sql_mode不同从而导致两者的错误码不一样:
if (table->in_use->abort_on_warning)
set_warning(Sql_condition::WARN_LEVEL_WARN, ER_DATA_TOO_LONG, 1);
else
set_warning(Sql_condition::WARN_LEVEL_WARN, WARN_DATA_TRUNCATED, 1);
return TYPE_WARN_TRUNCATED;
为什么这么设计,应该也是从数据正确性来看的,删除空格不影响最终数据的,但删除非空格的数据真的是丢数据了。
三、总结
1、varchar字段mysql内部用Field_varstring表示,插入时mysql会调用字段的store方法进行数据复制;
2、Field_varstring继承Field_longstr
并调用report_if_important_data来检查数据长度;
3、report_if_important_data调用test_if_important_data来检查是否超过长度,后者会根据每种字符集来做处理,本例是会略过相应空格。