diff --git a/src/query/service/src/pipelines/processors/transforms/transform_dictionary.rs b/src/query/service/src/pipelines/processors/transforms/transform_dictionary.rs index 806091ffaf1b..4490290a1ab0 100644 --- a/src/query/service/src/pipelines/processors/transforms/transform_dictionary.rs +++ b/src/query/service/src/pipelines/processors/transforms/transform_dictionary.rs @@ -14,6 +14,7 @@ use std::collections::BTreeMap; use std::collections::HashMap; +use std::collections::HashSet; use std::string::String; use std::sync::Arc; @@ -32,6 +33,7 @@ use databend_common_expression::types::StringType; use databend_common_expression::types::ValueType; use databend_common_expression::with_integer_mapped_type; use databend_common_expression::BlockEntry; +use databend_common_expression::Column; use databend_common_expression::ColumnBuilder; use databend_common_expression::DataBlock; use databend_common_expression::Scalar; @@ -53,6 +55,92 @@ use crate::sql::plans::DictGetFunctionArgument; use crate::sql::plans::DictionarySource; use crate::sql::IndexType; +macro_rules! sqlx_fetch_optional { + ($pool:expr, $sql:expr, $key_type:ty, $val_type:ty, $format_val_fn:expr) => {{ + let res: Option<($key_type, $val_type)> = + sqlx::query_as(&$sql).fetch_optional($pool).await?; + Ok(res.map(|(_, v)| $format_val_fn(v))) + }}; +} + +macro_rules! fetch_single_row_by_sqlx { + ($pool:expr, $sql:expr, $key_scalar:expr, $val_type:ty, $format_val_fn:expr) => {{ + match $key_scalar { + DataType::Boolean => { + sqlx_fetch_optional!($pool, $sql, bool, $val_type, $format_val_fn) + } + DataType::String => { + sqlx_fetch_optional!($pool, $sql, String, $val_type, $format_val_fn) + } + DataType::Number(num_ty) => with_integer_mapped_type!(|KEY_NUM_TYPE| match num_ty { + NumberDataType::KEY_NUM_TYPE => { + sqlx_fetch_optional!($pool, $sql, KEY_NUM_TYPE, $val_type, $format_val_fn) + } + NumberDataType::Float32 => { + sqlx_fetch_optional!($pool, $sql, f32, $val_type, $format_val_fn) + } + NumberDataType::Float64 => { + sqlx_fetch_optional!($pool, $sql, f64, $val_type, $format_val_fn) + } + }), + _ => Err(ErrorCode::DictionarySourceError(format!( + "MySQL dictionary operator currently does not support value type {}", + $key_scalar, + ))), + } + }}; +} + +macro_rules! fetch_all_rows_by_sqlx { + ($pool:expr, $sql:expr, $key_scalar:expr, $val_type:ty, $format_key_fn:expr) => { + match $key_scalar { + DataType::Boolean => { + let res: Vec<(bool, $val_type)> = sqlx::query_as($sql).fetch_all($pool).await?; + res.into_iter() + .map(|(k, v)| ($format_key_fn(ScalarRef::Boolean(k)), v)) + .collect() + } + DataType::String => { + let res: Vec<(String, $val_type)> = sqlx::query_as($sql).fetch_all($pool).await?; + res.into_iter() + .map(|(k, v)| ($format_key_fn(ScalarRef::String(&k)), v)) + .collect() + } + DataType::Number(num_ty) => { + with_integer_mapped_type!(|NUM_TYPE| match num_ty { + NumberDataType::NUM_TYPE => { + let res: Vec<(NUM_TYPE, $val_type)> = + sqlx::query_as($sql).fetch_all($pool).await?; + res.into_iter() + .map(|(k, v)| (format!("{}", k), v)) + .collect() + } + NumberDataType::Float32 => { + let res: Vec<(f32, $val_type)> = + sqlx::query_as($sql).fetch_all($pool).await?; + res.into_iter() + .map(|(k, v)| (format!("{}", k), v)) + .collect() + } + NumberDataType::Float64 => { + let res: Vec<(f64, $val_type)> = + sqlx::query_as($sql).fetch_all($pool).await?; + res.into_iter() + .map(|(k, v)| (format!("{}", k), v)) + .collect() + } + }) + } + _ => { + return Err(ErrorCode::DictionarySourceError(format!( + "MySQL dictionary operator currently does not support value type: {}", + $key_scalar + ))); + } + } + }; +} + pub(crate) enum DictionaryOperator { Redis(ConnectionManager), Mysql((MySqlPool, String)), @@ -95,21 +183,14 @@ impl DictionaryOperator { DictionaryOperator::Mysql((pool, sql)) => match value { Value::Scalar(scalar) => { let value = self - .get_data_from_mysql(scalar.as_ref(), data_type, pool, sql) + .get_scalar_value_from_mysql(scalar.as_ref(), data_type, pool, sql) .await? .unwrap_or(default_value.clone()); Ok(Value::Scalar(value)) } Value::Column(column) => { - let mut builder = ColumnBuilder::with_capacity(data_type, column.len()); - for scalar_ref in column.iter() { - let value = self - .get_data_from_mysql(scalar_ref, data_type, pool, sql) - .await? - .unwrap_or(default_value.clone()); - builder.push(value.as_ref()); - } - Ok(Value::Column(builder.build())) + self.get_column_values_from_mysql(column, data_type, default_value, pool, sql) + .await } }, } @@ -239,65 +320,156 @@ impl DictionaryOperator { } } - async fn get_data_from_mysql( + async fn get_scalar_value_from_mysql( &self, key: ScalarRef<'_>, - data_type: &DataType, + value_type: &DataType, pool: &MySqlPool, sql: &String, ) -> Result> { if key == ScalarRef::Null { return Ok(None); } - match data_type.remove_nullable() { + let new_sql = format!("{} ({}) LIMIT 1", sql, self.format_key(key.clone())); + let key_type = key.infer_data_type().remove_nullable(); + match value_type.remove_nullable() { DataType::Boolean => { - let value: Option = sqlx::query_scalar(sql) - .bind(self.format_key(key)) - .fetch_optional(pool) - .await?; - Ok(value.map(Scalar::Boolean)) + fetch_single_row_by_sqlx!(pool, new_sql, key_type, bool, Scalar::Boolean) } DataType::String => { - let value: Option = sqlx::query_scalar(sql) - .bind(self.format_key(key)) - .fetch_optional(pool) - .await?; - Ok(value.map(Scalar::String)) + fetch_single_row_by_sqlx!(pool, new_sql, key_type, String, Scalar::String) } DataType::Number(num_ty) => { with_integer_mapped_type!(|NUM_TYPE| match num_ty { NumberDataType::NUM_TYPE => { - let value: Option = sqlx::query_scalar(&sql) - .bind(self.format_key(key)) - .fetch_optional(pool) - .await?; - Ok(value.map(|v| Scalar::Number(NUM_TYPE::upcast_scalar(v)))) + fetch_single_row_by_sqlx!(pool, new_sql, key_type, NUM_TYPE, |v| { + Scalar::Number(NUM_TYPE::upcast_scalar(v)) + }) } NumberDataType::Float32 => { - let value: Option = sqlx::query_scalar(sql) - .bind(self.format_key(key)) - .fetch_optional(pool) - .await?; - Ok(value.map(|v| Scalar::Number(NumberScalar::Float32(v.into())))) + fetch_single_row_by_sqlx!(pool, new_sql, key_type, f32, |v: f32| { + Scalar::Number(NumberScalar::Float32(v.into())) + }) } NumberDataType::Float64 => { - let value: Option = sqlx::query_scalar(sql) - .bind(self.format_key(key)) - .fetch_optional(pool) - .await?; - Ok(value.map(|v| Scalar::Number(NumberScalar::Float64(v.into())))) + fetch_single_row_by_sqlx!(pool, new_sql, key_type, f64, |v: f64| { + Scalar::Number(NumberScalar::Float64(v.into())) + }) } }) } _ => Err(ErrorCode::DictionarySourceError(format!( - "MySQL dictionary operator currently does not support value type {data_type}" + "MySQL dictionary operator currently does not support value type {value_type}" ))), } } + async fn get_column_values_from_mysql( + &self, + column: &Column, + value_type: &DataType, + default_value: &Scalar, + pool: &MySqlPool, + sql: &String, + ) -> Result> { + // todo: The current method formats the key as a string, which causes some performance overhead. + // The next step is to use the key's native types directly, such as bool, i32, etc. + let key_cnt = column.len(); + let mut all_keys = Vec::with_capacity(key_cnt); + let mut key_set = HashSet::with_capacity(key_cnt); + for item in column.iter() { + if item != ScalarRef::Null { + key_set.insert(item.clone()); + } + all_keys.push(self.format_key(item)); + } + + let mut builder = ColumnBuilder::with_capacity(value_type, key_cnt); + if key_set.is_empty() { + for _ in 0..key_cnt { + builder.push(default_value.as_ref()); + } + return Ok(Value::Column(builder.build())); + } + let new_sql = format!("{} ({})", sql, self.format_keys(key_set)); + let key_type = column.data_type().remove_nullable(); + match value_type.remove_nullable() { + DataType::Boolean => { + let kv_pairs: HashMap = + fetch_all_rows_by_sqlx!(pool, &new_sql, key_type, bool, |k| self.format_key(k)); + for key in all_keys { + match kv_pairs.get(&key) { + Some(v) => builder.push(Scalar::Boolean(*v).as_ref()), + None => builder.push(default_value.as_ref()), + } + } + } + DataType::String => { + let kv_pairs: HashMap = + fetch_all_rows_by_sqlx!(pool, &new_sql, key_type, String, |k| self + .format_key(k)); + for key in all_keys { + match kv_pairs.get(&key) { + Some(v) => builder.push(Scalar::String(v.to_string()).as_ref()), + None => builder.push(default_value.as_ref()), + } + } + } + DataType::Number(num_ty) => { + with_integer_mapped_type!(|NUM_TYPE| match num_ty { + NumberDataType::NUM_TYPE => { + let kv_pairs: HashMap = + fetch_all_rows_by_sqlx!(pool, &new_sql, key_type, NUM_TYPE, |k| self + .format_key(k)); + for key in all_keys { + match kv_pairs.get(&key) { + Some(v) => builder + .push(Scalar::Number(NUM_TYPE::upcast_scalar(*v)).as_ref()), + None => builder.push(default_value.as_ref()), + } + } + } + NumberDataType::Float32 => { + let kv_pairs: HashMap = + fetch_all_rows_by_sqlx!(pool, &new_sql, key_type, f32, |k| self + .format_key(k)); + for key in all_keys { + match kv_pairs.get(&key) { + Some(v) => builder.push( + Scalar::Number(NumberScalar::Float32((*v).into())).as_ref(), + ), + None => builder.push(default_value.as_ref()), + } + } + } + NumberDataType::Float64 => { + let kv_pairs: HashMap = + fetch_all_rows_by_sqlx!(pool, &new_sql, key_type, f64, |k| self + .format_key(k)); + for key in all_keys { + match kv_pairs.get(&key) { + Some(v) => builder.push( + Scalar::Number(NumberScalar::Float64((*v).into())).as_ref(), + ), + None => builder.push(default_value.as_ref()), + } + } + } + }) + } + _ => { + return Err(ErrorCode::DictionarySourceError(format!( + "MySQL dictionary operator currently does not support value type {value_type}" + ))); + } + } + Ok(Value::Column(builder.build())) + } + + #[inline] fn format_key(&self, key: ScalarRef<'_>) -> String { match key { - ScalarRef::String(s) => s.to_string(), + ScalarRef::String(s) => format!("'{}'", s.replace("'", "\\'")), ScalarRef::Date(d) => format!("{}", date_to_string(d as i64, &TimeZone::UTC)), ScalarRef::Timestamp(t) => { format!("{}", timestamp_to_string(t, &TimeZone::UTC)) @@ -305,6 +477,17 @@ impl DictionaryOperator { _ => format!("{}", key), } } + + #[inline] + fn format_keys(&self, keys: HashSet) -> String { + format!( + "{}", + keys.into_iter() + .map(|key| self.format_key(key)) + .collect::>() + .join(",") + ) + } } impl TransformAsyncFunction { @@ -339,8 +522,11 @@ impl TransformAsyncFunction { sqlx::MySqlPool::connect(&sql_source.connection_url), )?; let sql = format!( - "SELECT {} FROM {} WHERE {} = ? LIMIT 1", - &sql_source.value_field, &sql_source.table, &sql_source.key_field + "SELECT {}, {} FROM {} WHERE {} in", + &sql_source.key_field, + &sql_source.value_field, + &sql_source.table, + &sql_source.key_field ); operators.insert(i, Arc::new(DictionaryOperator::Mysql((mysql_pool, sql)))); } diff --git a/tests/sqllogictests/src/mock_source/mysql_source.rs b/tests/sqllogictests/src/mock_source/mysql_source.rs index d8a314c278ff..6a8c1708c2c2 100644 --- a/tests/sqllogictests/src/mock_source/mysql_source.rs +++ b/tests/sqllogictests/src/mock_source/mysql_source.rs @@ -24,7 +24,6 @@ use msql_srv::MysqlShim; use msql_srv::QueryResultWriter; use msql_srv::StatementMetaWriter; use mysql_common::Value; -use sqlparser::ast::BinaryOperator; use sqlparser::ast::Expr; use sqlparser::ast::SelectItem; use sqlparser::ast::SetExpr; @@ -73,7 +72,7 @@ struct Backend { block: Vec>, prepared_id: u32, - prepared: HashMap, + prepared: HashMap, Vec)>, } impl Backend { @@ -156,25 +155,26 @@ impl MysqlShim for Backend { let mut table = None; let mut key = None; - let mut value = None; + let mut values = vec![]; + let mut in_list_keys = vec![]; - // Only support simple SQL select one field with an equal filter. - // for example: SELECT name FROM user WHERE id = 1; + // Only support SQL select two fields with an in expr. + // for example: SELECT id, name FROM user WHERE id in (1,2,3,4,5); if asts.len() == 1 { if let Statement::Query(query) = &asts[0] { if let SetExpr::Select(select) = *query.body.clone() { - if let SelectItem::UnnamedExpr(Expr::Identifier(ident)) = &select.projection[0] - { - value = Some(ident.value.clone()); + for proj in select.projection { + if let SelectItem::UnnamedExpr(Expr::Identifier(ident)) = &proj { + values.push(Some(ident.value.clone())); + } } if let TableFactor::Table { name, .. } = &select.from[0].relation { table = Some(name.0[0].value.clone()); } - if let Some(Expr::BinaryOp { left, op, .. }) = &select.selection { - if op == &BinaryOperator::Eq { - if let Expr::Identifier(ident) = *left.clone() { - key = Some(ident.value.clone()); - } + if let Some(Expr::InList { expr, list, .. }) = &select.selection { + if let Expr::Identifier(ident) = *expr.clone() { + key = Some(ident.value.clone()); + in_list_keys.extend(list.clone()); } } } @@ -184,36 +184,44 @@ impl MysqlShim for Backend { self.prepared_id += 1; let prepared_id = self.prepared_id; - if table.is_some() && key.is_some() && value.is_some() { + if table.is_some() && key.is_some() && !values.is_empty() { let table = table.unwrap(); - let key = key.unwrap(); - let value = value.unwrap(); + let key = key.unwrap(); let key_col = &self .schema .iter() .enumerate() .find(|&(_, f)| f.column == key); - let value_col = &self - .schema - .iter() - .enumerate() - .find(|&(_, f)| f.column == value); + let mut value_columns = vec![]; + for value in values { + let value = value.unwrap(); + let value_col = &self + .schema + .iter() + .enumerate() + .find(|&(_, f)| f.column == value); + value_columns.push(*value_col); + } - if table == self.table && key_col.is_some() && value_col.is_some() { + if table == self.table && key_col.is_some() && !value_columns.is_empty() { let (key_idx, key_col) = key_col.unwrap(); - let (value_idx, value_col) = value_col.unwrap(); - - let prepared_idices = (key_idx, value_idx); - self.prepared.insert(prepared_id, prepared_idices); // keys are bind as string type. let mut key_col = key_col.clone(); key_col.coltype = ColumnType::MYSQL_TYPE_VAR_STRING; let key_cols = vec![key_col]; - let value_cols = vec![value_col.clone()]; + let mut value_cols = vec![]; + let mut value_idx_vec = vec![]; + for value_col in value_columns { + let (value_idx, value_col) = value_col.unwrap(); + value_idx_vec.push(value_idx); + value_cols.push(value_col.clone()); + } + let prepared_idices = (key_idx, value_idx_vec, in_list_keys); + self.prepared.insert(prepared_id, prepared_idices); // add key and value columns for execute. return info.reply(prepared_id, key_cols.as_slice(), value_cols.as_slice()); @@ -227,61 +235,97 @@ impl MysqlShim for Backend { fn on_execute( &mut self, id: u32, - param_parser: msql_srv::ParamParser, + _: msql_srv::ParamParser, results: QueryResultWriter, ) -> io::Result<()> { - let params: Vec<_> = param_parser - .into_iter() - .map(|p| p.value) - .collect::>(); - - // ignore if params are empty. - if params.len() != 1 { - return results.completed(0, 0); - } - let param = params[0]; - - let (key_idx, value_idx) = self.prepared.get(&id).unwrap(); - + let (key_idx, value_idx_vec, in_list_keys) = self.prepared.get(&id).unwrap(); let key_field = self.schema[*key_idx].clone(); let key_column = self.block[*key_idx].clone(); - let mut row = None; - // find matched row by compare key params. + // find matched rows by compare key params. + let mut rows: Vec> = vec![]; match key_field.coltype { - ColumnType::MYSQL_TYPE_TINY - | ColumnType::MYSQL_TYPE_SHORT - | ColumnType::MYSQL_TYPE_LONG - | ColumnType::MYSQL_TYPE_LONGLONG => { - let param: &str = param.into(); - let key = param.parse::().unwrap(); - let key_param = Value::Int(key); - for (i, key) in key_column.iter().enumerate() { - if key == &key_param { - row = Some(i); - break; + ColumnType::MYSQL_TYPE_TINY => { + for param in in_list_keys { + let param = format!("{}", param); + if param == "NULL" { + continue; + } + let key = param.parse::().unwrap(); + let key_param = Value::Int(key.into()); + for (i, key) in key_column.iter().enumerate() { + if key == &key_param { + rows.push(Some(i)); + break; + } + } + } + } + ColumnType::MYSQL_TYPE_SHORT => { + for param in in_list_keys { + let param = format!("{}", param); + if param == "NULL" { + continue; + } + let key = param.parse::().unwrap(); + let key_param = Value::UInt(key); + for (i, key) in key_column.iter().enumerate() { + if key == &key_param { + rows.push(Some(i)); + break; + } + } + } + } + ColumnType::MYSQL_TYPE_LONG | ColumnType::MYSQL_TYPE_LONGLONG => { + for param in in_list_keys { + let param = format!("{}", param); + if param == "NULL" { + continue; + } + let key = param.parse::().unwrap(); + let key_param = Value::Int(key); + for (i, key) in key_column.iter().enumerate() { + if key == &key_param { + rows.push(Some(i)); + break; + } } } } ColumnType::MYSQL_TYPE_FLOAT | ColumnType::MYSQL_TYPE_DOUBLE => { - let param: &str = param.into(); - let key = param.parse::().unwrap(); - let key_param = Value::Double(key); - for (i, key) in key_column.iter().enumerate() { - if key == &key_param { - row = Some(i); - break; + for param in in_list_keys { + let param = format!("{}", param); + if param == "NULL" { + continue; + } + let key = param.parse::().unwrap(); + let key_param = Value::Double(key); + for (i, key) in key_column.iter().enumerate() { + if key == &key_param { + rows.push(Some(i)); + break; + } } } } ColumnType::MYSQL_TYPE_VAR_STRING => { - let param: &str = param.into(); - let key = param.as_bytes().to_vec(); - let key_param = Value::Bytes(key); - for (i, key) in key_column.iter().enumerate() { - if key == &key_param { - row = Some(i); - break; + for param in in_list_keys { + let param = format!("{}", param); + if param == "NULL" { + continue; + } + let param_str = param + .strip_prefix('\'') + .and_then(|s| s.strip_suffix('\'')) + .unwrap_or(¶m); + let key = param_str.as_bytes().to_vec(); + let key_param = Value::Bytes(key); + for (i, key) in key_column.iter().enumerate() { + if key == &key_param { + rows.push(Some(i)); + break; + } } } } @@ -289,65 +333,79 @@ impl MysqlShim for Backend { } // return NULL if params not matched. - if row.is_none() { + if rows.is_empty() { return results.completed(0, 0); } - let row = row.unwrap(); - let value_field = self.schema[*value_idx].clone(); - let value_column = self.block[*value_idx].clone(); - let value = value_column[row].clone(); + let value_idx1 = value_idx_vec[0]; + let value_field1 = self.schema[value_idx1].clone(); + let value_column1 = self.block[value_idx1].clone(); + + let value_idx2 = value_idx_vec[1]; + let value_field2 = self.schema[value_idx2].clone(); + let value_column2 = self.block[value_idx2].clone(); - let cols = vec![value_field.clone()]; + let cols = vec![value_field1.clone(), value_field2.clone()]; let mut rw = results.start(&cols)?; - match value { - Value::Bytes(v) => { - rw.write_col(v)?; - } - Value::Int(v) => match value_field.coltype { - ColumnType::MYSQL_TYPE_TINY => { - rw.write_col(v as i8)?; - } - ColumnType::MYSQL_TYPE_SHORT => { - rw.write_col(v as i16)?; - } - ColumnType::MYSQL_TYPE_LONG => { - rw.write_col(v as i32)?; - } - ColumnType::MYSQL_TYPE_LONGLONG => { - rw.write_col(v)?; - } - _ => { - unreachable!() - } - }, - Value::UInt(v) => match value_field.coltype { - ColumnType::MYSQL_TYPE_TINY => { - rw.write_col(v as u8)?; - } - ColumnType::MYSQL_TYPE_SHORT => { - rw.write_col(v as u16)?; - } - ColumnType::MYSQL_TYPE_LONG => { - rw.write_col(v as u32)?; - } - ColumnType::MYSQL_TYPE_LONGLONG => { - rw.write_col(v)?; - } - _ => { - unreachable!() + + for row in rows.into_iter().map(|r| r.unwrap()) { + let value1 = value_column1[row].clone(); + let value2 = value_column2[row].clone(); + for (value, value_field) in [ + (value1, value_field1.clone()), + (value2, value_field2.clone()), + ] { + match value { + Value::Bytes(v) => { + rw.write_col(v)?; + } + Value::Int(v) => match value_field.coltype { + ColumnType::MYSQL_TYPE_TINY => { + rw.write_col(v as i8)?; + } + ColumnType::MYSQL_TYPE_SHORT => { + rw.write_col(v as u16)?; + } + ColumnType::MYSQL_TYPE_LONG => { + rw.write_col(v as i32)?; + } + ColumnType::MYSQL_TYPE_LONGLONG => { + rw.write_col(v)?; + } + _ => { + unreachable!() + } + }, + Value::UInt(v) => match value_field.coltype { + ColumnType::MYSQL_TYPE_TINY => { + rw.write_col(v as u8)?; + } + ColumnType::MYSQL_TYPE_SHORT => { + rw.write_col(v as u16)?; + } + ColumnType::MYSQL_TYPE_LONG => { + rw.write_col(v as u32)?; + } + ColumnType::MYSQL_TYPE_LONGLONG => { + rw.write_col(v)?; + } + _ => { + unreachable!() + } + }, + Value::Float(v) => { + rw.write_col(v)?; + } + Value::Double(v) => { + rw.write_col(v)?; + } + _ => { + rw.write_col("")?; + } } - }, - Value::Float(v) => { - rw.write_col(v)?; - } - Value::Double(v) => { - rw.write_col(v)?; - } - _ => { - rw.write_col("")?; } + rw.end_row()?; } rw.finish() } diff --git a/tests/sqllogictests/suites/query/functions/02_0077_function_dict_get.test b/tests/sqllogictests/suites/query/functions/02_0077_function_dict_get.test index bdbe8df06a0b..4a84dee72401 100644 --- a/tests/sqllogictests/suites/query/functions/02_0077_function_dict_get.test +++ b/tests/sqllogictests/suites/query/functions/02_0077_function_dict_get.test @@ -148,57 +148,149 @@ NULL default_value NULL default_value statement ok -create or replace table mysql_t1(id int, name string) +CREATE OR REPLACE DICTIONARY mysql_dic_id(id int, name string, age uint16, salary float, active bool) PRIMARY KEY id SOURCE(mysql(host='localhost' port='3106' username='root' password='123456' db='test' table='user')); statement ok -insert into mysql_t1 values(1, 'Alice'),(2, 'Bob'),(3, 'Lily'),(4, 'Tom'),(5, 'Tim') +CREATE OR REPLACE DICTIONARY mysql_dic_id_not_null(id int not null default 0, name string not null default 'default_name', age uint16 not null default 0, salary float not null default 0.0, active bool not null default false) PRIMARY KEY id SOURCE(mysql(host='localhost' port='3106' username='root' password='123456' db='test' table='user')); statement ok -CREATE OR REPLACE DICTIONARY mysql_d1(id int, name string, age uint16, salary float, active bool) PRIMARY KEY id SOURCE(mysql(host='localhost' port='3106' username='root' password='123456' db='test' table='user')); +CREATE OR REPLACE DICTIONARY mysql_dic_name(id int, name string, age uint16, salary float, active bool) PRIMARY KEY name SOURCE(mysql(host='localhost' port='3106' username='root' password='123456' db='test' table='user')); -query TIFT -select dict_get(mysql_d1, 'name', 1), dict_get(mysql_d1, 'age', 1), dict_get(mysql_d1, 'salary', 1), dict_get(mysql_d1, 'active', 1) +statement ok +CREATE OR REPLACE DICTIONARY mysql_dic_age(id int, name string, age uint16, salary float, active bool) PRIMARY KEY age SOURCE(mysql(host='localhost' port='3106' username='root' password='123456' db='test' table='user')); + +statement ok +CREATE OR REPLACE DICTIONARY mysql_dic_salary(id int, name string, age uint16, salary float, active bool) PRIMARY KEY salary SOURCE(mysql(host='localhost' port='3106' username='root' password='123456' db='test' table='user')); + +statement ok +CREATE OR REPLACE DICTIONARY mysql_dic_active(id int, name string, age uint16, salary float, active bool) PRIMARY KEY active SOURCE(mysql(host='localhost' port='3106' username='root' password='123456' db='test' table='user')); + +# Scalar +query ITIFT +select dict_get(mysql_dic_id, 'id', 1), dict_get(mysql_dic_id, 'name', 1), dict_get(mysql_dic_id, 'age', 1), dict_get(mysql_dic_id, 'salary', 1), dict_get(mysql_dic_id, 'active', 1) ---- -Alice 24 100.0 1 +1 Alice 24 100.0 1 -query TIFT -select dict_get(mysql_d1, 'name', 5), dict_get(mysql_d1, 'age', 5), dict_get(mysql_d1, 'salary', 5), dict_get(mysql_d1, 'active', 5) +query ITIFT +select dict_get(mysql_dic_id, 'id', 5), dict_get(mysql_dic_id, 'name', 5), dict_get(mysql_dic_id, 'age', 5), dict_get(mysql_dic_id, 'salary', 5), dict_get(mysql_dic_id, 'active', 5) ---- -NULL NULL NULL NULL +NULL NULL NULL NULL NULL query ITIFT -select id, dict_get(mysql_d1, 'name', id), dict_get(mysql_d1, 'age', id), dict_get(mysql_d1, 'salary', id), dict_get(mysql_d1, 'active', id) from mysql_t1 +select dict_get(mysql_dic_id_not_null, 'id', 5), dict_get(mysql_dic_id_not_null, 'name', 5), dict_get(mysql_dic_id_not_null, 'age', 5), dict_get(mysql_dic_id_not_null, 'salary', 5), dict_get(mysql_dic_id_not_null, 'active', 5) +---- +0 default_name 0 0.0 0 + +statement error 1006 +select dict_get(mysql_dic_id, 'id2', 5) + +statement error 1006 +select dict_get(mysql_dic_id, 'id', 'Alice') + +query ITIFT +select dict_get(mysql_dic_name, 'id', 'Alice'), dict_get(mysql_dic_name, 'name', 'Alice'), dict_get(mysql_dic_name, 'age', 'Alice'), dict_get(mysql_dic_name, 'salary', 'Alice'), dict_get(mysql_dic_name, 'active', 'Alice') ---- 1 Alice 24 100.0 1 -2 Bob 35 200.1 0 -3 Lily 41 1000.2 1 -4 Tom 55 3000.55 0 -5 NULL NULL NULL NULL -query ITI -select id, name, dict_get(mysql_d1, 'age', id) as age from mysql_t1 where age > 35 +query ITIFT +select dict_get(mysql_dic_name, 'id', '\'Alice\''), dict_get(mysql_dic_name, 'name', '\'Alice\''), dict_get(mysql_dic_name, 'age', '\'Alice\''), dict_get(mysql_dic_name, 'salary', '\'Alice\''), dict_get(mysql_dic_name, 'active', '\'Alice\'') +---- +NULL NULL NULL NULL NULL + +query ITIFT +select dict_get(mysql_dic_age, 'id', 24), dict_get(mysql_dic_age, 'name', 24), dict_get(mysql_dic_age, 'age', 24), dict_get(mysql_dic_age, 'salary', 24), dict_get(mysql_dic_age, 'active', 24) +---- +1 Alice 24 100.0 1 + +query ITIFT +select dict_get(mysql_dic_age, 'id', 999), dict_get(mysql_dic_age, 'name', 999), dict_get(mysql_dic_age, 'age', 999), dict_get(mysql_dic_age, 'salary', 999), dict_get(mysql_dic_age, 'active', 999) +---- +NULL NULL NULL NULL NULL + +query ITIFT +select dict_get(mysql_dic_salary, 'id', 100.0), dict_get(mysql_dic_salary, 'name', 100.0), dict_get(mysql_dic_salary, 'age', 100.0), dict_get(mysql_dic_salary, 'salary', 100.0), dict_get(mysql_dic_salary, 'active', 100.0) +---- +1 Alice 24 100.0 1 + +query ITIFT +select dict_get(mysql_dic_salary, 'id', -1.0), dict_get(mysql_dic_salary, 'name', -1.0), dict_get(mysql_dic_salary, 'age', -1.0), dict_get(mysql_dic_salary, 'salary', -1.0), dict_get(mysql_dic_salary, 'active', -1.0) ---- -3 Lily 41 -4 Tom 55 +NULL NULL NULL NULL NULL + +query ITIFT +select dict_get(mysql_dic_active, 'id', true), dict_get(mysql_dic_active, 'name', true), dict_get(mysql_dic_active, 'age', true), dict_get(mysql_dic_active, 'salary', true), dict_get(mysql_dic_active, 'active', true) +---- +1 Alice 24 100.0 1 + +# Column +statement ok +create or replace table mysql_t(id int, name string, age uint16, salary float, active bool) statement ok -CREATE OR REPLACE DICTIONARY mysql_d2(id int, name string, age uint16, salary float, active bool) PRIMARY KEY name SOURCE(mysql(host='localhost' port='3106' username='root' password='123456' db='test' table='user')); +insert into mysql_t values(1, 'Alice', 24, 100.0, true),(2, 'Bob', 35, 200.1, false),(3, '\'Lily\'', 41, 1000.2, true),(4, '\'\'Tom\'\'', 55, 3000.55, 0),(null, null, null, null, null) -query TIFT -select dict_get(mysql_d2, 'id', 'Alice'), dict_get(mysql_d2, 'age', 'Alice'), dict_get(mysql_d2, 'salary', 'Alice'), dict_get(mysql_d2, 'active', 'Alice') +query IITIFT +select id, dict_get(mysql_dic_id, 'id', id), dict_get(mysql_dic_id, 'name', id), dict_get(mysql_dic_id, 'age', id), dict_get(mysql_dic_id, 'salary', id), dict_get(mysql_dic_id, 'active', id) from mysql_t ---- -1 24 100.0 1 +1 1 Alice 24 100.0 1 +2 2 Bob 35 200.1 0 +3 3 Lily 41 1000.2 1 +4 4 Tom 55 3000.55 0 +NULL NULL NULL NULL NULL NULL -query TIFT -select dict_get(mysql_d2, 'id', 'Nancy'), dict_get(mysql_d2, 'age', 'Nancy'), dict_get(mysql_d2, 'salary', 'Nancy'), dict_get(mysql_d2, 'active', 'Nancy') +query ITIFT +select id, dict_get(mysql_dic_id_not_null, 'id', id), dict_get(mysql_dic_id_not_null, 'name', id), dict_get(mysql_dic_id_not_null, 'age', id), dict_get(mysql_dic_id_not_null, 'salary', id), dict_get(mysql_dic_id_not_null, 'active', id) from mysql_t ---- -NULL NULL NULL NULL +1 1 Alice 24 100.0 1 +2 2 Bob 35 200.1 0 +3 3 Lily 41 1000.2 1 +4 4 Tom 55 3000.55 0 +NULL 0 default_name 0 0.0 0 + +query ITI +select id, name, dict_get(mysql_dic_id, 'age', id) as age from mysql_t where age > 35 +---- +3 'Lily' 41 +4 ''Tom'' 55 query ITIFT -select name, dict_get(mysql_d2, 'id', name), dict_get(mysql_d2, 'age', name), dict_get(mysql_d2, 'salary', name), dict_get(mysql_d2, 'active', name) from mysql_t1 +select name, dict_get(mysql_dic_name, 'id', name), dict_get(mysql_dic_name, 'age', name), dict_get(mysql_dic_name, 'salary', name), dict_get(mysql_dic_name, 'active', name) from mysql_t ---- Alice 1 24 100.0 1 Bob 2 35 200.1 0 -Lily 3 41 1000.2 1 -Tom 4 55 3000.55 0 -Tim NULL NULL NULL NULL +'Lily' NULL NULL NULL NULL +''Tom'' NULL NULL NULL NULL +NULL NULL NULL NULL NULL + +query IITFT +select age, dict_get(mysql_dic_age, 'id', age), dict_get(mysql_dic_age, 'name', age), dict_get(mysql_dic_age, 'salary', age), dict_get(mysql_dic_age, 'active', age) from mysql_t +---- +24 1 Alice 100.0 1 +35 2 Bob 200.1 0 +41 3 Lily 1000.2 1 +55 4 Tom 3000.55 0 +NULL NULL NULL NULL NULL + +query FITIT +select salary, dict_get(mysql_dic_salary, 'id', salary), dict_get(mysql_dic_salary, 'name', salary), dict_get(mysql_dic_salary, 'age', salary), dict_get(mysql_dic_salary, 'active', salary) from mysql_t +---- +100.0 1 Alice 24 1 +200.1 2 Bob 35 0 +1000.2 3 Lily 41 1 +3000.55 4 Tom 55 0 +NULL NULL NULL NULL NULL + +# There may be multiple rows per active value in the `user` table. So the result is nondeterminity. It could also be: +# 1 3 Lily 41 1000.2 +# 0 4 Tom 55 3000.55 +# 1 3 Lily 41 1000.2 +# 0 4 Tom 55 3000.55 +# NULL NULL NULL NULL NULL +query TITIF +select active, dict_get(mysql_dic_active, 'id', active), dict_get(mysql_dic_active, 'name', active), dict_get(mysql_dic_active, 'age', active), dict_get(mysql_dic_active, 'salary', active) from mysql_t +---- +1 1 Alice 24 100.0 +0 2 Bob 35 200.1 +1 1 Alice 24 100.0 +0 2 Bob 35 200.1 +NULL NULL NULL NULL NULL