From 44e75b142d907a6da79217f3367b8ce7f87ca6dd Mon Sep 17 00:00:00 2001 From: Yingwen Date: Wed, 30 Apr 2025 16:23:50 +0800 Subject: [PATCH] feat: cast strings to numerics automatically in mysql connections (#6015) * chore: insert support string to numeric auto cast * test: add sqlness test * chore: remove log * test: fix sql test * style: fix clippy * test: test invalid number * feat: do not convert to default if unable to parse * chore: update comment * test: update sqlness test * test: update prepare test --- .../src/req_convert/insert/stmt_to_region.rs | 16 +- src/operator/src/statement/admin.rs | 6 +- src/operator/src/statement/ddl.rs | 11 +- src/servers/src/mysql/helper.rs | 6 +- src/session/src/context.rs | 5 + src/sql/src/error.rs | 11 +- src/sql/src/statements.rs | 424 +++++++++++++++++- .../common/insert/mysql_insert.result | 41 ++ .../standalone/common/insert/mysql_insert.sql | 21 + .../common/prepare/mysql_prepare.result | 2 +- 10 files changed, 515 insertions(+), 28 deletions(-) create mode 100644 tests/cases/standalone/common/insert/mysql_insert.result create mode 100644 tests/cases/standalone/common/insert/mysql_insert.sql diff --git a/src/operator/src/req_convert/insert/stmt_to_region.rs b/src/operator/src/req_convert/insert/stmt_to_region.rs index 69137c4908..5f6649b4e4 100644 --- a/src/operator/src/req_convert/insert/stmt_to_region.rs +++ b/src/operator/src/req_convert/insert/stmt_to_region.rs @@ -129,6 +129,7 @@ impl<'a> StatementToRegion<'a> { column_schema, &sql_row[i], Some(&query_ctx.timezone()), + query_ctx.auto_string_to_numeric(), )?; grpc_row.values.push(value); } @@ -205,10 +206,14 @@ fn column_names<'a>(stmt: &'a Insert, table_schema: &'a SchemaRef) -> Vec<&'a St } } +/// Converts SQL value to gRPC value according to the column schema. +/// If `auto_string_to_numeric` is true, tries to cast the string value to numeric values, +/// and fills the default value if the cast fails. fn sql_value_to_grpc_value( column_schema: &ColumnSchema, sql_val: &SqlValue, timezone: Option<&Timezone>, + auto_string_to_numeric: bool, ) -> Result { let column = &column_schema.name; let value = if replace_default(sql_val) { @@ -222,8 +227,15 @@ fn sql_value_to_grpc_value( column: column.clone(), })? } else { - statements::sql_value_to_value(column, &column_schema.data_type, sql_val, timezone, None) - .context(ParseSqlSnafu)? + statements::sql_value_to_value( + column, + &column_schema.data_type, + sql_val, + timezone, + None, + auto_string_to_numeric, + ) + .context(ParseSqlSnafu)? }; let grpc_value = value_to_grpc_value(value); diff --git a/src/operator/src/statement/admin.rs b/src/operator/src/statement/admin.rs index 885605e6d3..a956b5f7fc 100644 --- a/src/operator/src/statement/admin.rs +++ b/src/operator/src/statement/admin.rs @@ -185,7 +185,7 @@ fn values_to_vectors_by_exact_types( args.iter() .zip(exact_types.iter()) .map(|(value, data_type)| { - let value = sql_value_to_value(DUMMY_COLUMN, data_type, value, tz, None) + let value = sql_value_to_value(DUMMY_COLUMN, data_type, value, tz, None, false) .context(error::ParseSqlValueSnafu)?; Ok(value_to_vector(value)) @@ -202,7 +202,9 @@ fn values_to_vectors_by_valid_types( args.iter() .map(|value| { for data_type in valid_types { - if let Ok(value) = sql_value_to_value(DUMMY_COLUMN, data_type, value, tz, None) { + if let Ok(value) = + sql_value_to_value(DUMMY_COLUMN, data_type, value, tz, None, false) + { return Ok(value_to_vector(value)); } } diff --git a/src/operator/src/statement/ddl.rs b/src/operator/src/statement/ddl.rs index afae466dc0..ad2f5bcf71 100644 --- a/src/operator/src/statement/ddl.rs +++ b/src/operator/src/statement/ddl.rs @@ -1648,8 +1648,15 @@ fn convert_value( timezone: &Timezone, unary_op: Option, ) -> Result { - sql_value_to_value("", &data_type, value, Some(timezone), unary_op) - .context(ParseSqlValueSnafu) + sql_value_to_value( + "", + &data_type, + value, + Some(timezone), + unary_op, + false, + ) + .context(ParseSqlValueSnafu) } #[cfg(test)] diff --git a/src/servers/src/mysql/helper.rs b/src/servers/src/mysql/helper.rs index 467430b31d..a2e05d1c06 100644 --- a/src/servers/src/mysql/helper.rs +++ b/src/servers/src/mysql/helper.rs @@ -241,10 +241,12 @@ pub fn convert_value(param: &ParamValue, t: &ConcreteDataType) -> Result Result { match param { Expr::Value(v) => { - let v = sql_value_to_value("", t, v, None, None); + let v = sql_value_to_value("", t, v, None, None, true); match v { Ok(v) => v .try_to_scalar_value(t) @@ -256,7 +258,7 @@ pub fn convert_expr_to_scalar_value(param: &Expr, t: &ConcreteDataType) -> Resul } } Expr::UnaryOp { op, expr } if let Expr::Value(v) = &**expr => { - let v = sql_value_to_value("", t, v, None, Some(*op)); + let v = sql_value_to_value("", t, v, None, Some(*op), true); match v { Ok(v) => v .try_to_scalar_value(t) diff --git a/src/session/src/context.rs b/src/session/src/context.rs index 84087e66e7..2366ee1c8b 100644 --- a/src/session/src/context.rs +++ b/src/session/src/context.rs @@ -406,6 +406,11 @@ impl QueryContext { pub fn get_snapshot(&self, region_id: u64) -> Option { self.snapshot_seqs.read().unwrap().get(®ion_id).cloned() } + + /// Returns `true` if the session can cast strings to numbers in MySQL style. + pub fn auto_string_to_numeric(&self) -> bool { + matches!(self.channel, Channel::Mysql) + } } impl QueryContextBuilder { diff --git a/src/sql/src/error.rs b/src/sql/src/error.rs index e07efdbe6c..ab1aea82bc 100644 --- a/src/sql/src/error.rs +++ b/src/sql/src/error.rs @@ -355,6 +355,14 @@ pub enum Error { #[snafu(implicit)] location: Location, }, + + #[snafu(display("Unable to convert {} to datatype {:?}", value, datatype))] + ConvertStr { + value: String, + datatype: ConcreteDataType, + #[snafu(implicit)] + location: Location, + }, } impl ErrorExt for Error { @@ -391,7 +399,8 @@ impl ErrorExt for Error { | InvalidInterval { .. } | InvalidUnaryOp { .. } | InvalidPartitionNumber { .. } - | UnsupportedUnaryOp { .. } => StatusCode::InvalidArguments, + | UnsupportedUnaryOp { .. } + | ConvertStr { .. } => StatusCode::InvalidArguments, SerializeColumnDefaultConstraint { source, .. } => source.status_code(), ConvertToGrpcDataType { source, .. } => source.status_code(), diff --git a/src/sql/src/statements.rs b/src/sql/src/statements.rs index 984ec4c6be..bc5bd6821a 100644 --- a/src/sql/src/statements.rs +++ b/src/sql/src/statements.rs @@ -53,11 +53,11 @@ use crate::ast::{ Value as SqlValue, }; use crate::error::{ - self, ColumnTypeMismatchSnafu, ConvertSqlValueSnafu, ConvertToGrpcDataTypeSnafu, - ConvertValueSnafu, DatatypeSnafu, InvalidCastSnafu, InvalidSqlValueSnafu, InvalidUnaryOpSnafu, - ParseSqlValueSnafu, Result, SerializeColumnDefaultConstraintSnafu, SetFulltextOptionSnafu, - SetSkippingIndexOptionSnafu, TimestampOverflowSnafu, UnsupportedDefaultValueSnafu, - UnsupportedUnaryOpSnafu, + self, ColumnTypeMismatchSnafu, ConvertSqlValueSnafu, ConvertStrSnafu, + ConvertToGrpcDataTypeSnafu, ConvertValueSnafu, DatatypeSnafu, InvalidCastSnafu, + InvalidSqlValueSnafu, InvalidUnaryOpSnafu, ParseSqlValueSnafu, Result, + SerializeColumnDefaultConstraintSnafu, SetFulltextOptionSnafu, SetSkippingIndexOptionSnafu, + TimestampOverflowSnafu, UnsupportedDefaultValueSnafu, UnsupportedUnaryOpSnafu, }; use crate::statements::create::Column; pub use crate::statements::option_map::OptionMap; @@ -70,7 +70,14 @@ fn parse_string_to_value( s: String, data_type: &ConcreteDataType, timezone: Option<&Timezone>, + auto_string_to_numeric: bool, ) -> Result { + if auto_string_to_numeric { + if let Some(value) = auto_cast_to_numeric(&s, data_type)? { + return Ok(value); + } + } + ensure!( data_type.is_stringifiable(), ColumnTypeMismatchSnafu { @@ -133,6 +140,42 @@ fn parse_string_to_value( } } +/// Casts string to value of specified numeric data type. +/// If the string cannot be parsed, returns an error. +/// +/// Returns None if the data type doesn't support auto casting. +fn auto_cast_to_numeric(s: &str, data_type: &ConcreteDataType) -> Result> { + let value = match data_type { + ConcreteDataType::Boolean(_) => s.parse::().map(Value::Boolean).ok(), + ConcreteDataType::Int8(_) => s.parse::().map(Value::Int8).ok(), + ConcreteDataType::Int16(_) => s.parse::().map(Value::Int16).ok(), + ConcreteDataType::Int32(_) => s.parse::().map(Value::Int32).ok(), + ConcreteDataType::Int64(_) => s.parse::().map(Value::Int64).ok(), + ConcreteDataType::UInt8(_) => s.parse::().map(Value::UInt8).ok(), + ConcreteDataType::UInt16(_) => s.parse::().map(Value::UInt16).ok(), + ConcreteDataType::UInt32(_) => s.parse::().map(Value::UInt32).ok(), + ConcreteDataType::UInt64(_) => s.parse::().map(Value::UInt64).ok(), + ConcreteDataType::Float32(_) => s + .parse::() + .map(|v| Value::Float32(OrderedF32::from(v))) + .ok(), + ConcreteDataType::Float64(_) => s + .parse::() + .map(|v| Value::Float64(OrderedF64::from(v))) + .ok(), + _ => return Ok(None), + }; + + match value { + Some(value) => Ok(Some(value)), + None => ConvertStrSnafu { + value: s, + datatype: data_type.clone(), + } + .fail(), + } +} + fn parse_hex_string(s: &str) -> Result { match hex::decode(s) { Ok(b) => Ok(Value::Binary(Bytes::from(b))), @@ -228,12 +271,16 @@ where } } +/// Converts SQL value to value according to the data type. +/// If `auto_string_to_numeric` is true, tries to cast the string value to numeric values, +/// and returns error if the cast fails. pub fn sql_value_to_value( column_name: &str, data_type: &ConcreteDataType, sql_val: &SqlValue, timezone: Option<&Timezone>, unary_op: Option, + auto_string_to_numeric: bool, ) -> Result { let mut value = match sql_val { SqlValue::Number(n, _) => sql_number_to_value(data_type, n)?, @@ -250,9 +297,13 @@ pub fn sql_value_to_value( (*b).into() } - SqlValue::DoubleQuotedString(s) | SqlValue::SingleQuotedString(s) => { - parse_string_to_value(column_name, s.clone(), data_type, timezone)? - } + SqlValue::DoubleQuotedString(s) | SqlValue::SingleQuotedString(s) => parse_string_to_value( + column_name, + s.clone(), + data_type, + timezone, + auto_string_to_numeric, + )?, SqlValue::HexStringLiteral(s) => { // Should not directly write binary into json column ensure!( @@ -371,7 +422,7 @@ fn parse_column_default_constraint( { let default_constraint = match &opt.option { ColumnOption::Default(Expr::Value(v)) => ColumnDefaultConstraint::Value( - sql_value_to_value(column_name, data_type, v, timezone, None)?, + sql_value_to_value(column_name, data_type, v, timezone, None, false)?, ), ColumnOption::Default(Expr::Function(func)) => { let mut func = format!("{func}").to_lowercase(); @@ -397,7 +448,8 @@ fn parse_column_default_constraint( } if let Expr::Value(v) = &**expr { - let value = sql_value_to_value(column_name, data_type, v, timezone, Some(*op))?; + let value = + sql_value_to_value(column_name, data_type, v, timezone, Some(*op), false)?; ColumnDefaultConstraint::Value(value) } else { return UnsupportedDefaultValueSnafu { @@ -440,8 +492,6 @@ pub fn has_primary_key_option(column_def: &ColumnDef) -> bool { }) } -// TODO(yingwen): Make column nullable by default, and checks invalid case like -// a column is not nullable but has a default value null. /// Create a `ColumnSchema` from `Column`. pub fn column_to_schema( column: &Column, @@ -809,7 +859,8 @@ mod tests { &ConcreteDataType::float64_datatype(), &sql_val, None, - None + None, + false ) .unwrap() ); @@ -822,7 +873,8 @@ mod tests { &ConcreteDataType::boolean_datatype(), &sql_val, None, - None + None, + false ) .unwrap() ); @@ -835,7 +887,8 @@ mod tests { &ConcreteDataType::float64_datatype(), &sql_val, None, - None + None, + false ) .unwrap() ); @@ -847,6 +900,7 @@ mod tests { &sql_val, None, None, + false, ); assert!(v.is_err()); assert!(format!("{v:?}").contains("Failed to parse number '3.0' to boolean column type")); @@ -858,6 +912,7 @@ mod tests { &sql_val, None, None, + false, ); assert!(v.is_err()); assert!( @@ -874,6 +929,7 @@ mod tests { &sql_val, None, None, + false, ) .unwrap(); assert_eq!(Value::Binary(Bytes::from(b"Hello world!".as_slice())), v); @@ -885,6 +941,7 @@ mod tests { &sql_val, None, None, + false, ) .unwrap(); assert_eq!( @@ -899,6 +956,7 @@ mod tests { &sql_val, None, None, + false, ); assert!(v.is_err()); assert!( @@ -913,6 +971,7 @@ mod tests { &sql_val, None, None, + false, ); assert!(v.is_err()); assert!(format!("{v:?}").contains("invalid character"), "v is {v:?}",); @@ -924,6 +983,7 @@ mod tests { &sql_val, None, None, + false, ); assert!(v.is_err()); @@ -934,6 +994,7 @@ mod tests { &sql_val, None, None, + false, ) .unwrap(); assert_eq!( @@ -948,13 +1009,14 @@ mod tests { } #[test] - pub fn test_parse_date_literal() { + fn test_parse_date_literal() { let value = sql_value_to_value( "date", &ConcreteDataType::date_datatype(), &SqlValue::DoubleQuotedString("2022-02-22".to_string()), None, None, + false, ) .unwrap(); assert_eq!(ConcreteDataType::date_datatype(), value.data_type()); @@ -971,6 +1033,7 @@ mod tests { &SqlValue::DoubleQuotedString("2022-02-22".to_string()), Some(&Timezone::from_tz_string("+07:00").unwrap()), None, + false, ) .unwrap(); assert_eq!(ConcreteDataType::date_datatype(), value.data_type()); @@ -988,6 +1051,7 @@ mod tests { "2022-02-22T00:01:01+08:00".to_string(), &ConcreteDataType::timestamp_millisecond_datatype(), None, + false, ) .unwrap() { @@ -1005,6 +1069,7 @@ mod tests { "2022-02-22T00:01:01+08:00".to_string(), &ConcreteDataType::timestamp_datatype(TimeUnit::Second), None, + false, ) .unwrap() { @@ -1022,6 +1087,7 @@ mod tests { "2022-02-22T00:01:01+08:00".to_string(), &ConcreteDataType::timestamp_datatype(TimeUnit::Microsecond), None, + false, ) .unwrap() { @@ -1039,6 +1105,7 @@ mod tests { "2022-02-22T00:01:01+08:00".to_string(), &ConcreteDataType::timestamp_datatype(TimeUnit::Nanosecond), None, + false, ) .unwrap() { @@ -1056,6 +1123,7 @@ mod tests { "2022-02-22T00:01:01+08".to_string(), &ConcreteDataType::timestamp_datatype(TimeUnit::Nanosecond), None, + false, ) .is_err()); @@ -1065,6 +1133,7 @@ mod tests { "2022-02-22T00:01:01".to_string(), &ConcreteDataType::timestamp_datatype(TimeUnit::Nanosecond), Some(&Timezone::from_tz_string("Asia/Shanghai").unwrap()), + false, ) .unwrap() { @@ -1086,6 +1155,7 @@ mod tests { r#"{"a": "b"}"#.to_string(), &ConcreteDataType::json_datatype(), None, + false, ) { Ok(Value::Binary(b)) => { assert_eq!( @@ -1105,6 +1175,7 @@ mod tests { r#"Nicola Kovac is the best rifler in the world"#.to_string(), &ConcreteDataType::json_datatype(), None, + false, ) .is_err()) } @@ -1476,7 +1547,8 @@ mod tests { &ConcreteDataType::string_datatype(), &SqlValue::Placeholder("default".into()), None, - None + None, + false ) .is_err()); assert!(sql_value_to_value( @@ -1485,6 +1557,7 @@ mod tests { &SqlValue::Placeholder("default".into()), None, Some(UnaryOperator::Minus), + false ) .is_err()); assert!(sql_value_to_value( @@ -1493,6 +1566,7 @@ mod tests { &SqlValue::Number("3".into(), false), None, Some(UnaryOperator::Minus), + false ) .is_err()); assert!(sql_value_to_value( @@ -1500,8 +1574,322 @@ mod tests { &ConcreteDataType::uint16_datatype(), &SqlValue::Number("3".into(), false), None, - None + None, + false ) .is_ok()); } + + #[test] + fn test_string_to_value_auto_numeric() { + // Test string to boolean with auto cast + let result = parse_string_to_value( + "col", + "true".to_string(), + &ConcreteDataType::boolean_datatype(), + None, + true, + ) + .unwrap(); + assert_eq!(Value::Boolean(true), result); + + // Test invalid string to boolean with auto cast + let result = parse_string_to_value( + "col", + "not_a_boolean".to_string(), + &ConcreteDataType::boolean_datatype(), + None, + true, + ); + assert!(result.is_err()); + + // Test string to int8 + let result = parse_string_to_value( + "col", + "42".to_string(), + &ConcreteDataType::int8_datatype(), + None, + true, + ) + .unwrap(); + assert_eq!(Value::Int8(42), result); + + // Test invalid string to int8 with auto cast + let result = parse_string_to_value( + "col", + "not_an_int8".to_string(), + &ConcreteDataType::int8_datatype(), + None, + true, + ); + assert!(result.is_err()); + + // Test string to int16 + let result = parse_string_to_value( + "col", + "1000".to_string(), + &ConcreteDataType::int16_datatype(), + None, + true, + ) + .unwrap(); + assert_eq!(Value::Int16(1000), result); + + // Test invalid string to int16 with auto cast + let result = parse_string_to_value( + "col", + "not_an_int16".to_string(), + &ConcreteDataType::int16_datatype(), + None, + true, + ); + assert!(result.is_err()); + + // Test string to int32 + let result = parse_string_to_value( + "col", + "100000".to_string(), + &ConcreteDataType::int32_datatype(), + None, + true, + ) + .unwrap(); + assert_eq!(Value::Int32(100000), result); + + // Test invalid string to int32 with auto cast + let result = parse_string_to_value( + "col", + "not_an_int32".to_string(), + &ConcreteDataType::int32_datatype(), + None, + true, + ); + assert!(result.is_err()); + + // Test string to int64 + let result = parse_string_to_value( + "col", + "1000000".to_string(), + &ConcreteDataType::int64_datatype(), + None, + true, + ) + .unwrap(); + assert_eq!(Value::Int64(1000000), result); + + // Test invalid string to int64 with auto cast + let result = parse_string_to_value( + "col", + "not_an_int64".to_string(), + &ConcreteDataType::int64_datatype(), + None, + true, + ); + assert!(result.is_err()); + + // Test string to uint8 + let result = parse_string_to_value( + "col", + "200".to_string(), + &ConcreteDataType::uint8_datatype(), + None, + true, + ) + .unwrap(); + assert_eq!(Value::UInt8(200), result); + + // Test invalid string to uint8 with auto cast + let result = parse_string_to_value( + "col", + "not_a_uint8".to_string(), + &ConcreteDataType::uint8_datatype(), + None, + true, + ); + assert!(result.is_err()); + + // Test string to uint16 + let result = parse_string_to_value( + "col", + "60000".to_string(), + &ConcreteDataType::uint16_datatype(), + None, + true, + ) + .unwrap(); + assert_eq!(Value::UInt16(60000), result); + + // Test invalid string to uint16 with auto cast + let result = parse_string_to_value( + "col", + "not_a_uint16".to_string(), + &ConcreteDataType::uint16_datatype(), + None, + true, + ); + assert!(result.is_err()); + + // Test string to uint32 + let result = parse_string_to_value( + "col", + "4000000000".to_string(), + &ConcreteDataType::uint32_datatype(), + None, + true, + ) + .unwrap(); + assert_eq!(Value::UInt32(4000000000), result); + + // Test invalid string to uint32 with auto cast + let result = parse_string_to_value( + "col", + "not_a_uint32".to_string(), + &ConcreteDataType::uint32_datatype(), + None, + true, + ); + assert!(result.is_err()); + + // Test string to uint64 + let result = parse_string_to_value( + "col", + "18446744073709551615".to_string(), + &ConcreteDataType::uint64_datatype(), + None, + true, + ) + .unwrap(); + assert_eq!(Value::UInt64(18446744073709551615), result); + + // Test invalid string to uint64 with auto cast + let result = parse_string_to_value( + "col", + "not_a_uint64".to_string(), + &ConcreteDataType::uint64_datatype(), + None, + true, + ); + assert!(result.is_err()); + + // Test string to float32 + let result = parse_string_to_value( + "col", + "3.5".to_string(), + &ConcreteDataType::float32_datatype(), + None, + true, + ) + .unwrap(); + assert_eq!(Value::Float32(OrderedF32::from(3.5)), result); + + // Test invalid string to float32 with auto cast + let result = parse_string_to_value( + "col", + "not_a_float32".to_string(), + &ConcreteDataType::float32_datatype(), + None, + true, + ); + assert!(result.is_err()); + + // Test string to float64 + let result = parse_string_to_value( + "col", + "3.5".to_string(), + &ConcreteDataType::float64_datatype(), + None, + true, + ) + .unwrap(); + assert_eq!(Value::Float64(OrderedF64::from(3.5)), result); + + // Test invalid string to float64 with auto cast + let result = parse_string_to_value( + "col", + "not_a_float64".to_string(), + &ConcreteDataType::float64_datatype(), + None, + true, + ); + assert!(result.is_err()); + } + + #[test] + fn test_auto_string_to_numeric() { + // Test with auto_string_to_numeric=true + let sql_val = SqlValue::SingleQuotedString("123".to_string()); + let v = sql_value_to_value( + "a", + &ConcreteDataType::int32_datatype(), + &sql_val, + None, + None, + true, + ) + .unwrap(); + assert_eq!(Value::Int32(123), v); + + // Test with a float string + let sql_val = SqlValue::SingleQuotedString("3.5".to_string()); + let v = sql_value_to_value( + "a", + &ConcreteDataType::float64_datatype(), + &sql_val, + None, + None, + true, + ) + .unwrap(); + assert_eq!(Value::Float64(OrderedFloat(3.5)), v); + + // Test with auto_string_to_numeric=false + let sql_val = SqlValue::SingleQuotedString("123".to_string()); + let v = sql_value_to_value( + "a", + &ConcreteDataType::int32_datatype(), + &sql_val, + None, + None, + false, + ); + assert!(v.is_err()); + + // Test with an invalid numeric string but auto_string_to_numeric=true + // Should return an error now with the new auto_cast_to_numeric behavior + let sql_val = SqlValue::SingleQuotedString("not_a_number".to_string()); + let v = sql_value_to_value( + "a", + &ConcreteDataType::int32_datatype(), + &sql_val, + None, + None, + true, + ); + assert!(v.is_err()); + + // Test with boolean type + let sql_val = SqlValue::SingleQuotedString("true".to_string()); + let v = sql_value_to_value( + "a", + &ConcreteDataType::boolean_datatype(), + &sql_val, + None, + None, + true, + ) + .unwrap(); + assert_eq!(Value::Boolean(true), v); + + // Non-numeric types should still be handled normally + let sql_val = SqlValue::SingleQuotedString("hello".to_string()); + let v = sql_value_to_value( + "a", + &ConcreteDataType::string_datatype(), + &sql_val, + None, + None, + true, + ); + assert!(v.is_ok()); + } } diff --git a/tests/cases/standalone/common/insert/mysql_insert.result b/tests/cases/standalone/common/insert/mysql_insert.result new file mode 100644 index 0000000000..a2813f3a3b --- /dev/null +++ b/tests/cases/standalone/common/insert/mysql_insert.result @@ -0,0 +1,41 @@ +-- SQLNESS PROTOCOL MYSQL +CREATE TABLE integers ( + val INT, + ts TIMESTAMP, + TIME INDEX(ts) +); + +affected_rows: 0 + +-- SQLNESS PROTOCOL MYSQL +INSERT INTO integers VALUES (11, 1), (12, 2); + +affected_rows: 2 + +-- SQLNESS PROTOCOL MYSQL +INSERT INTO integers VALUES ('13', 3), ('14', 4); + +affected_rows: 2 + +-- SQLNESS PROTOCOL MYSQL +INSERT INTO integers VALUES ('15a', 5), ('16', 6); + +Failed to execute query, err: MySqlError { ERROR 1210 (HY000): (InvalidArguments): Unable to convert 15a to datatype Int32(Int32Type) } + +-- SQLNESS PROTOCOL MYSQL +SELECT * FROM integers ORDER BY ts; + ++-----+----------------------------+ +| val | ts | ++-----+----------------------------+ +| 11 | 1970-01-01 00:00:00.001000 | +| 12 | 1970-01-01 00:00:00.002000 | +| 13 | 1970-01-01 00:00:00.003000 | +| 14 | 1970-01-01 00:00:00.004000 | ++-----+----------------------------+ + +-- SQLNESS PROTOCOL MYSQL +DROP TABLE integers; + +affected_rows: 0 + diff --git a/tests/cases/standalone/common/insert/mysql_insert.sql b/tests/cases/standalone/common/insert/mysql_insert.sql new file mode 100644 index 0000000000..f06ee4c983 --- /dev/null +++ b/tests/cases/standalone/common/insert/mysql_insert.sql @@ -0,0 +1,21 @@ +-- SQLNESS PROTOCOL MYSQL +CREATE TABLE integers ( + val INT, + ts TIMESTAMP, + TIME INDEX(ts) +); + +-- SQLNESS PROTOCOL MYSQL +INSERT INTO integers VALUES (11, 1), (12, 2); + +-- SQLNESS PROTOCOL MYSQL +INSERT INTO integers VALUES ('13', 3), ('14', 4); + +-- SQLNESS PROTOCOL MYSQL +INSERT INTO integers VALUES ('15a', 5), ('16', 6); + +-- SQLNESS PROTOCOL MYSQL +SELECT * FROM integers ORDER BY ts; + +-- SQLNESS PROTOCOL MYSQL +DROP TABLE integers; diff --git a/tests/cases/standalone/common/prepare/mysql_prepare.result b/tests/cases/standalone/common/prepare/mysql_prepare.result index 37540834a1..8ebc94d570 100644 --- a/tests/cases/standalone/common/prepare/mysql_prepare.result +++ b/tests/cases/standalone/common/prepare/mysql_prepare.result @@ -22,7 +22,7 @@ EXECUTE stmt USING 1; -- SQLNESS PROTOCOL MYSQL EXECUTE stmt USING 'a'; -Failed to execute query, err: MySqlError { ERROR 1210 (HY000): (InvalidArguments): Invalid request parameter: Column expect type: Int32(Int32Type), actual: String(StringType) } +Failed to execute query, err: MySqlError { ERROR 1210 (HY000): (InvalidArguments): Invalid request parameter: Unable to convert a to datatype Int32(Int32Type) } -- SQLNESS PROTOCOL MYSQL DEALLOCATE stmt;