一、问题说明
一朋友线上用的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 listare 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 == 1and check that all items return warning in case of problem withstoring 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;elseint2store(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_statusField_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_statusField_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);elseset_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 booltest_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);elseset_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来检查是否超过长度,后者会根据每种字符集来做处理,本例是会略过相应空格。




