mirror of
https://github.com/GreptimeTeam/greptimedb.git
synced 2026-11-15 07:17:01 +00:00
Merge upstream/main into narthex (pre-merge rehearsal, not for push)
Rehearsal merge of main (f8d8fe1071) into narthex (115d85b669) on a throwaway local branch. 31 semantic conflicts resolved; workspace and all test targets compile; promql 1215/1215 and mito2 series_index 185/185 pass. Pre-merge pins and follow-ups are marked with TODO(narthex-merge): - greptime-proto is temporarily pinned to the fork branch carrying GreptimeTeam/greptime-proto#355; replace with the upstream rev once it lands. - native payload byte cache is disabled (main removed PageRangeCache); two samples tests are #[ignore]d until it gets a dedicated page-cache budget. - columnar metadata now has its own CacheManager tier (a quarter of sst_meta_cache_size); the unified decoded tier was not kept. - query.experimental_native_promql is registered as a query option, otherwise the native route is silently rejected by the option snapshot. Signed-off-by: discord9 <discord9@163.com>
This commit is contained in:
@@ -32,3 +32,37 @@ impl ObjectNamePartExt for ObjectNamePart {
|
||||
ident.value.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// Formats an object name by joining its identifier values without quotes.
|
||||
pub fn object_name_to_string_unquoted(name: &ObjectName) -> String {
|
||||
name.0
|
||||
.iter()
|
||||
.map(ObjectNamePartExt::to_string_unquoted)
|
||||
.collect::<Vec<_>>()
|
||||
.join(".")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use sqlparser::ast::{Ident, ObjectName, ObjectNamePart};
|
||||
|
||||
use super::object_name_to_string_unquoted;
|
||||
|
||||
#[test]
|
||||
fn test_object_name_to_string_unquoted() {
|
||||
let quoted = ObjectName(vec![ObjectNamePart::Identifier(Ident::with_quote(
|
||||
'"',
|
||||
"query.parallelism",
|
||||
))]);
|
||||
let unquoted = ObjectName(vec![
|
||||
ObjectNamePart::Identifier(Ident::new("query")),
|
||||
ObjectNamePart::Identifier(Ident::new("parallelism")),
|
||||
]);
|
||||
|
||||
assert_eq!("query.parallelism", object_name_to_string_unquoted("ed));
|
||||
assert_eq!(
|
||||
object_name_to_string_unquoted(&unquoted),
|
||||
object_name_to_string_unquoted("ed)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,8 +28,8 @@ pub(crate) use json::parse_json2_type_and_options;
|
||||
pub use json::parse_json2_type_hint_path;
|
||||
use snafu::{OptionExt, ResultExt, ensure};
|
||||
use sqlparser::ast::{
|
||||
ColumnOption, ColumnOptionDef, DataType, Expr, KeyOrIndexDisplay, NullsDistinctOption,
|
||||
PrimaryKeyConstraint, UniqueConstraint,
|
||||
ColumnOption, ColumnOptionDef, DataType, Expr, FunctionArg, FunctionArgExpr, FunctionArguments,
|
||||
KeyOrIndexDisplay, NullsDistinctOption, PrimaryKeyConstraint, UniqueConstraint,
|
||||
};
|
||||
use sqlparser::dialect::keywords::Keyword;
|
||||
use sqlparser::keywords::ALL_KEYWORDS;
|
||||
@@ -1300,6 +1300,17 @@ fn ensure_one_expr(expr: &Expr, columns: &[&Column]) -> Result<()> {
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
// The function and its argument shape are validated when the rule is converted.
|
||||
Expr::Function(function) => {
|
||||
if let FunctionArguments::List(args) = &function.args {
|
||||
for arg in &args.args {
|
||||
if let FunctionArg::Unnamed(FunctionArgExpr::Expr(expr)) = arg {
|
||||
ensure_one_expr(expr, columns)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Expr::Value(_) => Ok(()),
|
||||
Expr::UnaryOp { expr, .. } => {
|
||||
ensure_one_expr(expr, columns)?;
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
use datatypes::extension::json::JSON2_REMAINDER_FIELD_NAME;
|
||||
use datatypes::json::JSON2_MAX_STRUCTURED_DEPTH;
|
||||
use datatypes::schema::FulltextOptions;
|
||||
use snafu::{ResultExt, ensure};
|
||||
use sqlparser::ast::{DataType, ExactNumberInfo, ObjectName};
|
||||
use sqlparser::dialect::keywords::Keyword;
|
||||
@@ -24,6 +25,7 @@ use crate::ast::Ident;
|
||||
use crate::dialect::GreptimeDbDialect;
|
||||
use crate::error::{InvalidSqlSnafu, Result, SyntaxSnafu};
|
||||
use crate::parsers::create_parser::{INVERTED, SKIPPING};
|
||||
use crate::statements::OptionMap;
|
||||
use crate::statements::create::{Json2Options, JsonTypeHint};
|
||||
|
||||
const JSON2_TYPE_NAME: &str = "JSON2";
|
||||
@@ -157,6 +159,8 @@ fn parse_json2_type_hint(parser: &mut Parser<'_>) -> Result<JsonTypeHint> {
|
||||
let data_type = validate_json2_type_hint_type(data_type)?;
|
||||
|
||||
let mut inverted_index = false;
|
||||
let mut skipping_index = None;
|
||||
let mut fulltext_index = None;
|
||||
|
||||
loop {
|
||||
if parser.parse_keywords(&[Keyword::NOT, Keyword::NULL])
|
||||
@@ -194,14 +198,90 @@ fn parse_json2_type_hint(parser: &mut Parser<'_>) -> Result<JsonTypeHint> {
|
||||
)
|
||||
}
|
||||
);
|
||||
ensure!(
|
||||
!path
|
||||
.iter()
|
||||
.any(|segment| segment == JSON2_REMAINDER_FIELD_NAME),
|
||||
InvalidSqlSnafu {
|
||||
msg: format!(
|
||||
"JSON2 indexed type hint path cannot contain reserved field '{JSON2_REMAINDER_FIELD_NAME}'"
|
||||
)
|
||||
}
|
||||
);
|
||||
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(),
|
||||
parser.next_token();
|
||||
ensure!(
|
||||
parser.parse_keyword(Keyword::INDEX),
|
||||
InvalidSqlSnafu {
|
||||
msg: "expect INDEX after SKIPPING keyword",
|
||||
}
|
||||
);
|
||||
ensure!(
|
||||
skipping_index.is_none(),
|
||||
InvalidSqlSnafu {
|
||||
msg: "duplicated SKIPPING INDEX option for JSON2 type hint",
|
||||
}
|
||||
);
|
||||
let options = parser
|
||||
.parse_options(Keyword::WITH)
|
||||
.context(SyntaxSnafu)?
|
||||
.into_iter()
|
||||
.map(crate::util::parse_option_string)
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
for (key, _) in &options {
|
||||
ensure!(
|
||||
crate::parsers::utils::validate_column_skipping_index_create_option(key),
|
||||
InvalidSqlSnafu {
|
||||
msg: format!("invalid SKIPPING INDEX option: {key}"),
|
||||
}
|
||||
);
|
||||
}
|
||||
.fail();
|
||||
skipping_index = Some(
|
||||
datatypes::schema::SkippingIndexOptions::try_from(
|
||||
OptionMap::new(options).into_map(),
|
||||
)
|
||||
.context(crate::error::SetSkippingIndexOptionSnafu)?,
|
||||
);
|
||||
} else if parser.parse_keyword(Keyword::FULLTEXT) {
|
||||
ensure!(
|
||||
matches!(data_type, DataType::String(None)),
|
||||
InvalidSqlSnafu {
|
||||
msg: "JSON2 FULLTEXT index only supports string type hints",
|
||||
}
|
||||
);
|
||||
ensure!(
|
||||
parser.parse_keyword(Keyword::INDEX),
|
||||
InvalidSqlSnafu {
|
||||
msg: "expect INDEX after FULLTEXT keyword",
|
||||
}
|
||||
);
|
||||
ensure!(
|
||||
fulltext_index.is_none(),
|
||||
InvalidSqlSnafu {
|
||||
msg: "duplicated FULLTEXT INDEX option for JSON2 type hint",
|
||||
}
|
||||
);
|
||||
let options = parser
|
||||
.parse_options(Keyword::WITH)
|
||||
.context(SyntaxSnafu)?
|
||||
.into_iter()
|
||||
.map(crate::util::parse_option_string)
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
for (key, _) in &options {
|
||||
ensure!(
|
||||
crate::parsers::utils::validate_column_fulltext_create_option(key),
|
||||
InvalidSqlSnafu {
|
||||
msg: format!("invalid FULLTEXT INDEX option: {key}")
|
||||
}
|
||||
);
|
||||
}
|
||||
fulltext_index = Some(
|
||||
FulltextOptions::try_from(OptionMap::new(options).into_map())
|
||||
.context(crate::error::SetFulltextOptionSnafu)?,
|
||||
);
|
||||
} else if matches!(parser.peek_token().token, Token::Comma | Token::RParen) {
|
||||
break;
|
||||
} else {
|
||||
@@ -215,6 +295,8 @@ fn parse_json2_type_hint(parser: &mut Parser<'_>) -> Result<JsonTypeHint> {
|
||||
path,
|
||||
data_type,
|
||||
inverted_index,
|
||||
skipping_index,
|
||||
fulltext_index,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -300,6 +382,117 @@ mod tests {
|
||||
create_table.columns.remove(0)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json2_fulltext_index() {
|
||||
use datatypes::schema::FulltextBackend;
|
||||
for (options, backend) in [
|
||||
("", FulltextBackend::Bloom),
|
||||
(
|
||||
"WITH (backend='tantivy', analyzer='Chinese', case_sensitive='true')",
|
||||
FulltextBackend::Tantivy,
|
||||
),
|
||||
(
|
||||
"WITH (backend='bloom', granularity='8', false_positive_rate='0.05')",
|
||||
FulltextBackend::Bloom,
|
||||
),
|
||||
] {
|
||||
let sql = format!(
|
||||
"CREATE TABLE logs (attrs JSON2 (message STRING FULLTEXT INDEX {options} SKIPPING INDEX INVERTED INDEX), ts TIMESTAMP TIME INDEX)"
|
||||
);
|
||||
let column = parse_json2_column(&sql);
|
||||
let options = column.extensions.json2_options.unwrap();
|
||||
let settings = options.build_json_settings().unwrap();
|
||||
let hint = &settings.type_hints()[0];
|
||||
assert_eq!(hint.fulltext_index.as_ref().unwrap().backend, backend);
|
||||
assert!(hint.fulltext_index.as_ref().unwrap().enable);
|
||||
assert!(hint.inverted_index);
|
||||
assert!(hint.skipping_index.is_some());
|
||||
let rebuilt = parse_json2_column(&format!(
|
||||
"CREATE TABLE logs (attrs JSON2 {options}, ts TIMESTAMP TIME INDEX)"
|
||||
));
|
||||
assert_eq!(
|
||||
settings,
|
||||
rebuilt
|
||||
.extensions
|
||||
.json2_options
|
||||
.unwrap()
|
||||
.build_json_settings()
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
for hint in [
|
||||
"message BIGINT FULLTEXT INDEX",
|
||||
"message STRING FULLTEXT INDEX FULLTEXT INDEX",
|
||||
"message STRING FULLTEXT",
|
||||
"message STRING FULLTEXT INDEX WITH (analyzer='unknown')",
|
||||
"message STRING FULLTEXT INDEX WITH (backend='unknown')",
|
||||
"message STRING FULLTEXT INDEX WITH (granularity='0')",
|
||||
"message STRING FULLTEXT INDEX WITH (false_positive_rate='0')",
|
||||
"message STRING FULLTEXT INDEX WITH (unknown='1')",
|
||||
] {
|
||||
let sql = format!("CREATE TABLE logs (attrs JSON2 ({hint}), ts TIMESTAMP TIME INDEX)");
|
||||
let parsed = ParserContext::create_with_dialect(
|
||||
&sql,
|
||||
&GreptimeDbDialect {},
|
||||
ParseOptions::default(),
|
||||
);
|
||||
assert!(parsed.is_err(), "{sql}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json2_skipping_index() {
|
||||
let column = parse_json2_column(
|
||||
"CREATE TABLE logs (attrs JSON2 (service.name STRING SKIPPING INDEX WITH (granularity='8', false_positive_rate='0.05') INVERTED INDEX, status BIGINT SKIPPING INDEX), ts TIMESTAMP TIME INDEX)",
|
||||
);
|
||||
let options = column.extensions.json2_options.as_ref().unwrap();
|
||||
let settings = options.build_json_settings().unwrap();
|
||||
let hint = &settings.type_hints()[0];
|
||||
assert!(hint.inverted_index);
|
||||
assert_eq!(hint.skipping_index.as_ref().unwrap().granularity, 8);
|
||||
assert_eq!(
|
||||
hint.skipping_index.as_ref().unwrap().false_positive_rate(),
|
||||
0.05
|
||||
);
|
||||
assert_eq!(
|
||||
settings.type_hints()[1].skipping_index,
|
||||
Some(datatypes::schema::SkippingIndexOptions::default())
|
||||
);
|
||||
let rebuilt = parse_json2_column(&format!(
|
||||
"CREATE TABLE logs (attrs JSON2 {options}, ts TIMESTAMP TIME INDEX)"
|
||||
));
|
||||
assert_eq!(
|
||||
settings,
|
||||
rebuilt
|
||||
.extensions
|
||||
.json2_options
|
||||
.unwrap()
|
||||
.build_json_settings()
|
||||
.unwrap()
|
||||
);
|
||||
for option in [
|
||||
"SKIPPING INDEX SKIPPING INDEX",
|
||||
"SKIPPING",
|
||||
"SKIPPING INDEX WITH (granularity='0')",
|
||||
"SKIPPING INDEX WITH (false_positive_rate='0')",
|
||||
"SKIPPING INDEX WITH (type='unknown')",
|
||||
"SKIPPING INDEX WITH (unknown='1')",
|
||||
] {
|
||||
let sql = format!(
|
||||
"CREATE TABLE logs (attrs JSON2 (a STRING {option}), ts TIMESTAMP TIME INDEX)"
|
||||
);
|
||||
assert!(
|
||||
ParserContext::create_with_dialect(
|
||||
&sql,
|
||||
&GreptimeDbDialect {},
|
||||
ParseOptions::default()
|
||||
)
|
||||
.is_err(),
|
||||
"{sql}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_json2_type_hint_path() {
|
||||
assert_eq!(
|
||||
|
||||
@@ -18,10 +18,11 @@ use sqlparser::tokenizer::Token;
|
||||
|
||||
use crate::error::{self, InvalidSqlSnafu, InvalidTableNameSnafu, Result, UnexpectedTokenSnafu};
|
||||
use crate::parser::ParserContext;
|
||||
use crate::parsers::utils;
|
||||
use crate::statements::statement::Statement;
|
||||
use crate::statements::truncate::TruncateTable;
|
||||
|
||||
/// `TRUNCATE [TABLE] table_name;`
|
||||
/// `TRUNCATE [TABLE] table_name [FILE RANGE ...] [WITH (wait = ..., timeout = ...)];`
|
||||
impl ParserContext<'_> {
|
||||
pub(crate) fn parse_truncate(&mut self) -> Result<Statement> {
|
||||
let _ = self.parser.next_token();
|
||||
@@ -46,7 +47,10 @@ impl ParserContext<'_> {
|
||||
|
||||
// if no range is specified, we just truncate the table
|
||||
if !have_range {
|
||||
return Ok(Statement::TruncateTable(TruncateTable::new(table_ident)));
|
||||
let options = utils::parse_ddl_options(&mut self.parser)?;
|
||||
return Ok(Statement::TruncateTable(
|
||||
TruncateTable::new(table_ident).with_options(options),
|
||||
));
|
||||
}
|
||||
|
||||
// parse a list of time ranges consist of (Timestamp, Timestamp),?
|
||||
@@ -100,6 +104,7 @@ impl ParserContext<'_> {
|
||||
let peek = self.parser.peek_token().token;
|
||||
|
||||
match peek {
|
||||
Token::Word(word) if word.keyword == Keyword::WITH => break,
|
||||
sqlparser::tokenizer::Token::EOF | Token::SemiColon => {
|
||||
if time_ranges.is_empty() {
|
||||
return Err(InvalidSqlSnafu {
|
||||
@@ -112,24 +117,26 @@ impl ParserContext<'_> {
|
||||
Token::Comma => {
|
||||
self.parser.next_token(); // Consume the comma
|
||||
let next_peek = self.parser.peek_token().token; // Peek the token after the comma
|
||||
if matches!(next_peek, Token::EOF | Token::SemiColon) {
|
||||
if matches!(next_peek, Token::EOF | Token::SemiColon)
|
||||
|| matches!(next_peek, Token::Word(word) if word.keyword == Keyword::WITH)
|
||||
{
|
||||
break; // Trailing comma, end of statement
|
||||
}
|
||||
// Otherwise, continue to parse next range
|
||||
continue;
|
||||
}
|
||||
_ => UnexpectedTokenSnafu {
|
||||
expected: "a comma or end of statement",
|
||||
expected: "a comma, WITH or end of statement",
|
||||
actual: self.peek_token_as_string(),
|
||||
}
|
||||
.fail()?,
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Statement::TruncateTable(TruncateTable::new_with_ranges(
|
||||
table_ident,
|
||||
time_ranges,
|
||||
)))
|
||||
let options = utils::parse_ddl_options(&mut self.parser)?;
|
||||
Ok(Statement::TruncateTable(
|
||||
TruncateTable::new_with_ranges(table_ident, time_ranges).with_options(options),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,6 +148,64 @@ mod tests {
|
||||
use crate::dialect::GreptimeDbDialect;
|
||||
use crate::parser::ParseOptions;
|
||||
|
||||
#[test]
|
||||
fn test_parse_truncate_ddl_options() {
|
||||
for prefix in [
|
||||
"TRUNCATE foo",
|
||||
"TRUNCATE TABLE foo",
|
||||
"TRUNCATE TABLE foo FILE RANGE (0, 20)",
|
||||
"TRUNCATE TABLE foo FILE RANGE (0, 20), (30, 40),",
|
||||
] {
|
||||
let sql = format!("{prefix} WITH (WAIT = false, TIMEOUT = '30s');");
|
||||
let statements = ParserContext::create_with_dialect(
|
||||
&sql,
|
||||
&GreptimeDbDialect {},
|
||||
ParseOptions::default(),
|
||||
)
|
||||
.unwrap();
|
||||
let Statement::TruncateTable(truncate) = &statements[0] else {
|
||||
unreachable!()
|
||||
};
|
||||
assert_eq!(truncate.options().get("wait"), Some("false"));
|
||||
assert_eq!(truncate.options().get("timeout"), Some("30s"));
|
||||
|
||||
let formatted = truncate.to_string();
|
||||
let reparsed = ParserContext::create_with_dialect(
|
||||
&formatted,
|
||||
&GreptimeDbDialect {},
|
||||
ParseOptions::default(),
|
||||
)
|
||||
.unwrap();
|
||||
let Statement::TruncateTable(reparsed) = &reparsed[0] else {
|
||||
unreachable!()
|
||||
};
|
||||
assert_eq!(truncate.table_name(), reparsed.table_name());
|
||||
assert_eq!(truncate.time_ranges(), reparsed.time_ranges());
|
||||
assert_eq!(
|
||||
truncate.options().to_str_map(),
|
||||
reparsed.options().to_str_map()
|
||||
);
|
||||
}
|
||||
|
||||
for options in [
|
||||
"ttl = '1d'",
|
||||
"unknown = true",
|
||||
"wait = NULL",
|
||||
"timeout = ['30s']",
|
||||
] {
|
||||
let sql = format!("TRUNCATE TABLE foo WITH ({options})");
|
||||
assert!(
|
||||
ParserContext::create_with_dialect(
|
||||
&sql,
|
||||
&GreptimeDbDialect {},
|
||||
ParseOptions::default(),
|
||||
)
|
||||
.is_err(),
|
||||
"{sql}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn test_parse_truncate_with_ranges() {
|
||||
let sql = r#"TRUNCATE foo FILE RANGE (0, 20)"#;
|
||||
@@ -451,7 +516,7 @@ mod tests {
|
||||
assert!(
|
||||
result.is_err()
|
||||
&& format!("{result:?}")
|
||||
.contains("expected: 'a comma or end of statement', found: ("),
|
||||
.contains("expected: 'a comma, WITH or end of statement', found: ("),
|
||||
"result is: {result:?}"
|
||||
);
|
||||
|
||||
|
||||
@@ -28,18 +28,20 @@ use datafusion_expr::{AggregateUDF, Expr, HigherOrderUDF, ScalarUDF, TableSource
|
||||
use datafusion_sql::planner::{ContextProvider, SqlToRel};
|
||||
use datatypes::arrow::datatypes::DataType;
|
||||
use datatypes::schema::{
|
||||
COLUMN_COMPRESSION_OPT_KEY_LEVEL, COLUMN_COMPRESSION_OPT_KEY_PAGE_SIZE,
|
||||
COLUMN_COMPRESSION_OPT_KEY_TYPE, COLUMN_FULLTEXT_OPT_KEY_ANALYZER,
|
||||
COLUMN_FULLTEXT_OPT_KEY_BACKEND, COLUMN_FULLTEXT_OPT_KEY_CASE_SENSITIVE,
|
||||
COLUMN_FULLTEXT_OPT_KEY_FALSE_POSITIVE_RATE, COLUMN_FULLTEXT_OPT_KEY_GRANULARITY,
|
||||
COLUMN_SKIPPING_INDEX_OPT_KEY_FALSE_POSITIVE_RATE, COLUMN_SKIPPING_INDEX_OPT_KEY_GRANULARITY,
|
||||
COLUMN_SKIPPING_INDEX_OPT_KEY_TYPE,
|
||||
COLUMN_COMPRESSION_OPT_KEY_ENCODING, COLUMN_COMPRESSION_OPT_KEY_LEVEL,
|
||||
COLUMN_COMPRESSION_OPT_KEY_PAGE_SIZE, COLUMN_COMPRESSION_OPT_KEY_TYPE,
|
||||
COLUMN_FULLTEXT_OPT_KEY_ANALYZER, COLUMN_FULLTEXT_OPT_KEY_BACKEND,
|
||||
COLUMN_FULLTEXT_OPT_KEY_CASE_SENSITIVE, COLUMN_FULLTEXT_OPT_KEY_FALSE_POSITIVE_RATE,
|
||||
COLUMN_FULLTEXT_OPT_KEY_GRANULARITY, COLUMN_SKIPPING_INDEX_OPT_KEY_FALSE_POSITIVE_RATE,
|
||||
COLUMN_SKIPPING_INDEX_OPT_KEY_GRANULARITY, COLUMN_SKIPPING_INDEX_OPT_KEY_TYPE,
|
||||
};
|
||||
use snafu::{ResultExt, ensure};
|
||||
use sqlparser::dialect::Dialect;
|
||||
use sqlparser::keywords::Keyword;
|
||||
use sqlparser::parser::Parser;
|
||||
use table::requests::{SEMANTIC_PREFIX, validate_semantic_option, validate_table_option};
|
||||
use table::requests::{
|
||||
SEMANTIC_PREFIX, VALID_DDL_OPTION_KEYS, validate_semantic_option, validate_table_option,
|
||||
};
|
||||
|
||||
use crate::error::{
|
||||
ConvertToLogicalExpressionSnafu, InvalidSqlSnafu, InvalidTableOptionSnafu, ParseSqlValueSnafu,
|
||||
@@ -382,6 +384,7 @@ pub fn validate_column_compression_create_option(key: &str) -> bool {
|
||||
COLUMN_COMPRESSION_OPT_KEY_TYPE,
|
||||
COLUMN_COMPRESSION_OPT_KEY_LEVEL,
|
||||
COLUMN_COMPRESSION_OPT_KEY_PAGE_SIZE,
|
||||
COLUMN_COMPRESSION_OPT_KEY_ENCODING,
|
||||
]
|
||||
.contains(&key)
|
||||
}
|
||||
@@ -422,15 +425,32 @@ pub fn convert_month_day_nano_to_duration(
|
||||
Ok(std::time::Duration::new(adjusted_seconds, nanos_remainder))
|
||||
}
|
||||
|
||||
/// Parses submission options, accepting only scalar `wait` and `timeout` values.
|
||||
pub(crate) fn parse_ddl_options(parser: &mut Parser) -> Result<OptionMap> {
|
||||
parse_with_options_inner(parser, true)
|
||||
}
|
||||
|
||||
pub fn parse_with_options(parser: &mut Parser) -> Result<OptionMap> {
|
||||
parse_with_options_inner(parser, false)
|
||||
}
|
||||
|
||||
fn parse_with_options_inner(parser: &mut Parser, ddl_only: bool) -> Result<OptionMap> {
|
||||
let options = parser
|
||||
.parse_options(Keyword::WITH)
|
||||
.context(SyntaxSnafu)?
|
||||
.into_iter()
|
||||
.map(parse_option_string)
|
||||
.collect::<Result<HashMap<String, OptionValue>>>()?;
|
||||
// Validate before OptionMap can discard non-scalar secret options.
|
||||
for (key, value) in &options {
|
||||
if key.starts_with(SEMANTIC_PREFIX) {
|
||||
if ddl_only {
|
||||
ensure!(
|
||||
VALID_DDL_OPTION_KEYS.contains(&key.as_str()) && value.as_string().is_some(),
|
||||
InvalidSqlSnafu {
|
||||
msg: format!("invalid DDL option '{key}': '{value}'")
|
||||
}
|
||||
);
|
||||
} else if key.starts_with(SEMANTIC_PREFIX) {
|
||||
// Semantic keys are whitelisted and value-checked against their domain,
|
||||
// so a user cannot set an unknown key or an out-of-range value.
|
||||
let value = value.as_string().unwrap_or_default();
|
||||
|
||||
@@ -733,6 +733,8 @@ mod tests {
|
||||
path: vec!["kind".to_string()],
|
||||
data_type: ConcreteDataType::string_datatype(),
|
||||
inverted_index: false,
|
||||
skipping_index: None,
|
||||
fulltext_index: None,
|
||||
}],
|
||||
None,
|
||||
)?)?;
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::{Display, Formatter, Write};
|
||||
use std::ops::ControlFlow;
|
||||
|
||||
use common_catalog::consts::FILE_ENGINE;
|
||||
use datatypes::json::{JSON2_DEFAULT_MAX_AUTO_EXPANDED_PATHS, JsonSettings};
|
||||
@@ -22,7 +23,7 @@ use datatypes::schema::{CompressionOptions, FulltextOptions, SkippingIndexOption
|
||||
use itertools::Itertools;
|
||||
use serde::Serialize;
|
||||
use snafu::ResultExt;
|
||||
use sqlparser::ast::{ColumnOptionDef, DataType, Expr};
|
||||
use sqlparser::ast::{ColumnOptionDef, DataType, Expr, visit_expressions_mut};
|
||||
use sqlparser_derive::{Visit, VisitMut};
|
||||
|
||||
use crate::ast::{ColumnDef, Ident, ObjectName};
|
||||
@@ -200,6 +201,33 @@ impl Display for Json2Options {
|
||||
}
|
||||
f.write_char(' ')?;
|
||||
write_data_type(f, &hint.data_type)?;
|
||||
if let Some(options) = &hint.skipping_index {
|
||||
write!(
|
||||
f,
|
||||
" SKIPPING INDEX WITH (granularity = '{}', false_positive_rate = '{}', type = '{}')",
|
||||
options.granularity,
|
||||
options.false_positive_rate(),
|
||||
options.index_type
|
||||
)?;
|
||||
}
|
||||
if let Some(options) = &hint.fulltext_index
|
||||
&& options.enable
|
||||
{
|
||||
write!(
|
||||
f,
|
||||
" FULLTEXT INDEX WITH (analyzer = '{}', case_sensitive = '{}', backend = '{}'",
|
||||
options.analyzer, options.case_sensitive, options.backend
|
||||
)?;
|
||||
if options.backend == datatypes::schema::FulltextBackend::Bloom {
|
||||
write!(
|
||||
f,
|
||||
", granularity = '{}', false_positive_rate = '{}'",
|
||||
options.granularity,
|
||||
options.false_positive_rate()
|
||||
)?;
|
||||
}
|
||||
f.write_char(')')?;
|
||||
}
|
||||
if hint.inverted_index {
|
||||
f.write_str(" INVERTED INDEX")?;
|
||||
}
|
||||
@@ -218,6 +246,8 @@ impl Json2Options {
|
||||
path: hint.path.clone(),
|
||||
data_type: sql_data_type_to_concrete_data_type(&hint.data_type)?,
|
||||
inverted_index: hint.inverted_index,
|
||||
skipping_index: hint.skipping_index.clone(),
|
||||
fulltext_index: hint.fulltext_index.clone(),
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
@@ -233,6 +263,10 @@ pub struct JsonTypeHint {
|
||||
pub path: Vec<String>,
|
||||
pub data_type: DataType,
|
||||
pub inverted_index: bool,
|
||||
/// Skipping index options for this hinted path.
|
||||
pub skipping_index: Option<SkippingIndexOptions>,
|
||||
/// Fulltext index options for a string hinted path.
|
||||
pub fulltext_index: Option<FulltextOptions>,
|
||||
}
|
||||
|
||||
impl Column {
|
||||
@@ -353,6 +387,8 @@ impl ColumnExtensions {
|
||||
path: hint.path,
|
||||
data_type,
|
||||
inverted_index: hint.inverted_index,
|
||||
skipping_index: hint.skipping_index,
|
||||
fulltext_index: hint.fulltext_index,
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
@@ -395,11 +431,17 @@ pub struct Partitions {
|
||||
}
|
||||
|
||||
impl Partitions {
|
||||
/// set quotes to all [Ident]s from column list
|
||||
/// Sets quotes to all column [Ident]s, in the column list and in the expressions.
|
||||
pub fn set_quote(&mut self, quote_style: char) {
|
||||
self.column_list
|
||||
.iter_mut()
|
||||
.for_each(|c| c.quote_style = Some(quote_style));
|
||||
let _ = visit_expressions_mut(&mut self.exprs, |expr| {
|
||||
if let Expr::Identifier(ident) = expr {
|
||||
ident.quote_style = Some(quote_style);
|
||||
}
|
||||
ControlFlow::<()>::Continue(())
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -811,6 +853,8 @@ mod tests {
|
||||
path: vec!["x".repeat(8192)],
|
||||
data_type: DataType::Text,
|
||||
inverted_index: true,
|
||||
skipping_index: None,
|
||||
fulltext_index: None,
|
||||
}],
|
||||
});
|
||||
assert!(!create.consume_sql_size_budget(&mut 4096));
|
||||
@@ -831,6 +875,8 @@ mod tests {
|
||||
path: vec![path.into()],
|
||||
data_type: DataType::Text,
|
||||
inverted_index: false,
|
||||
skipping_index: None,
|
||||
fulltext_index: None,
|
||||
}],
|
||||
};
|
||||
assert_eq!(
|
||||
@@ -1156,26 +1202,36 @@ ENGINE=mito
|
||||
path: vec!["i".to_string()],
|
||||
data_type: ConcreteDataType::int64_datatype(),
|
||||
inverted_index: false,
|
||||
skipping_index: None,
|
||||
fulltext_index: None,
|
||||
},
|
||||
DatatypeJsonTypeHint {
|
||||
path: vec!["f".to_string()],
|
||||
data_type: ConcreteDataType::float64_datatype(),
|
||||
inverted_index: false,
|
||||
skipping_index: None,
|
||||
fulltext_index: None,
|
||||
},
|
||||
DatatypeJsonTypeHint {
|
||||
path: vec!["u".to_string()],
|
||||
data_type: ConcreteDataType::uint64_datatype(),
|
||||
inverted_index: false,
|
||||
skipping_index: None,
|
||||
fulltext_index: None,
|
||||
},
|
||||
DatatypeJsonTypeHint {
|
||||
path: vec!["s".to_string()],
|
||||
data_type: ConcreteDataType::string_datatype(),
|
||||
inverted_index: false,
|
||||
skipping_index: None,
|
||||
fulltext_index: None,
|
||||
},
|
||||
DatatypeJsonTypeHint {
|
||||
path: vec!["b".to_string()],
|
||||
data_type: ConcreteDataType::boolean_datatype(),
|
||||
inverted_index: false,
|
||||
skipping_index: None,
|
||||
fulltext_index: None,
|
||||
},
|
||||
],
|
||||
None,
|
||||
@@ -1202,6 +1258,8 @@ ENGINE=mito
|
||||
path: vec!["u".to_string()],
|
||||
data_type: ConcreteDataType::date_datatype(),
|
||||
inverted_index: false,
|
||||
skipping_index: None,
|
||||
fulltext_index: None,
|
||||
}],
|
||||
None,
|
||||
)
|
||||
|
||||
@@ -18,11 +18,15 @@ use itertools::Itertools;
|
||||
use serde::Serialize;
|
||||
use sqlparser::ast::{ObjectName, Visit, VisitMut, Visitor, VisitorMut};
|
||||
|
||||
use crate::statements::OptionMap;
|
||||
|
||||
/// TRUNCATE TABLE statement.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||
pub struct TruncateTable {
|
||||
table_name: ObjectName,
|
||||
time_ranges: Vec<(sqlparser::ast::Value, sqlparser::ast::Value)>,
|
||||
/// Options for this DDL submission, never persisted as table options.
|
||||
options: OptionMap,
|
||||
}
|
||||
|
||||
impl Visit for TruncateTable {
|
||||
@@ -32,6 +36,7 @@ impl Visit for TruncateTable {
|
||||
start.visit(visitor)?;
|
||||
end.visit(visitor)?;
|
||||
}
|
||||
Visit::visit(&self.options, visitor)?;
|
||||
::std::ops::ControlFlow::Continue(())
|
||||
}
|
||||
}
|
||||
@@ -43,6 +48,7 @@ impl VisitMut for TruncateTable {
|
||||
start.visit(visitor)?;
|
||||
end.visit(visitor)?;
|
||||
}
|
||||
VisitMut::visit(&mut self.options, visitor)?;
|
||||
::std::ops::ControlFlow::Continue(())
|
||||
}
|
||||
}
|
||||
@@ -53,6 +59,7 @@ impl TruncateTable {
|
||||
Self {
|
||||
table_name,
|
||||
time_ranges: Vec::new(),
|
||||
options: OptionMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,9 +71,21 @@ impl TruncateTable {
|
||||
Self {
|
||||
table_name,
|
||||
time_ranges,
|
||||
options: OptionMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the options for this DDL submission.
|
||||
pub fn with_options(mut self, options: OptionMap) -> Self {
|
||||
self.options = options;
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns the options for this DDL submission.
|
||||
pub fn options(&self) -> &OptionMap {
|
||||
&self.options
|
||||
}
|
||||
|
||||
pub fn table_name(&self) -> &ObjectName {
|
||||
&self.table_name
|
||||
}
|
||||
@@ -80,19 +99,23 @@ impl Display for TruncateTable {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let table_name = self.table_name();
|
||||
write!(f, r#"TRUNCATE TABLE {table_name}"#)?;
|
||||
if self.time_ranges.is_empty() {
|
||||
return Ok(());
|
||||
if !self.time_ranges.is_empty() {
|
||||
write!(f, " FILE RANGE ")?;
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
self.time_ranges
|
||||
.iter()
|
||||
.map(|(start, end)| format!("({}, {})", start, end))
|
||||
.join(", ")
|
||||
)?;
|
||||
}
|
||||
|
||||
write!(f, " FILE RANGE ")?;
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
self.time_ranges
|
||||
.iter()
|
||||
.map(|(start, end)| format!("({}, {})", start, end))
|
||||
.join(", ")
|
||||
)
|
||||
if !self.options.is_empty() {
|
||||
f.write_str(" WITH (")?;
|
||||
self.options.write_sql(f, ", ", "", false)?;
|
||||
f.write_str(")")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user