feat(json2): type hint (#8247)

* feat(json2): type hint

* test(datatypes): add JsonSettings serde round-trip test

* minor refactor

This reverts commit 7ff5a5249a09be5396536284fe822b5761ef4e6a.

* fix: code review
This commit is contained in:
fys
2026-06-30 07:37:31 +00:00
committed by GitHub
parent d3cc1b1888
commit aa563628fb
26 changed files with 1756 additions and 2216 deletions
+4 -11
View File
@@ -215,13 +215,6 @@ pub enum Error {
location: Location,
},
#[snafu(display("Invalid JSON structure setting, reason: {reason}"))]
InvalidJsonStructureSetting {
reason: String,
#[snafu(implicit)]
location: Location,
},
#[snafu(display("Failed to serialize column default constraint"))]
SerializeColumnDefaultConstraint {
#[snafu(implicit)]
@@ -348,7 +341,7 @@ pub enum Error {
},
#[snafu(display("Failed to set JSON structure settings: {value}"))]
SetJsonStructureSettings {
SetJsonSettings {
value: String,
source: datatypes::error::Error,
#[snafu(implicit)]
@@ -381,7 +374,6 @@ impl ErrorExt for Error {
InvalidColumnOption { .. }
| InvalidExprAsOptionValue { .. }
| InvalidJsonStructureSetting { .. }
| InvalidDatabaseName { .. }
| InvalidDatabaseOption { .. }
| ColumnTypeMismatch { .. }
@@ -400,8 +392,9 @@ impl ErrorExt for Error {
#[cfg(feature = "enterprise")]
InvalidTriggerWebhookOption { .. } => StatusCode::InvalidArguments,
SerializeColumnDefaultConstraint { source, .. }
| SetJsonStructureSettings { source, .. } => source.status_code(),
SerializeColumnDefaultConstraint { source, .. } | SetJsonSettings { source, .. } => {
source.status_code()
}
ConvertToGrpcDataType { source, .. } => source.status_code(),
SqlCommon { source, .. } => source.status_code(),
+1 -2
View File
@@ -372,8 +372,7 @@ mod tests {
let ts_col = columns.first().unwrap();
assert_eq!(
expected_type,
sql_data_type_to_concrete_data_type(ts_col.data_type(), &Default::default())
.unwrap()
sql_data_type_to_concrete_data_type(ts_col.data_type()).unwrap()
);
}
_ => unreachable!(),
+9 -8
View File
@@ -710,12 +710,13 @@ impl<'a> ParserContext<'a> {
let mut extensions = ColumnExtensions::default();
let data_type = parser.parse_data_type().context(SyntaxSnafu)?;
// Must immediately parse the JSON datatype format because it is closely after the "JSON"
// datatype, like this: "JSON(format = ...)".
if matches!(data_type, DataType::JSON) {
extensions.json_datatype_options = json::parse_json_datatype_options(parser)?;
}
let data_type =
if let Some((data_type, type_hints)) = json::parse_json2_type_and_hints(parser)? {
extensions.json_type_hints = type_hints;
data_type
} else {
parser.parse_data_type().context(SyntaxSnafu)?
};
let mut options = vec![];
loop {
@@ -910,7 +911,7 @@ impl<'a> ParserContext<'a> {
);
let column_type = get_unalias_type(column_type);
let data_type = sql_data_type_to_concrete_data_type(&column_type, column_extensions)?;
let data_type = sql_data_type_to_concrete_data_type(&column_type)?;
ensure!(
data_type == ConcreteDataType::string_datatype(),
InvalidColumnOptionSnafu {
@@ -1007,7 +1008,7 @@ impl<'a> ParserContext<'a> {
// Check that column is a vector type
let column_type = get_unalias_type(column_type);
let data_type = sql_data_type_to_concrete_data_type(&column_type, column_extensions)?;
let data_type = sql_data_type_to_concrete_data_type(&column_type)?;
ensure!(
matches!(data_type, ConcreteDataType::Vector(_)),
InvalidColumnOptionSnafu {
+498 -138
View File
@@ -12,163 +12,523 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use snafu::ResultExt;
use snafu::{ResultExt, ensure};
use sqlparser::ast::{DataType, ExactNumberInfo, Expr, ObjectName, UnaryOperator};
use sqlparser::dialect::keywords::Keyword;
use sqlparser::parser::Parser;
use sqlparser::tokenizer::Token;
use crate::error::{Result, SyntaxSnafu};
use crate::statements::OptionMap;
use crate::util;
use crate::ast::Ident;
use crate::error::{InvalidSqlSnafu, Result, SyntaxSnafu};
use crate::parsers::create_parser::{INVERTED, SKIPPING};
use crate::statements::create::JsonTypeHint;
use crate::statements::transform::type_alias::get_type_by_alias;
pub(super) fn parse_json_datatype_options(parser: &mut Parser<'_>) -> Result<Option<OptionMap>> {
if parser.consume_token(&Token::LParen) {
let result = parser
.parse_comma_separated0(Parser::parse_sql_option, Token::RParen)
.context(SyntaxSnafu)
.and_then(|options| {
options
.into_iter()
.map(util::parse_option_string)
.collect::<Result<Vec<_>>>()
})?;
parser.expect_token(&Token::RParen).context(SyntaxSnafu)?;
Ok(Some(OptionMap::new(result)))
} else {
Ok(None)
const JSON2_TYPE_NAME: &str = "JSON2";
pub(super) fn parse_json2_type_and_hints(
parser: &mut Parser<'_>,
) -> Result<Option<(DataType, Vec<JsonTypeHint>)>> {
let token = parser.peek_token();
let Token::Word(word) = &token.token else {
return Ok(None);
};
if !word.value.eq_ignore_ascii_case(JSON2_TYPE_NAME) || word.quote_style.is_some() {
return Ok(None);
}
parser.next_token();
let data_type = DataType::Custom(ObjectName::from(vec![Ident::new(JSON2_TYPE_NAME)]), vec![]);
let type_hints = if parser.consume_token(&Token::LParen) {
parse_json2_type_hints(parser)?
} else {
vec![]
};
Ok(Some((data_type, type_hints)))
}
fn parse_json2_type_hints(parser: &mut Parser<'_>) -> Result<Vec<JsonTypeHint>> {
let mut hints = Vec::new();
if parser.consume_token(&Token::RParen) {
return Ok(hints);
}
loop {
let hint = parse_json2_type_hint(parser)?;
ensure_no_path_conflict(&hints, &hint.path)?;
hints.push(hint);
if parser.consume_token(&Token::Comma) {
if parser.consume_token(&Token::RParen) {
break;
}
} else {
parser.expect_token(&Token::RParen).context(SyntaxSnafu)?;
break;
}
}
Ok(hints)
}
fn parse_json2_type_hint(parser: &mut Parser<'_>) -> Result<JsonTypeHint> {
let path = parse_json2_path(parser)?;
let data_type = parser.parse_data_type().context(SyntaxSnafu)?;
let data_type = normalize_json2_type_hint_type(data_type)?;
let mut nullable = true;
let mut nullable_set = false;
let mut default = None;
let mut inverted_index = false;
loop {
if parser.parse_keywords(&[Keyword::NOT, Keyword::NULL]) {
ensure!(
!nullable_set,
InvalidSqlSnafu {
msg: format!(
"NULL/NOT NULL option already specified for JSON2 type hint '{}'",
path.join(".")
)
}
);
nullable = false;
nullable_set = true;
} else if parser.parse_keyword(Keyword::NULL) {
ensure!(
!nullable_set,
InvalidSqlSnafu {
msg: format!(
"NULL/NOT NULL option already specified for JSON2 type hint '{}'",
path.join(".")
)
}
);
nullable = true;
nullable_set = true;
} else if parser.parse_keyword(Keyword::DEFAULT) {
ensure!(
default.is_none(),
InvalidSqlSnafu {
msg: format!(
"duplicated DEFAULT option for JSON2 type hint '{}'",
path.join(".")
)
}
);
let expr = parser.parse_expr().context(SyntaxSnafu)?;
ensure_json2_default_expr_is_literal(&expr)?;
default = Some(expr);
} else if let Token::Word(word) = parser.peek_token().token
&& word.value.eq_ignore_ascii_case(INVERTED)
{
parser.next_token();
ensure!(
parser.parse_keyword(Keyword::INDEX),
InvalidSqlSnafu {
msg: format!(
"expect INDEX after INVERTED keyword for JSON2 type hint '{}'",
path.join(".")
)
}
);
ensure!(
!inverted_index,
InvalidSqlSnafu {
msg: format!(
"duplicated INVERTED INDEX option for JSON2 type hint '{}'",
path.join(".")
)
}
);
inverted_index = true;
} else if let Token::Word(word) = parser.peek_token().token
&& word.value.eq_ignore_ascii_case(SKIPPING)
{
return InvalidSqlSnafu {
msg: "JSON2 type hint SKIPPING INDEX is not supported yet".to_string(),
}
.fail();
} else if matches!(parser.peek_token().token, Token::Comma | Token::RParen) {
break;
} else {
return parser
.expected("JSON2 type hint option", parser.peek_token())
.context(SyntaxSnafu);
}
}
Ok(JsonTypeHint {
path,
data_type,
nullable,
default,
inverted_index,
})
}
fn parse_json2_path(parser: &mut Parser<'_>) -> Result<Vec<String>> {
let first = parser.parse_identifier().context(SyntaxSnafu)?;
let mut path = vec![first.value];
while parser.consume_token(&Token::Period) {
let segment = parser.parse_identifier().context(SyntaxSnafu)?;
path.push(segment.value);
}
ensure!(
!path.iter().any(|segment| segment.is_empty()),
InvalidSqlSnafu {
msg: "JSON2 type hint path segment cannot be empty".to_string(),
}
);
Ok(path)
}
fn normalize_json2_type_hint_type(data_type: DataType) -> Result<DataType> {
let data_type = get_type_by_alias(&data_type).unwrap_or(data_type);
let normalized = match data_type {
DataType::String(_) | DataType::Text | DataType::Varchar(_) | DataType::Char(_) => {
DataType::String(None)
}
DataType::TinyInt(_)
| DataType::SmallInt(_)
| DataType::Int(_)
| DataType::Integer(_)
| DataType::BigInt(_) => DataType::BigInt(None),
DataType::TinyIntUnsigned(_)
| DataType::SmallIntUnsigned(_)
| DataType::IntUnsigned(_)
| DataType::UnsignedInteger
| DataType::BigIntUnsigned(_) => DataType::BigIntUnsigned(None),
DataType::Float(_) | DataType::Real | DataType::Double(_) => {
DataType::Double(ExactNumberInfo::None)
}
DataType::Boolean => DataType::Boolean,
_ => {
return InvalidSqlSnafu {
msg: format!("unsupported JSON2 type hint data type: {data_type}"),
}
.fail();
}
};
Ok(normalized)
}
fn ensure_json2_default_expr_is_literal(expr: &Expr) -> Result<()> {
let is_literal = match expr {
Expr::Value(_) => true,
Expr::UnaryOp { op, expr } => {
matches!(op, UnaryOperator::Plus | UnaryOperator::Minus)
&& matches!(expr.as_ref(), Expr::Value(_))
}
_ => false,
};
ensure!(
is_literal,
InvalidSqlSnafu {
msg: "JSON2 type hint DEFAULT only supports literal values",
}
);
Ok(())
}
fn ensure_no_path_conflict(hints: &[JsonTypeHint], path: &[String]) -> Result<()> {
for hint in hints {
ensure!(
hint.path != path,
InvalidSqlSnafu {
msg: format!("duplicated JSON2 type hint path '{}'", path.join("."))
}
);
ensure!(
!hint.path.starts_with(path) && !path.starts_with(&hint.path),
InvalidSqlSnafu {
msg: format!(
"JSON2 type hint path '{}' conflicts with '{}'",
path.join("."),
hint.path.join(".")
)
}
);
}
Ok(())
}
#[cfg(test)]
mod tests {
use sqlparser::ast::{DataType, Expr, Ident, StructField};
use sqlparser::ast::{DataType, ExactNumberInfo};
use crate::dialect::GreptimeDbDialect;
use crate::parser::{ParseOptions, ParserContext};
use crate::statements::OptionMap;
use crate::statements::create::{
Column, JSON_FORMAT_FULL_STRUCTURED, JSON_FORMAT_PARTIAL, JSON_FORMAT_RAW, JSON_OPT_FIELDS,
JSON_OPT_FORMAT, JSON_OPT_UNSTRUCTURED_KEYS,
};
use crate::statements::create::Column;
use crate::statements::statement::Statement;
use crate::util::OptionValue;
fn parse_json2_column(sql: &str) -> Column {
let Statement::CreateTable(mut create_table) =
ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
.unwrap()
.remove(0)
else {
unreachable!()
};
create_table.columns.remove(0)
}
#[test]
fn test_parse_json_datatype_options() {
fn parse(sql: &str) -> Option<OptionMap> {
let Statement::CreateTable(mut create_table) = ParserContext::create_with_dialect(
fn test_parse_json2_type_hints() {
let column = parse_json2_column(
r#"
CREATE TABLE traces (
log_json_data JSON2 (
"service.name" STRING NOT NULL DEFAULT 'null' INVERTED INDEX,
http.method STRING NOT NULL,
status_code INT64 NOT NULL,
comment STRING NULL,
),
ts TIMESTAMP TIME INDEX,
)"#,
);
assert!(matches!(
column.column_def.data_type,
DataType::Custom(_, _)
));
let hints = column.extensions.json_type_hints;
assert_eq!(hints.len(), 4);
assert_eq!(hints[0].path, vec!["service.name"]);
assert_eq!(hints[0].data_type, DataType::String(None));
assert!(!hints[0].nullable);
assert_eq!(
hints[0]
.default
.as_ref()
.map(|expr| expr.to_string())
.as_deref(),
Some("'null'")
);
assert!(hints[0].inverted_index);
assert_eq!(hints[1].path, vec!["http", "method"]);
assert_eq!(hints[1].data_type, DataType::String(None));
assert!(!hints[1].nullable);
assert!(!hints[1].inverted_index);
assert_eq!(hints[2].path, vec!["status_code"]);
assert_eq!(hints[2].data_type, DataType::BigInt(None));
assert!(!hints[2].nullable);
assert_eq!(hints[3].path, vec!["comment"]);
assert_eq!(hints[3].data_type, DataType::String(None));
assert!(hints[3].nullable);
}
#[test]
fn test_parse_json2_type_hint_default_nullable() {
let column = parse_json2_column(
r#"
CREATE TABLE traces (
log_json_data JSON2 (http.method STRING),
ts TIMESTAMP TIME INDEX,
)"#,
);
let hints = column.extensions.json_type_hints;
assert_eq!(hints.len(), 1);
assert!(hints[0].nullable);
}
#[test]
fn test_parse_json2_type_hint_quoted_path_segments() {
let column = parse_json2_column(
r#"
CREATE TABLE traces (
log_json_data JSON2 (
"a".b STRING,
"x"."y" STRING,
"a.b"."c" STRING,
a."b.c" STRING
),
ts TIMESTAMP TIME INDEX,
)"#,
);
let hints = column.extensions.json_type_hints;
assert_eq!(hints.len(), 4);
assert_eq!(hints[0].path, vec!["a", "b"]);
assert_eq!(hints[1].path, vec!["x", "y"]);
assert_eq!(hints[2].path, vec!["a.b", "c"]);
assert_eq!(hints[3].path, vec!["a", "b.c"]);
}
#[test]
fn test_parse_json2_type_hint_normalizes_numeric_types() {
let column = parse_json2_column(
r#"
CREATE TABLE traces (
log_json_data JSON2 (
tinyint_value TINYINT,
smallint_value SMALLINT,
int_value INT,
integer_value INTEGER,
bigint_value BIGINT,
int64_value INT64,
tinyuint_value TINYINT UNSIGNED,
smalluint_value SMALLINT UNSIGNED,
uint_value INT UNSIGNED,
uint64_value UINT64,
float_value FLOAT,
real_value REAL,
double_value DOUBLE,
float64_value FLOAT64
),
ts TIMESTAMP TIME INDEX,
)"#,
);
let hints = column.extensions.json_type_hints;
assert_eq!(hints.len(), 14);
for hint in hints.iter().take(6) {
assert_eq!(hint.data_type, DataType::BigInt(None));
}
for hint in hints.iter().skip(6).take(4) {
assert_eq!(hint.data_type, DataType::BigIntUnsigned(None));
}
for hint in hints.iter().skip(10) {
assert_eq!(hint.data_type, DataType::Double(ExactNumberInfo::None));
}
}
#[test]
fn test_parse_json2_type_hint_default_accepts_signed_literals() {
let column = parse_json2_column(
r#"
CREATE TABLE traces (
log_json_data JSON2 (
negative_int INT64 DEFAULT -5,
positive_float FLOAT64 DEFAULT +1.5
),
ts TIMESTAMP TIME INDEX,
)"#,
);
let hints = column.extensions.json_type_hints;
assert_eq!(hints.len(), 2);
assert_eq!(
hints[0]
.default
.as_ref()
.map(|expr| expr.to_string())
.as_deref(),
Some("-5")
);
assert_eq!(
hints[1]
.default
.as_ref()
.map(|expr| expr.to_string())
.as_deref(),
Some("+1.5")
);
}
#[test]
fn test_parse_json2_type_hint_default_rejects_function() {
let result = ParserContext::create_with_dialect(
r#"
CREATE TABLE traces (
log_json_data JSON2 (status_code INT64 DEFAULT abs(-1)),
ts TIMESTAMP TIME INDEX,
)"#,
&GreptimeDbDialect {},
ParseOptions::default(),
);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("DEFAULT only supports literal values")
);
}
#[test]
fn test_parse_json2_type_hint_rejects_duplicate_path() {
let result = ParserContext::create_with_dialect(
r#"
CREATE TABLE traces (
log_json_data JSON2 (a.b STRING, a.b INT64),
ts TIMESTAMP TIME INDEX,
)"#,
&GreptimeDbDialect {},
ParseOptions::default(),
);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("duplicated"));
}
#[test]
fn test_parse_json2_type_hint_rejects_parent_child_path() {
let result = ParserContext::create_with_dialect(
r#"
CREATE TABLE traces (
log_json_data JSON2 (a STRING, a.b INT64),
ts TIMESTAMP TIME INDEX,
)"#,
&GreptimeDbDialect {},
ParseOptions::default(),
);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("conflicts"));
}
#[test]
fn test_parse_json2_type_hint_rejects_duplicated_nullability() {
for sql in [
r#"
CREATE TABLE traces (
log_json_data JSON2 (a STRING NULL NULL),
ts TIMESTAMP TIME INDEX,
)"#,
r#"
CREATE TABLE traces (
log_json_data JSON2 (a STRING NOT NULL NOT NULL),
ts TIMESTAMP TIME INDEX,
)"#,
r#"
CREATE TABLE traces (
log_json_data JSON2 (a STRING NOT NULL NULL),
ts TIMESTAMP TIME INDEX,
)"#,
r#"
CREATE TABLE traces (
log_json_data JSON2 (a STRING NULL NOT NULL),
ts TIMESTAMP TIME INDEX,
)"#,
] {
let result = ParserContext::create_with_dialect(
sql,
&GreptimeDbDialect {},
ParseOptions::default(),
)
.unwrap()
.remove(0) else {
unreachable!()
};
);
let Column {
column_def,
extensions,
} = create_table.columns.remove(0);
assert_eq!(column_def.name.to_string(), "my_json");
assert_eq!(column_def.data_type, DataType::JSON);
assert!(column_def.options.is_empty());
extensions.json_datatype_options
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("NULL/NOT NULL option already specified")
);
}
let sql = r#"
CREATE TABLE json_data (
my_json JSON(format = "partial", fields = Struct<i Int, "o.a" String, "o.b" String, `x.y.z` Float64>),
ts TIMESTAMP TIME INDEX,
)"#;
let options = parse(sql).unwrap();
assert_eq!(options.len(), 2);
let option = options.value(JSON_OPT_FIELDS);
let expected = OptionValue::try_new(Expr::Struct {
values: vec![],
fields: vec![
StructField {
field_name: Some(Ident::new("i")),
field_type: DataType::Int(None),
options: None,
},
StructField {
field_name: Some(Ident::with_quote('"', "o.a")),
field_type: DataType::String(None),
options: None,
},
StructField {
field_name: Some(Ident::with_quote('"', "o.b")),
field_type: DataType::String(None),
options: None,
},
StructField {
field_name: Some(Ident::with_quote('`', "x.y.z")),
field_type: DataType::Float64,
options: None,
},
],
})
.ok();
assert_eq!(option, expected.as_ref());
let sql = r#"
CREATE TABLE json_data (
my_json JSON(format = "partial", unstructured_keys = ["k", "foo.bar", "a.b.c"]),
ts TIMESTAMP TIME INDEX,
)"#;
let options = parse(sql).unwrap();
assert_eq!(options.len(), 2);
assert_eq!(
options.value(JSON_OPT_FORMAT).and_then(|x| x.as_string()),
Some(JSON_FORMAT_PARTIAL)
);
let expected = vec!["k", "foo.bar", "a.b.c"];
assert_eq!(
options
.value(JSON_OPT_UNSTRUCTURED_KEYS)
.and_then(|x| x.as_list()),
Some(expected)
);
let sql = r#"
CREATE TABLE json_data (
my_json JSON(format = "structured"),
ts TIMESTAMP TIME INDEX,
)"#;
let options = parse(sql).unwrap();
assert_eq!(options.len(), 1);
assert_eq!(
options.value(JSON_OPT_FORMAT).and_then(|x| x.as_string()),
Some(JSON_FORMAT_FULL_STRUCTURED)
);
let sql = r#"
CREATE TABLE json_data (
my_json JSON(format = "raw"),
ts TIMESTAMP TIME INDEX,
)"#;
let options = parse(sql).unwrap();
assert_eq!(options.len(), 1);
assert_eq!(
options.value(JSON_OPT_FORMAT).and_then(|x| x.as_string()),
Some(JSON_FORMAT_RAW)
);
let sql = r#"
CREATE TABLE json_data (
my_json JSON(),
ts TIMESTAMP TIME INDEX,
)"#;
let options = parse(sql).unwrap();
assert!(options.is_empty());
let sql = r#"
CREATE TABLE json_data (
my_json JSON,
ts TIMESTAMP TIME INDEX,
)"#;
let options = parse(sql);
assert!(options.is_none());
}
}
+15 -32
View File
@@ -40,7 +40,6 @@ use api::v1::SemanticType;
use common_sql::default_constraint::parse_column_default_constraint;
use common_time::timezone::Timezone;
use datatypes::extension::json::{JsonExtensionType, JsonMetadata};
use datatypes::json::JsonStructureSettings;
use datatypes::prelude::ConcreteDataType;
use datatypes::schema::{COMMENT_KEY, ColumnDefaultConstraint, ColumnSchema};
use datatypes::types::json_type::JsonNativeType;
@@ -55,10 +54,10 @@ use crate::ast::{
};
use crate::error::{
self, ConvertToGrpcDataTypeSnafu, ConvertValueSnafu, Result,
SerializeColumnDefaultConstraintSnafu, SetFulltextOptionSnafu, SetJsonStructureSettingsSnafu,
SerializeColumnDefaultConstraintSnafu, SetFulltextOptionSnafu, SetJsonSettingsSnafu,
SetSkippingIndexOptionSnafu, SetVectorIndexOptionSnafu, SqlCommonSnafu,
};
use crate::statements::create::{Column, ColumnExtensions};
use crate::statements::create::Column;
pub use crate::statements::option_map::OptionMap;
pub(crate) use crate::statements::transform::transform_statements;
@@ -110,7 +109,7 @@ pub fn column_to_schema(
&& !is_time_index;
let name = column.name().value.clone();
let data_type = sql_data_type_to_concrete_data_type(column.data_type(), &column.extensions)?;
let data_type = sql_data_type_to_concrete_data_type(column.data_type())?;
let default_constraint =
parse_column_default_constraint(&name, &data_type, column.options(), timezone)
.context(SqlCommonSnafu)?;
@@ -164,16 +163,13 @@ pub fn column_to_schema(
false
};
if is_json2_column {
let settings = column
.extensions
.build_json_structure_settings()?
.unwrap_or_default();
let settings = column.extensions.build_json_settings()?.unwrap_or_default();
let extension = JsonExtensionType::new(Arc::new(JsonMetadata {
json_structure_settings: Some(settings.clone()),
json_settings: Some(settings.clone()),
}));
column_schema
.with_extension_type(&extension)
.with_context(|_| SetJsonStructureSettingsSnafu {
.with_context(|_| SetJsonSettingsSnafu {
value: format!("{settings:?}"),
})?;
}
@@ -187,7 +183,7 @@ pub fn sql_column_def_to_grpc_column_def(
timezone: Option<&Timezone>,
) -> Result<api::v1::ColumnDef> {
let name = col.name.value.clone();
let data_type = sql_data_type_to_concrete_data_type(&col.data_type, &Default::default())?;
let data_type = sql_data_type_to_concrete_data_type(&col.data_type)?;
let is_nullable = col
.options
@@ -228,10 +224,7 @@ pub fn sql_column_def_to_grpc_column_def(
})
}
pub fn sql_data_type_to_concrete_data_type(
data_type: &SqlDataType,
column_extensions: &ColumnExtensions,
) -> Result<ConcreteDataType> {
pub fn sql_data_type_to_concrete_data_type(data_type: &SqlDataType) -> Result<ConcreteDataType> {
match data_type {
SqlDataType::BigInt(_) | SqlDataType::Int64 => Ok(ConcreteDataType::int64_datatype()),
SqlDataType::BigIntUnsigned(_) => Ok(ConcreteDataType::uint64_datatype()),
@@ -299,19 +292,9 @@ pub fn sql_data_type_to_concrete_data_type(
Ok(ConcreteDataType::vector_datatype(dim))
}
JSON2_TYPE_NAME if args.is_empty() => {
let native_type = column_extensions
.build_json_structure_settings()?
.and_then(|x| match x {
JsonStructureSettings::Structured(Some(fields))
| JsonStructureSettings::PartialUnstructuredByKey {
fields: Some(fields),
..
} => Some(JsonNativeType::from(&ConcreteDataType::Struct(fields))),
JsonStructureSettings::UnstructuredRaw => Some(JsonNativeType::Variant),
_ => None,
})
.unwrap_or(JsonNativeType::Object(Default::default()));
let format = JsonFormat::Json2(Box::new(native_type));
// Currently, JSON2 is not inferred as any native type initially.
// TODO(fys): infer it later from type hints.
let format = JsonFormat::Json2(Box::new(JsonNativeType::Null));
Ok(ConcreteDataType::Json(JsonType::new(format)))
}
_ => error::SqlTypeNotSupportedSnafu {
@@ -390,7 +373,7 @@ mod tests {
fn check_type(sql_type: SqlDataType, data_type: ConcreteDataType) {
assert_eq!(
data_type,
sql_data_type_to_concrete_data_type(&sql_type, &Default::default()).unwrap()
sql_data_type_to_concrete_data_type(&sql_type).unwrap()
);
}
@@ -744,7 +727,7 @@ mod tests {
vector_options: None,
skipping_index_options: None,
inverted_index_options: None,
json_datatype_options: None,
json_type_hints: vec![],
vector_index_options: None,
},
};
@@ -776,7 +759,7 @@ mod tests {
vector_options: None,
skipping_index_options: None,
inverted_index_options: None,
json_datatype_options: None,
json_type_hints: vec![],
vector_index_options: Some(OptionMap::from([
("metric".to_string(), "cosine".to_string()),
("connectivity".to_string(), "32".to_string()),
@@ -817,7 +800,7 @@ mod tests {
vector_options: None,
skipping_index_options: None,
inverted_index_options: None,
json_datatype_options: None,
json_type_hints: vec![],
vector_index_options: Some(OptionMap::default()),
},
};
+427 -127
View File
@@ -12,47 +12,40 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use std::collections::{HashMap, HashSet};
use std::collections::HashMap;
use std::fmt::{Display, Formatter};
use std::sync::Arc;
use common_catalog::consts::FILE_ENGINE;
use datatypes::data_type::ConcreteDataType;
use datatypes::json::JsonStructureSettings;
use common_sql::default_constraint::parse_column_default_constraint;
use datatypes::json::JsonSettings;
use datatypes::prelude::ConcreteDataType;
use datatypes::schema::{
FulltextOptions, SkippingIndexOptions, VectorDistanceMetric, VectorIndexEngineType,
VectorIndexOptions,
ColumnDefaultConstraint, FulltextOptions, SkippingIndexOptions, VectorDistanceMetric,
VectorIndexEngineType, VectorIndexOptions,
};
use datatypes::types::StructType;
use itertools::Itertools;
use serde::Serialize;
use snafu::{OptionExt, ResultExt};
use sqlparser::ast::{ColumnOptionDef, DataType, Expr};
use snafu::ResultExt;
use sqlparser::ast::{ColumnOption, ColumnOptionDef, DataType, Expr};
use sqlparser_derive::{Visit, VisitMut};
use crate::ast::{ColumnDef, Ident, ObjectName, Value as SqlValue};
use crate::dialect::GreptimeDbDialect;
use crate::error::{
InvalidFlowQuerySnafu, InvalidJsonStructureSettingSnafu, InvalidSqlSnafu, Result,
SetFulltextOptionSnafu, SetSkippingIndexOptionSnafu,
InvalidFlowQuerySnafu, InvalidSqlSnafu, Result, SetFulltextOptionSnafu,
SetSkippingIndexOptionSnafu,
};
use crate::parser::ParserContext;
use crate::statements::query::Query as GtQuery;
use crate::statements::statement::Statement;
use crate::statements::tql::Tql;
use crate::statements::{OptionMap, sql_data_type_to_concrete_data_type};
use crate::util::OptionValue;
use crate::statements::{OptionMap, sql_data_type_to_concrete_data_type, value_to_sql_value};
const LINE_SEP: &str = ",\n";
const COMMA_SEP: &str = ", ";
const INDENT: usize = 2;
pub const VECTOR_OPT_DIM: &str = "dim";
pub const JSON_OPT_UNSTRUCTURED_KEYS: &str = "unstructured_keys";
pub const JSON_OPT_FORMAT: &str = "format";
pub(crate) const JSON_OPT_FIELDS: &str = "fields";
pub const JSON_FORMAT_FULL_STRUCTURED: &str = "structured";
pub const JSON_FORMAT_RAW: &str = "raw";
pub const JSON_FORMAT_PARTIAL: &str = "partial";
macro_rules! format_indent {
($fmt: expr, $arg: expr) => {
format!($fmt, format_args!("{: >1$}", "", INDENT), $arg)
@@ -143,7 +136,16 @@ pub struct ColumnExtensions {
pub inverted_index_options: Option<OptionMap>,
/// Vector index options for HNSW-based vector similarity search.
pub vector_index_options: Option<OptionMap>,
pub json_datatype_options: Option<OptionMap>,
pub json_type_hints: Vec<JsonTypeHint>,
}
#[derive(Debug, PartialEq, Eq, Clone, Visit, VisitMut, Serialize)]
pub struct JsonTypeHint {
pub path: Vec<String>,
pub data_type: DataType,
pub nullable: bool,
pub default: Option<Expr>,
pub inverted_index: bool,
}
impl Column {
@@ -178,14 +180,11 @@ impl Display for Column {
}
write!(f, "{} {}", self.column_def.name, self.column_def.data_type)?;
if let Some(options) = &self.extensions.json_datatype_options {
if !self.extensions.json_type_hints.is_empty() {
write!(
f,
"({})",
options
.entries()
.map(|(k, v)| format!("{k} = {v}"))
.join(COMMA_SEP)
"{}",
format_json_type_hints(&self.extensions.json_type_hints)
)?;
}
for option in &self.column_def.options {
@@ -335,106 +334,168 @@ impl ColumnExtensions {
Ok(Some(result))
}
pub fn build_json_structure_settings(&self) -> Result<Option<JsonStructureSettings>> {
let Some(options) = self.json_datatype_options.as_ref() else {
pub fn build_json_settings(&self) -> Result<Option<JsonSettings>> {
if self.json_type_hints.is_empty() {
return Ok(None);
};
let unstructured_keys = options
.value(JSON_OPT_UNSTRUCTURED_KEYS)
.and_then(|v| {
v.as_list().map(|x| {
x.into_iter()
.map(|x| x.to_string())
.collect::<HashSet<String>>()
})
})
.unwrap_or_default();
let fields = if let Some(value) = options.value(JSON_OPT_FIELDS) {
let fields = value
.as_struct_fields()
.context(InvalidJsonStructureSettingSnafu {
reason: format!(r#"expect "{JSON_OPT_FIELDS}" a struct, actual: "{value}""#,),
})?;
let fields = fields
.iter()
.map(|field| {
let name = field.field_name.as_ref().map(|x| x.value.clone()).context(
InvalidJsonStructureSettingSnafu {
reason: format!(r#"missing field name in "{field}""#),
},
)?;
let datatype = sql_data_type_to_concrete_data_type(
&field.field_type,
&Default::default(),
)?;
Ok(datatypes::types::StructField::new(name, datatype, true))
})
.collect::<Result<_>>()?;
Some(StructType::new(Arc::new(fields)))
} else {
None
};
let format = options
.get(JSON_OPT_FORMAT)
.unwrap_or(JSON_FORMAT_FULL_STRUCTURED);
let settings = match format {
JSON_FORMAT_FULL_STRUCTURED => JsonStructureSettings::Structured(fields),
JSON_FORMAT_PARTIAL => {
let fields = fields.map(|fields| {
let mut fields = Arc::unwrap_or_clone(fields.fields());
fields.push(datatypes::types::StructField::new(
JsonStructureSettings::RAW_FIELD.to_string(),
ConcreteDataType::string_datatype(),
true,
));
StructType::new(Arc::new(fields))
});
JsonStructureSettings::PartialUnstructuredByKey {
fields,
unstructured_keys,
}
}
JSON_FORMAT_RAW => JsonStructureSettings::UnstructuredRaw,
_ => {
return InvalidSqlSnafu {
msg: format!("unknown JSON datatype 'format': {format}"),
}
.fail();
}
};
Ok(Some(settings))
}
pub fn set_json_structure_settings(&mut self, settings: JsonStructureSettings) {
let mut map = OptionMap::default();
let format = match settings {
JsonStructureSettings::Structured(_) => JSON_FORMAT_FULL_STRUCTURED,
JsonStructureSettings::PartialUnstructuredByKey { .. } => JSON_FORMAT_PARTIAL,
JsonStructureSettings::UnstructuredRaw => JSON_FORMAT_RAW,
};
map.insert(JSON_OPT_FORMAT.to_string(), format.to_string());
if let JsonStructureSettings::PartialUnstructuredByKey {
fields: _,
unstructured_keys,
} = settings
{
let value = OptionValue::from(
unstructured_keys
.iter()
.map(|x| x.as_str())
.sorted()
.collect::<Vec<_>>(),
);
map.insert_options(JSON_OPT_UNSTRUCTURED_KEYS, value);
}
self.json_datatype_options = Some(map);
Ok(Some(JsonSettings::new(
self.json_type_hints
.iter()
.map(|hint| {
Ok(datatypes::json::JsonTypeHint {
path: hint.path.clone(),
data_type: json_type_hint_concrete_data_type(&hint.data_type)?,
nullable: hint.nullable,
default_constraint: build_json_type_hint_default_constraint(hint)?,
inverted_index: hint.inverted_index,
})
})
.collect::<Result<Vec<_>>>()?,
)))
}
pub fn set_json_settings(&mut self, settings: JsonSettings) -> Result<()> {
self.json_type_hints = settings
.type_hints
.into_iter()
.map(|hint| {
let data_type = json_type_hint_sql_data_type(&hint.data_type)?;
let default = hint
.default_constraint
.map(|constraint| column_default_constraint_to_expr(&constraint))
.transpose()?;
Ok(JsonTypeHint {
path: hint.path,
data_type,
nullable: hint.nullable,
default,
inverted_index: hint.inverted_index,
})
})
.collect::<Result<Vec<_>>>()?;
Ok(())
}
}
fn build_json_type_hint_default_constraint(
hint: &JsonTypeHint,
) -> Result<Option<ColumnDefaultConstraint>> {
let Some(default) = &hint.default else {
return Ok(None);
};
let data_type = json_type_hint_concrete_data_type(&hint.data_type)?;
let opts = [ColumnOptionDef {
name: None,
option: ColumnOption::Default(default.clone()),
}];
// Use the JSON path as the column name context for default value parsing errors.
let json_path = hint.path.join(".");
let default_constraint = parse_column_default_constraint(&json_path, &data_type, &opts, None)
.context(crate::error::SqlCommonSnafu)?;
if let Some(constraint) = &default_constraint {
constraint
.validate(&data_type, hint.nullable)
.map_err(|e| {
InvalidSqlSnafu {
msg: format!("invalid DEFAULT for JSON2 type hint '{}': {e}", json_path),
}
.build()
})?;
}
Ok(default_constraint)
}
fn json_type_hint_concrete_data_type(data_type: &DataType) -> Result<ConcreteDataType> {
let data_type = sql_data_type_to_concrete_data_type(data_type)?;
normalize_json_type_hint_concrete_data_type(&data_type)
}
fn normalize_json_type_hint_concrete_data_type(
data_type: &ConcreteDataType,
) -> Result<ConcreteDataType> {
let normalized = match data_type {
ConcreteDataType::String(_) => ConcreteDataType::string_datatype(),
ConcreteDataType::Int8(_)
| ConcreteDataType::Int16(_)
| ConcreteDataType::Int32(_)
| ConcreteDataType::Int64(_) => ConcreteDataType::int64_datatype(),
ConcreteDataType::UInt8(_)
| ConcreteDataType::UInt16(_)
| ConcreteDataType::UInt32(_)
| ConcreteDataType::UInt64(_) => ConcreteDataType::uint64_datatype(),
ConcreteDataType::Float32(_) | ConcreteDataType::Float64(_) => {
ConcreteDataType::float64_datatype()
}
ConcreteDataType::Boolean(_) => ConcreteDataType::boolean_datatype(),
_ => {
return InvalidSqlSnafu {
msg: format!("unsupported JSON2 type hint data type: {data_type}"),
}
.fail();
}
};
Ok(normalized)
}
fn json_type_hint_sql_data_type(data_type: &ConcreteDataType) -> Result<DataType> {
let data_type = normalize_json_type_hint_concrete_data_type(data_type)?;
let sql_type = match data_type {
ConcreteDataType::String(_) => DataType::String(None),
ConcreteDataType::Int64(_) => DataType::BigInt(None),
ConcreteDataType::UInt64(_) => DataType::BigIntUnsigned(None),
ConcreteDataType::Float64(_) => DataType::Double(sqlparser::ast::ExactNumberInfo::None),
ConcreteDataType::Boolean(_) => DataType::Boolean,
_ => unreachable!("JSON2 type hint data type should have been normalized"),
};
Ok(sql_type)
}
fn column_default_constraint_to_expr(constraint: &ColumnDefaultConstraint) -> Result<Expr> {
match constraint {
ColumnDefaultConstraint::Value(value) => Ok(Expr::Value(value_to_sql_value(value)?.into())),
ColumnDefaultConstraint::Function(function) => {
ParserContext::parse_function(function, &GreptimeDbDialect {})
}
}
}
fn format_json_type_hint(hint: &JsonTypeHint) -> String {
let path = hint
.path
.iter()
.map(|segment| format_json_path_segment(segment))
.join(".");
let nullability = if hint.nullable { " NULL" } else { " NOT NULL" };
let default = hint
.default
.as_ref()
.map(|expr| format!(" DEFAULT {expr}"))
.unwrap_or_default();
let inverted_index = if hint.inverted_index {
" INVERTED INDEX"
} else {
""
};
format!(
"{} {}{}{}{}",
path, hint.data_type, nullability, default, inverted_index
)
}
fn format_json_type_hints(hints: &[JsonTypeHint]) -> String {
format!(
"(\n {}\n )",
hints.iter().map(format_json_type_hint).join(",\n ")
)
}
fn format_json_path_segment(segment: &str) -> String {
format!("\"{}\"", segment.replace('"', "\"\""))
}
/// Partition on columns or values.
@@ -720,6 +781,11 @@ impl Display for CreateView {
mod tests {
use std::assert_matches;
use datatypes::json::{JsonSettings, JsonTypeHint as DatatypeJsonTypeHint};
use datatypes::prelude::ConcreteDataType;
use datatypes::schema::ColumnDefaultConstraint;
use datatypes::value::Value;
use crate::dialect::GreptimeDbDialect;
use crate::error::Error;
use crate::parser::{ParseOptions, ParserContext};
@@ -889,6 +955,240 @@ ENGINE=mito
);
}
#[test]
fn test_display_json2_type_hints_quotes_path_segments() {
let sql = r#"CREATE TABLE traces (
log_json_data JSON2 (
"service.name" STRING,
"a.b"."c" INT64 NOT NULL,
a."b.c" STRING
),
ts TIMESTAMP TIME INDEX
)"#;
let result =
ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
.unwrap();
match &result[0] {
Statement::CreateTable(c) => {
let new_sql = format!("\n{}", c);
assert_eq!(
r#"
CREATE TABLE traces (
log_json_data JSON2(
"service.name" STRING NULL,
"a.b"."c" BIGINT NOT NULL,
"a"."b.c" STRING NULL
),
ts TIMESTAMP NOT NULL,
TIME INDEX (ts)
)
ENGINE=mito
"#,
&new_sql
);
let new_result = ParserContext::create_with_dialect(
&new_sql,
&GreptimeDbDialect {},
ParseOptions::default(),
)
.unwrap();
assert_eq!(result, new_result);
}
_ => unreachable!(),
}
}
#[test]
fn test_display_json2_type_hints_quotes_numeric_segments() {
let sql = r#"CREATE TABLE traces (
log_json_data JSON2 (
"1abc" STRING,
a."2b" INT64 NOT NULL
),
ts TIMESTAMP TIME INDEX
)"#;
let result =
ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
.unwrap();
match &result[0] {
Statement::CreateTable(c) => {
let new_sql = format!("\n{}", c);
assert_eq!(
r#"
CREATE TABLE traces (
log_json_data JSON2(
"1abc" STRING NULL,
"a"."2b" BIGINT NOT NULL
),
ts TIMESTAMP NOT NULL,
TIME INDEX (ts)
)
ENGINE=mito
"#,
&new_sql
);
let new_result = ParserContext::create_with_dialect(
&new_sql,
&GreptimeDbDialect {},
ParseOptions::default(),
)
.unwrap();
assert_eq!(result, new_result);
}
_ => unreachable!(),
}
}
#[test]
fn test_json2_type_hint_default_builds_default_constraint() {
let sql = r#"CREATE TABLE traces (
log_json_data JSON2 (
status_code INT64 DEFAULT -5,
duration FLOAT64 DEFAULT +1.5,
error BOOLEAN DEFAULT false,
message STRING DEFAULT 'unknown'
),
ts TIMESTAMP TIME INDEX
)"#;
let result =
ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
.unwrap();
let Statement::CreateTable(create_table) = &result[0] else {
unreachable!()
};
let settings = create_table.columns[0]
.extensions
.build_json_settings()
.unwrap()
.unwrap();
let hints = settings.type_hints;
assert_eq!(hints[0].data_type, ConcreteDataType::int64_datatype());
assert_eq!(
hints[0].default_constraint,
Some(ColumnDefaultConstraint::Value(Value::Int64(-5)))
);
assert_eq!(hints[1].data_type, ConcreteDataType::float64_datatype());
assert_eq!(
hints[1].default_constraint,
Some(ColumnDefaultConstraint::Value(Value::Float64(1.5.into())))
);
assert_eq!(hints[2].data_type, ConcreteDataType::boolean_datatype());
assert_eq!(
hints[2].default_constraint,
Some(ColumnDefaultConstraint::Value(Value::Boolean(false)))
);
assert_eq!(hints[3].data_type, ConcreteDataType::string_datatype());
assert_eq!(
hints[3].default_constraint,
Some(ColumnDefaultConstraint::Value(Value::String(
"unknown".into()
)))
);
}
#[test]
fn test_json2_type_hint_not_null_default_null_is_rejected() {
let sql = r#"CREATE TABLE traces (
log_json_data JSON2 (
status_code INT64 NOT NULL DEFAULT NULL
),
ts TIMESTAMP TIME INDEX
)"#;
let result =
ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
.unwrap();
let Statement::CreateTable(create_table) = &result[0] else {
unreachable!()
};
let err = create_table.columns[0]
.extensions
.build_json_settings()
.unwrap_err();
assert!(
err.to_string()
.contains("Default value should not be null for non null column")
);
}
#[test]
fn test_set_json_settings_normalizes_type_hint_sql_types() {
let mut extensions = super::ColumnExtensions::default();
extensions
.set_json_settings(JsonSettings::new(vec![
DatatypeJsonTypeHint {
path: vec!["i".to_string()],
data_type: ConcreteDataType::int32_datatype(),
nullable: true,
default_constraint: None,
inverted_index: false,
},
DatatypeJsonTypeHint {
path: vec!["f".to_string()],
data_type: ConcreteDataType::float32_datatype(),
nullable: true,
default_constraint: None,
inverted_index: false,
},
DatatypeJsonTypeHint {
path: vec!["u".to_string()],
data_type: ConcreteDataType::uint32_datatype(),
nullable: true,
default_constraint: None,
inverted_index: false,
},
DatatypeJsonTypeHint {
path: vec!["s".to_string()],
data_type: ConcreteDataType::string_datatype(),
nullable: true,
default_constraint: None,
inverted_index: false,
},
DatatypeJsonTypeHint {
path: vec!["b".to_string()],
data_type: ConcreteDataType::boolean_datatype(),
nullable: true,
default_constraint: None,
inverted_index: false,
},
]))
.unwrap();
assert_eq!(
extensions
.json_type_hints
.iter()
.map(|hint| hint.data_type.to_string())
.collect::<Vec<_>>(),
vec!["BIGINT", "DOUBLE", "BIGINT UNSIGNED", "STRING", "BOOLEAN"]
);
}
#[test]
fn test_set_json_settings_rejects_unsupported_type_hint_type() {
let mut extensions = super::ColumnExtensions::default();
let err = extensions
.set_json_settings(JsonSettings::new(vec![DatatypeJsonTypeHint {
path: vec!["u".to_string()],
data_type: ConcreteDataType::date_datatype(),
nullable: true,
default_constraint: None,
inverted_index: false,
}]))
.unwrap_err();
assert!(
err.to_string()
.contains("unsupported JSON2 type hint data type")
);
}
#[test]
fn test_display_create_database() {
let sql = r"create database test;";
@@ -1068,7 +1368,7 @@ AS SELECT number FROM numbers_input where number > 10"#,
vector_options: None,
skipping_index_options: None,
inverted_index_options: None,
json_datatype_options: None,
json_type_hints: vec![],
vector_index_options: Some(OptionMap::from([(
"connectivity".to_string(),
"0".to_string(),
@@ -1089,7 +1389,7 @@ AS SELECT number FROM numbers_input where number > 10"#,
vector_options: None,
skipping_index_options: None,
inverted_index_options: None,
json_datatype_options: None,
json_type_hints: vec![],
vector_index_options: Some(OptionMap::from([(
"expansion_add".to_string(),
"0".to_string(),
@@ -1110,7 +1410,7 @@ AS SELECT number FROM numbers_input where number > 10"#,
vector_options: None,
skipping_index_options: None,
inverted_index_options: None,
json_datatype_options: None,
json_type_hints: vec![],
vector_index_options: Some(OptionMap::from([(
"expansion_search".to_string(),
"0".to_string(),
@@ -1131,7 +1431,7 @@ AS SELECT number FROM numbers_input where number > 10"#,
vector_options: None,
skipping_index_options: None,
inverted_index_options: None,
json_datatype_options: None,
json_type_hints: vec![],
vector_index_options: Some(OptionMap::from([
("connectivity".to_string(), "32".to_string()),
("expansion_add".to_string(), "200".to_string()),
@@ -117,9 +117,7 @@ impl TransformRule for TypeAliasTransformRule {
} if get_type_by_alias(data_type).is_some() => {
// Safety: checked in the match arm.
let new_type = get_type_by_alias(data_type).unwrap();
if let Ok(new_type) =
sql_data_type_to_concrete_data_type(&new_type, &Default::default())
{
if let Ok(new_type) = sql_data_type_to_concrete_data_type(&new_type) {
*expr = Expr::Function(cast_expr_to_arrow_cast_func(
(**cast_expr).clone(),
new_type.as_arrow_type().to_string(),
@@ -134,10 +132,9 @@ impl TransformRule for TypeAliasTransformRule {
expr: cast_expr,
..
} => {
if let Ok(concrete_type) = sql_data_type_to_concrete_data_type(
&DataType::Timestamp(*precision, *zone),
&Default::default(),
) {
if let Ok(concrete_type) =
sql_data_type_to_concrete_data_type(&DataType::Timestamp(*precision, *zone))
{
let new_type = concrete_type.as_arrow_type();
*expr = Expr::Function(cast_expr_to_arrow_cast_func(
(**cast_expr).clone(),
+1 -8
View File
@@ -26,7 +26,7 @@ use promql_parser::parser::{
use serde::Serialize;
use snafu::ensure;
use sqlparser::ast::{
Array, Expr, Ident, ObjectName, ObjectNamePart, SetExpr, SqlOption, StructField, TableFactor,
Array, Expr, Ident, ObjectName, ObjectNamePart, SetExpr, SqlOption, TableFactor,
TableWithJoins, Value, ValueWithSpan,
};
use sqlparser_derive::{Visit, VisitMut};
@@ -128,13 +128,6 @@ impl OptionValue {
_ => None,
}
}
pub(crate) fn as_struct_fields(&self) -> Option<&[StructField]> {
match &self.0 {
Expr::Struct { fields, .. } => Some(fields),
_ => None,
}
}
}
impl From<String> for OptionValue {