mirror of
https://github.com/GreptimeTeam/greptimedb.git
synced 2026-09-07 22:18:57 +00:00
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:
+4
-11
@@ -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(),
|
||||
|
||||
@@ -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!(),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user