mirror of
https://github.com/GreptimeTeam/greptimedb.git
synced 2026-01-16 18:22:55 +00:00
1615 lines
56 KiB
Rust
1615 lines
56 KiB
Rust
// Copyright 2023 Greptime Team
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
#[cfg(feature = "enterprise")]
|
|
pub mod trigger;
|
|
|
|
use std::collections::{HashMap, HashSet};
|
|
|
|
use api::helper::ColumnDataTypeWrapper;
|
|
use api::v1::alter_database_expr::Kind as AlterDatabaseKind;
|
|
use api::v1::alter_table_expr::Kind as AlterTableKind;
|
|
use api::v1::column_def::{options_from_column_schema, try_as_column_schema};
|
|
use api::v1::{
|
|
AddColumn, AddColumns, AlterDatabaseExpr, AlterTableExpr, Analyzer, ColumnDataType,
|
|
ColumnDataTypeExtension, CreateFlowExpr, CreateTableExpr, CreateViewExpr, DropColumn,
|
|
DropColumns, DropDefaults, ExpireAfter, FulltextBackend as PbFulltextBackend, ModifyColumnType,
|
|
ModifyColumnTypes, RenameTable, SemanticType, SetDatabaseOptions, SetDefaults, SetFulltext,
|
|
SetIndex, SetIndexes, SetInverted, SetSkipping, SetTableOptions,
|
|
SkippingIndexType as PbSkippingIndexType, TableName, UnsetDatabaseOptions, UnsetFulltext,
|
|
UnsetIndex, UnsetIndexes, UnsetInverted, UnsetSkipping, UnsetTableOptions, set_index,
|
|
unset_index,
|
|
};
|
|
use common_error::ext::BoxedError;
|
|
use common_grpc_expr::util::ColumnExpr;
|
|
use common_time::Timezone;
|
|
use datafusion::sql::planner::object_name_to_table_reference;
|
|
use datatypes::schema::{
|
|
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, COMMENT_KEY,
|
|
ColumnDefaultConstraint, ColumnSchema, FulltextAnalyzer, FulltextBackend, Schema,
|
|
SkippingIndexType,
|
|
};
|
|
use file_engine::FileOptions;
|
|
use query::sql::{
|
|
check_file_to_table_schema_compatibility, file_column_schemas_to_table,
|
|
infer_file_table_schema, prepare_file_table_files,
|
|
};
|
|
use session::context::QueryContextRef;
|
|
use session::table_name::table_idents_to_full_name;
|
|
use snafu::{OptionExt, ResultExt, ensure};
|
|
use sql::ast::{
|
|
ColumnDef, ColumnOption, ColumnOptionDef, Expr, Ident, ObjectName, ObjectNamePartExt,
|
|
};
|
|
use sql::dialect::GreptimeDbDialect;
|
|
use sql::parser::ParserContext;
|
|
use sql::statements::alter::{
|
|
AlterDatabase, AlterDatabaseOperation, AlterTable, AlterTableOperation,
|
|
};
|
|
use sql::statements::create::{
|
|
Column as SqlColumn, ColumnExtensions, CreateExternalTable, CreateFlow, CreateTable,
|
|
CreateView, TableConstraint,
|
|
};
|
|
use sql::statements::{
|
|
OptionMap, column_to_schema, concrete_data_type_to_sql_data_type,
|
|
sql_column_def_to_grpc_column_def, sql_data_type_to_concrete_data_type, value_to_sql_value,
|
|
};
|
|
use sql::util::extract_tables_from_query;
|
|
use store_api::mito_engine_options::{COMPACTION_OVERRIDE, COMPACTION_TYPE};
|
|
use table::requests::{FILE_TABLE_META_KEY, TableOptions};
|
|
use table::table_reference::TableReference;
|
|
#[cfg(feature = "enterprise")]
|
|
pub use trigger::to_create_trigger_task_expr;
|
|
|
|
use crate::error::{
|
|
BuildCreateExprOnInsertionSnafu, ColumnDataTypeSnafu, ConvertColumnDefaultConstraintSnafu,
|
|
ConvertIdentifierSnafu, EncodeJsonSnafu, ExternalSnafu, FindNewColumnsOnInsertionSnafu,
|
|
IllegalPrimaryKeysDefSnafu, InferFileTableSchemaSnafu, InvalidColumnDefSnafu,
|
|
InvalidFlowNameSnafu, InvalidSqlSnafu, NotSupportedSnafu, ParseSqlSnafu, ParseSqlValueSnafu,
|
|
PrepareFileTableSnafu, Result, SchemaIncompatibleSnafu, UnrecognizedTableOptionSnafu,
|
|
};
|
|
|
|
pub fn create_table_expr_by_column_schemas(
|
|
table_name: &TableReference<'_>,
|
|
column_schemas: &[api::v1::ColumnSchema],
|
|
engine: &str,
|
|
desc: Option<&str>,
|
|
) -> Result<CreateTableExpr> {
|
|
let column_exprs = ColumnExpr::from_column_schemas(column_schemas);
|
|
let expr = common_grpc_expr::util::build_create_table_expr(
|
|
None,
|
|
table_name,
|
|
column_exprs,
|
|
engine,
|
|
desc.unwrap_or("Created on insertion"),
|
|
)
|
|
.context(BuildCreateExprOnInsertionSnafu)?;
|
|
|
|
validate_create_expr(&expr)?;
|
|
Ok(expr)
|
|
}
|
|
|
|
pub fn extract_add_columns_expr(
|
|
schema: &Schema,
|
|
column_exprs: Vec<ColumnExpr>,
|
|
) -> Result<Option<AddColumns>> {
|
|
let add_columns = common_grpc_expr::util::extract_new_columns(schema, column_exprs)
|
|
.context(FindNewColumnsOnInsertionSnafu)?;
|
|
if let Some(add_columns) = &add_columns {
|
|
validate_add_columns_expr(add_columns)?;
|
|
}
|
|
Ok(add_columns)
|
|
}
|
|
|
|
// cpu float64,
|
|
// memory float64,
|
|
// TIME INDEX (ts),
|
|
// PRIMARY KEY(host)
|
|
// ) WITH (location='/var/data/city.csv', format='csv');
|
|
// ```
|
|
// The user needs to specify the TIME INDEX column. If there is no suitable
|
|
// column in the file to use as TIME INDEX, an additional placeholder column
|
|
// needs to be created as the TIME INDEX, and a `DEFAULT <value>` constraint
|
|
// should be added.
|
|
//
|
|
//
|
|
// When the `CREATE EXTERNAL TABLE` statement is in inferred form, like
|
|
// ```sql
|
|
// CREATE EXTERNAL TABLE IF NOT EXISTS city WITH (location='/var/data/city.csv',format='csv');
|
|
// ```
|
|
// 1. If the TIME INDEX column can be inferred from metadata, use that column
|
|
// as the TIME INDEX. Otherwise,
|
|
// 2. If a column named `greptime_timestamp` exists (with the requirement that
|
|
// the column is with type TIMESTAMP, otherwise an error is thrown), use
|
|
// that column as the TIME INDEX. Otherwise,
|
|
// 3. Automatically create the `greptime_timestamp` column and add a `DEFAULT 0`
|
|
// constraint.
|
|
pub(crate) async fn create_external_expr(
|
|
create: CreateExternalTable,
|
|
query_ctx: &QueryContextRef,
|
|
) -> Result<CreateTableExpr> {
|
|
let (catalog_name, schema_name, table_name) =
|
|
table_idents_to_full_name(&create.name, query_ctx)
|
|
.map_err(BoxedError::new)
|
|
.context(ExternalSnafu)?;
|
|
|
|
let mut table_options = create.options.into_map();
|
|
|
|
let (object_store, files) = prepare_file_table_files(&table_options)
|
|
.await
|
|
.context(PrepareFileTableSnafu)?;
|
|
|
|
let file_column_schemas = infer_file_table_schema(&object_store, &files, &table_options)
|
|
.await
|
|
.context(InferFileTableSchemaSnafu)?
|
|
.column_schemas;
|
|
|
|
let (time_index, primary_keys, table_column_schemas) = if !create.columns.is_empty() {
|
|
// expanded form
|
|
let time_index = find_time_index(&create.constraints)?;
|
|
let primary_keys = find_primary_keys(&create.columns, &create.constraints)?;
|
|
let column_schemas =
|
|
columns_to_column_schemas(&create.columns, &time_index, Some(&query_ctx.timezone()))?;
|
|
(time_index, primary_keys, column_schemas)
|
|
} else {
|
|
// inferred form
|
|
let (column_schemas, time_index) = file_column_schemas_to_table(&file_column_schemas);
|
|
let primary_keys = vec![];
|
|
(time_index, primary_keys, column_schemas)
|
|
};
|
|
|
|
check_file_to_table_schema_compatibility(&file_column_schemas, &table_column_schemas)
|
|
.context(SchemaIncompatibleSnafu)?;
|
|
|
|
let meta = FileOptions {
|
|
files,
|
|
file_column_schemas,
|
|
};
|
|
table_options.insert(
|
|
FILE_TABLE_META_KEY.to_string(),
|
|
serde_json::to_string(&meta).context(EncodeJsonSnafu)?,
|
|
);
|
|
|
|
let column_defs = column_schemas_to_defs(table_column_schemas, &primary_keys)?;
|
|
let expr = CreateTableExpr {
|
|
catalog_name,
|
|
schema_name,
|
|
table_name,
|
|
desc: String::default(),
|
|
column_defs,
|
|
time_index,
|
|
primary_keys,
|
|
create_if_not_exists: create.if_not_exists,
|
|
table_options,
|
|
table_id: None,
|
|
engine: create.engine.clone(),
|
|
};
|
|
|
|
Ok(expr)
|
|
}
|
|
|
|
/// Convert `CreateTable` statement to [`CreateTableExpr`] gRPC request.
|
|
pub fn create_to_expr(
|
|
create: &CreateTable,
|
|
query_ctx: &QueryContextRef,
|
|
) -> Result<CreateTableExpr> {
|
|
let (catalog_name, schema_name, table_name) =
|
|
table_idents_to_full_name(&create.name, query_ctx)
|
|
.map_err(BoxedError::new)
|
|
.context(ExternalSnafu)?;
|
|
|
|
let time_index = find_time_index(&create.constraints)?;
|
|
let table_options = HashMap::from(
|
|
&TableOptions::try_from_iter(create.options.to_str_map())
|
|
.context(UnrecognizedTableOptionSnafu)?,
|
|
);
|
|
|
|
let mut table_options = table_options;
|
|
if table_options.contains_key(COMPACTION_TYPE) {
|
|
table_options.insert(COMPACTION_OVERRIDE.to_string(), "true".to_string());
|
|
}
|
|
|
|
let primary_keys = find_primary_keys(&create.columns, &create.constraints)?;
|
|
|
|
let expr = CreateTableExpr {
|
|
catalog_name,
|
|
schema_name,
|
|
table_name,
|
|
desc: String::default(),
|
|
column_defs: columns_to_expr(
|
|
&create.columns,
|
|
&time_index,
|
|
&primary_keys,
|
|
Some(&query_ctx.timezone()),
|
|
)?,
|
|
time_index,
|
|
primary_keys,
|
|
create_if_not_exists: create.if_not_exists,
|
|
table_options,
|
|
table_id: None,
|
|
engine: create.engine.clone(),
|
|
};
|
|
|
|
validate_create_expr(&expr)?;
|
|
Ok(expr)
|
|
}
|
|
|
|
/// Convert gRPC's [`CreateTableExpr`] back to `CreateTable` statement.
|
|
/// You can use `create_table_expr_by_column_schemas` to create a `CreateTableExpr` from column schemas.
|
|
///
|
|
/// # Parameters
|
|
///
|
|
/// * `expr` - The `CreateTableExpr` to convert
|
|
/// * `quote_style` - Optional quote style for identifiers (defaults to MySQL style ` backtick)
|
|
pub fn expr_to_create(expr: &CreateTableExpr, quote_style: Option<char>) -> Result<CreateTable> {
|
|
let quote_style = quote_style.unwrap_or('`');
|
|
|
|
// Convert table name
|
|
let table_name = ObjectName(vec![sql::ast::ObjectNamePart::Identifier(
|
|
sql::ast::Ident::with_quote(quote_style, &expr.table_name),
|
|
)]);
|
|
|
|
// Convert columns
|
|
let mut columns = Vec::with_capacity(expr.column_defs.len());
|
|
for column_def in &expr.column_defs {
|
|
let column_schema = try_as_column_schema(column_def).context(InvalidColumnDefSnafu {
|
|
column: &column_def.name,
|
|
})?;
|
|
|
|
let mut options = Vec::new();
|
|
|
|
// Add NULL/NOT NULL constraint
|
|
if column_def.is_nullable {
|
|
options.push(ColumnOptionDef {
|
|
name: None,
|
|
option: ColumnOption::Null,
|
|
});
|
|
} else {
|
|
options.push(ColumnOptionDef {
|
|
name: None,
|
|
option: ColumnOption::NotNull,
|
|
});
|
|
}
|
|
|
|
// Add DEFAULT constraint if present
|
|
if let Some(default_constraint) = column_schema.default_constraint() {
|
|
let expr = match default_constraint {
|
|
ColumnDefaultConstraint::Value(v) => {
|
|
Expr::Value(value_to_sql_value(v).context(ParseSqlValueSnafu)?.into())
|
|
}
|
|
ColumnDefaultConstraint::Function(func_expr) => {
|
|
ParserContext::parse_function(func_expr, &GreptimeDbDialect {})
|
|
.context(ParseSqlSnafu)?
|
|
}
|
|
};
|
|
options.push(ColumnOptionDef {
|
|
name: None,
|
|
option: ColumnOption::Default(expr),
|
|
});
|
|
}
|
|
|
|
// Add COMMENT if present
|
|
if !column_def.comment.is_empty() {
|
|
options.push(ColumnOptionDef {
|
|
name: None,
|
|
option: ColumnOption::Comment(column_def.comment.clone()),
|
|
});
|
|
}
|
|
|
|
// Note: We don't add inline PRIMARY KEY options here,
|
|
// we'll handle all primary keys as constraints instead for consistency
|
|
|
|
// Handle column extensions (fulltext, inverted index, skipping index)
|
|
let mut extensions = ColumnExtensions::default();
|
|
|
|
// Add fulltext index options if present
|
|
if let Ok(Some(opt)) = column_schema.fulltext_options()
|
|
&& opt.enable
|
|
{
|
|
let mut map = HashMap::from([
|
|
(
|
|
COLUMN_FULLTEXT_OPT_KEY_ANALYZER.to_string(),
|
|
opt.analyzer.to_string(),
|
|
),
|
|
(
|
|
COLUMN_FULLTEXT_OPT_KEY_CASE_SENSITIVE.to_string(),
|
|
opt.case_sensitive.to_string(),
|
|
),
|
|
(
|
|
COLUMN_FULLTEXT_OPT_KEY_BACKEND.to_string(),
|
|
opt.backend.to_string(),
|
|
),
|
|
]);
|
|
if opt.backend == FulltextBackend::Bloom {
|
|
map.insert(
|
|
COLUMN_FULLTEXT_OPT_KEY_GRANULARITY.to_string(),
|
|
opt.granularity.to_string(),
|
|
);
|
|
map.insert(
|
|
COLUMN_FULLTEXT_OPT_KEY_FALSE_POSITIVE_RATE.to_string(),
|
|
opt.false_positive_rate().to_string(),
|
|
);
|
|
}
|
|
extensions.fulltext_index_options = Some(map.into());
|
|
}
|
|
|
|
// Add skipping index options if present
|
|
if let Ok(Some(opt)) = column_schema.skipping_index_options() {
|
|
let map = HashMap::from([
|
|
(
|
|
COLUMN_SKIPPING_INDEX_OPT_KEY_GRANULARITY.to_string(),
|
|
opt.granularity.to_string(),
|
|
),
|
|
(
|
|
COLUMN_SKIPPING_INDEX_OPT_KEY_FALSE_POSITIVE_RATE.to_string(),
|
|
opt.false_positive_rate().to_string(),
|
|
),
|
|
(
|
|
COLUMN_SKIPPING_INDEX_OPT_KEY_TYPE.to_string(),
|
|
opt.index_type.to_string(),
|
|
),
|
|
]);
|
|
extensions.skipping_index_options = Some(map.into());
|
|
}
|
|
|
|
// Add inverted index options if present
|
|
if column_schema.is_inverted_indexed() {
|
|
extensions.inverted_index_options = Some(HashMap::new().into());
|
|
}
|
|
|
|
let sql_column = SqlColumn {
|
|
column_def: ColumnDef {
|
|
name: Ident::with_quote(quote_style, &column_def.name),
|
|
data_type: concrete_data_type_to_sql_data_type(&column_schema.data_type)
|
|
.context(ParseSqlSnafu)?,
|
|
options,
|
|
},
|
|
extensions,
|
|
};
|
|
|
|
columns.push(sql_column);
|
|
}
|
|
|
|
// Convert constraints
|
|
let mut constraints = Vec::new();
|
|
|
|
// Add TIME INDEX constraint
|
|
constraints.push(TableConstraint::TimeIndex {
|
|
column: Ident::with_quote(quote_style, &expr.time_index),
|
|
});
|
|
|
|
// Add PRIMARY KEY constraint (always add as constraint for consistency)
|
|
if !expr.primary_keys.is_empty() {
|
|
let primary_key_columns: Vec<Ident> = expr
|
|
.primary_keys
|
|
.iter()
|
|
.map(|pk| Ident::with_quote(quote_style, pk))
|
|
.collect();
|
|
|
|
constraints.push(TableConstraint::PrimaryKey {
|
|
columns: primary_key_columns,
|
|
});
|
|
}
|
|
|
|
// Convert table options
|
|
let mut options = OptionMap::default();
|
|
for (key, value) in &expr.table_options {
|
|
options.insert(key.clone(), value.clone());
|
|
}
|
|
|
|
Ok(CreateTable {
|
|
if_not_exists: expr.create_if_not_exists,
|
|
table_id: expr.table_id.as_ref().map(|tid| tid.id).unwrap_or(0),
|
|
name: table_name,
|
|
columns,
|
|
engine: expr.engine.clone(),
|
|
constraints,
|
|
options,
|
|
partitions: None,
|
|
})
|
|
}
|
|
|
|
/// Validate the [`CreateTableExpr`] request.
|
|
pub fn validate_create_expr(create: &CreateTableExpr) -> Result<()> {
|
|
// construct column list
|
|
let mut column_to_indices = HashMap::with_capacity(create.column_defs.len());
|
|
for (idx, column) in create.column_defs.iter().enumerate() {
|
|
if let Some(indices) = column_to_indices.get(&column.name) {
|
|
return InvalidSqlSnafu {
|
|
err_msg: format!(
|
|
"column name `{}` is duplicated at index {} and {}",
|
|
column.name, indices, idx
|
|
),
|
|
}
|
|
.fail();
|
|
}
|
|
column_to_indices.insert(&column.name, idx);
|
|
}
|
|
|
|
// verify time_index exists
|
|
let _ = column_to_indices
|
|
.get(&create.time_index)
|
|
.with_context(|| InvalidSqlSnafu {
|
|
err_msg: format!(
|
|
"column name `{}` is not found in column list",
|
|
create.time_index
|
|
),
|
|
})?;
|
|
|
|
// verify primary_key exists
|
|
for pk in &create.primary_keys {
|
|
let _ = column_to_indices
|
|
.get(&pk)
|
|
.with_context(|| InvalidSqlSnafu {
|
|
err_msg: format!("column name `{}` is not found in column list", pk),
|
|
})?;
|
|
}
|
|
|
|
// construct primary_key set
|
|
let mut pk_set = HashSet::new();
|
|
for pk in &create.primary_keys {
|
|
if !pk_set.insert(pk) {
|
|
return InvalidSqlSnafu {
|
|
err_msg: format!("column name `{}` is duplicated in primary keys", pk),
|
|
}
|
|
.fail();
|
|
}
|
|
}
|
|
|
|
// verify time index is not primary key
|
|
if pk_set.contains(&create.time_index) {
|
|
return InvalidSqlSnafu {
|
|
err_msg: format!(
|
|
"column name `{}` is both primary key and time index",
|
|
create.time_index
|
|
),
|
|
}
|
|
.fail();
|
|
}
|
|
|
|
for column in &create.column_defs {
|
|
// verify do not contain interval type column issue #3235
|
|
if is_interval_type(&column.data_type()) {
|
|
return InvalidSqlSnafu {
|
|
err_msg: format!(
|
|
"column name `{}` is interval type, which is not supported",
|
|
column.name
|
|
),
|
|
}
|
|
.fail();
|
|
}
|
|
// verify do not contain datetime type column issue #5489
|
|
if is_date_time_type(&column.data_type()) {
|
|
return InvalidSqlSnafu {
|
|
err_msg: format!(
|
|
"column name `{}` is datetime type, which is not supported, please use `timestamp` type instead",
|
|
column.name
|
|
),
|
|
}
|
|
.fail();
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn validate_add_columns_expr(add_columns: &AddColumns) -> Result<()> {
|
|
for add_column in &add_columns.add_columns {
|
|
let Some(column_def) = &add_column.column_def else {
|
|
continue;
|
|
};
|
|
if is_date_time_type(&column_def.data_type()) {
|
|
return InvalidSqlSnafu {
|
|
err_msg: format!("column name `{}` is datetime type, which is not supported, please use `timestamp` type instead", column_def.name),
|
|
}
|
|
.fail();
|
|
}
|
|
if is_interval_type(&column_def.data_type()) {
|
|
return InvalidSqlSnafu {
|
|
err_msg: format!(
|
|
"column name `{}` is interval type, which is not supported",
|
|
column_def.name
|
|
),
|
|
}
|
|
.fail();
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn is_date_time_type(data_type: &ColumnDataType) -> bool {
|
|
matches!(data_type, ColumnDataType::Datetime)
|
|
}
|
|
|
|
fn is_interval_type(data_type: &ColumnDataType) -> bool {
|
|
matches!(
|
|
data_type,
|
|
ColumnDataType::IntervalYearMonth
|
|
| ColumnDataType::IntervalDayTime
|
|
| ColumnDataType::IntervalMonthDayNano
|
|
)
|
|
}
|
|
|
|
fn find_primary_keys(
|
|
columns: &[SqlColumn],
|
|
constraints: &[TableConstraint],
|
|
) -> Result<Vec<String>> {
|
|
let columns_pk = columns
|
|
.iter()
|
|
.filter_map(|x| {
|
|
if x.options()
|
|
.iter()
|
|
.any(|o| matches!(o.option, ColumnOption::PrimaryKey(_)))
|
|
{
|
|
Some(x.name().value.clone())
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.collect::<Vec<String>>();
|
|
|
|
ensure!(
|
|
columns_pk.len() <= 1,
|
|
IllegalPrimaryKeysDefSnafu {
|
|
msg: "not allowed to inline multiple primary keys in columns options"
|
|
}
|
|
);
|
|
|
|
let constraints_pk = constraints
|
|
.iter()
|
|
.filter_map(|constraint| match constraint {
|
|
TableConstraint::PrimaryKey { columns, .. } => {
|
|
Some(columns.iter().map(|ident| ident.value.clone()))
|
|
}
|
|
_ => None,
|
|
})
|
|
.flatten()
|
|
.collect::<Vec<String>>();
|
|
|
|
ensure!(
|
|
columns_pk.is_empty() || constraints_pk.is_empty(),
|
|
IllegalPrimaryKeysDefSnafu {
|
|
msg: "found definitions of primary keys in multiple places"
|
|
}
|
|
);
|
|
|
|
let mut primary_keys = Vec::with_capacity(columns_pk.len() + constraints_pk.len());
|
|
primary_keys.extend(columns_pk);
|
|
primary_keys.extend(constraints_pk);
|
|
Ok(primary_keys)
|
|
}
|
|
|
|
pub fn find_time_index(constraints: &[TableConstraint]) -> Result<String> {
|
|
let time_index = constraints
|
|
.iter()
|
|
.filter_map(|constraint| match constraint {
|
|
TableConstraint::TimeIndex { column, .. } => Some(&column.value),
|
|
_ => None,
|
|
})
|
|
.collect::<Vec<&String>>();
|
|
ensure!(
|
|
time_index.len() == 1,
|
|
InvalidSqlSnafu {
|
|
err_msg: "must have one and only one TimeIndex columns",
|
|
}
|
|
);
|
|
Ok(time_index[0].clone())
|
|
}
|
|
|
|
fn columns_to_expr(
|
|
column_defs: &[SqlColumn],
|
|
time_index: &str,
|
|
primary_keys: &[String],
|
|
timezone: Option<&Timezone>,
|
|
) -> Result<Vec<api::v1::ColumnDef>> {
|
|
let column_schemas = columns_to_column_schemas(column_defs, time_index, timezone)?;
|
|
column_schemas_to_defs(column_schemas, primary_keys)
|
|
}
|
|
|
|
fn columns_to_column_schemas(
|
|
columns: &[SqlColumn],
|
|
time_index: &str,
|
|
timezone: Option<&Timezone>,
|
|
) -> Result<Vec<ColumnSchema>> {
|
|
columns
|
|
.iter()
|
|
.map(|c| column_to_schema(c, time_index, timezone).context(ParseSqlSnafu))
|
|
.collect::<Result<Vec<ColumnSchema>>>()
|
|
}
|
|
|
|
// TODO(weny): refactor this function to use `try_as_column_def`
|
|
pub fn column_schemas_to_defs(
|
|
column_schemas: Vec<ColumnSchema>,
|
|
primary_keys: &[String],
|
|
) -> Result<Vec<api::v1::ColumnDef>> {
|
|
let column_datatypes: Vec<(ColumnDataType, Option<ColumnDataTypeExtension>)> = column_schemas
|
|
.iter()
|
|
.map(|c| {
|
|
ColumnDataTypeWrapper::try_from(c.data_type.clone())
|
|
.map(|w| w.to_parts())
|
|
.context(ColumnDataTypeSnafu)
|
|
})
|
|
.collect::<Result<Vec<_>>>()?;
|
|
|
|
column_schemas
|
|
.iter()
|
|
.zip(column_datatypes)
|
|
.map(|(schema, datatype)| {
|
|
let semantic_type = if schema.is_time_index() {
|
|
SemanticType::Timestamp
|
|
} else if primary_keys.contains(&schema.name) {
|
|
SemanticType::Tag
|
|
} else {
|
|
SemanticType::Field
|
|
} as i32;
|
|
let comment = schema
|
|
.metadata()
|
|
.get(COMMENT_KEY)
|
|
.cloned()
|
|
.unwrap_or_default();
|
|
|
|
Ok(api::v1::ColumnDef {
|
|
name: schema.name.clone(),
|
|
data_type: datatype.0 as i32,
|
|
is_nullable: schema.is_nullable(),
|
|
default_constraint: match schema.default_constraint() {
|
|
None => vec![],
|
|
Some(v) => {
|
|
v.clone()
|
|
.try_into()
|
|
.context(ConvertColumnDefaultConstraintSnafu {
|
|
column_name: &schema.name,
|
|
})?
|
|
}
|
|
},
|
|
semantic_type,
|
|
comment,
|
|
datatype_extension: datatype.1,
|
|
options: options_from_column_schema(schema),
|
|
})
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct RepartitionRequest {
|
|
pub catalog_name: String,
|
|
pub schema_name: String,
|
|
pub table_name: String,
|
|
pub from_exprs: Vec<Expr>,
|
|
pub into_exprs: Vec<Expr>,
|
|
}
|
|
|
|
pub(crate) fn to_repartition_request(
|
|
alter_table: AlterTable,
|
|
query_ctx: &QueryContextRef,
|
|
) -> Result<RepartitionRequest> {
|
|
let (catalog_name, schema_name, table_name) =
|
|
table_idents_to_full_name(alter_table.table_name(), query_ctx)
|
|
.map_err(BoxedError::new)
|
|
.context(ExternalSnafu)?;
|
|
|
|
let AlterTableOperation::Repartition { operation } = alter_table.alter_operation else {
|
|
return InvalidSqlSnafu {
|
|
err_msg: "expected REPARTITION operation",
|
|
}
|
|
.fail();
|
|
};
|
|
|
|
Ok(RepartitionRequest {
|
|
catalog_name,
|
|
schema_name,
|
|
table_name,
|
|
from_exprs: operation.from_exprs,
|
|
into_exprs: operation.into_exprs,
|
|
})
|
|
}
|
|
|
|
/// Converts a SQL alter table statement into a gRPC alter table expression.
|
|
pub(crate) fn to_alter_table_expr(
|
|
alter_table: AlterTable,
|
|
query_ctx: &QueryContextRef,
|
|
) -> Result<AlterTableExpr> {
|
|
let (catalog_name, schema_name, table_name) =
|
|
table_idents_to_full_name(alter_table.table_name(), query_ctx)
|
|
.map_err(BoxedError::new)
|
|
.context(ExternalSnafu)?;
|
|
|
|
let kind = match alter_table.alter_operation {
|
|
AlterTableOperation::AddConstraint(_) => {
|
|
return NotSupportedSnafu {
|
|
feat: "ADD CONSTRAINT",
|
|
}
|
|
.fail();
|
|
}
|
|
AlterTableOperation::AddColumns { add_columns } => AlterTableKind::AddColumns(AddColumns {
|
|
add_columns: add_columns
|
|
.into_iter()
|
|
.map(|add_column| {
|
|
let column_def = sql_column_def_to_grpc_column_def(
|
|
&add_column.column_def,
|
|
Some(&query_ctx.timezone()),
|
|
)
|
|
.map_err(BoxedError::new)
|
|
.context(ExternalSnafu)?;
|
|
if is_interval_type(&column_def.data_type()) {
|
|
return NotSupportedSnafu {
|
|
feat: "Add column with interval type",
|
|
}
|
|
.fail();
|
|
}
|
|
Ok(AddColumn {
|
|
column_def: Some(column_def),
|
|
location: add_column.location.as_ref().map(From::from),
|
|
add_if_not_exists: add_column.add_if_not_exists,
|
|
})
|
|
})
|
|
.collect::<Result<Vec<AddColumn>>>()?,
|
|
}),
|
|
AlterTableOperation::ModifyColumnType {
|
|
column_name,
|
|
target_type,
|
|
} => {
|
|
let target_type =
|
|
sql_data_type_to_concrete_data_type(&target_type, &Default::default())
|
|
.context(ParseSqlSnafu)?;
|
|
let (target_type, target_type_extension) = ColumnDataTypeWrapper::try_from(target_type)
|
|
.map(|w| w.to_parts())
|
|
.context(ColumnDataTypeSnafu)?;
|
|
if is_interval_type(&target_type) {
|
|
return NotSupportedSnafu {
|
|
feat: "Modify column type to interval type",
|
|
}
|
|
.fail();
|
|
}
|
|
AlterTableKind::ModifyColumnTypes(ModifyColumnTypes {
|
|
modify_column_types: vec![ModifyColumnType {
|
|
column_name: column_name.value,
|
|
target_type: target_type as i32,
|
|
target_type_extension,
|
|
}],
|
|
})
|
|
}
|
|
AlterTableOperation::DropColumn { name } => AlterTableKind::DropColumns(DropColumns {
|
|
drop_columns: vec![DropColumn {
|
|
name: name.value.clone(),
|
|
}],
|
|
}),
|
|
AlterTableOperation::RenameTable { new_table_name } => {
|
|
AlterTableKind::RenameTable(RenameTable {
|
|
new_table_name: new_table_name.clone(),
|
|
})
|
|
}
|
|
AlterTableOperation::SetTableOptions { options } => {
|
|
AlterTableKind::SetTableOptions(SetTableOptions {
|
|
table_options: options.into_iter().map(Into::into).collect(),
|
|
})
|
|
}
|
|
AlterTableOperation::UnsetTableOptions { keys } => {
|
|
AlterTableKind::UnsetTableOptions(UnsetTableOptions { keys })
|
|
}
|
|
AlterTableOperation::Repartition { .. } => {
|
|
return NotSupportedSnafu {
|
|
feat: "ALTER TABLE ... REPARTITION",
|
|
}
|
|
.fail();
|
|
}
|
|
AlterTableOperation::SetIndex { options } => {
|
|
let option = match options {
|
|
sql::statements::alter::SetIndexOperation::Fulltext {
|
|
column_name,
|
|
options,
|
|
} => SetIndex {
|
|
options: Some(set_index::Options::Fulltext(SetFulltext {
|
|
column_name: column_name.value,
|
|
enable: options.enable,
|
|
analyzer: match options.analyzer {
|
|
FulltextAnalyzer::English => Analyzer::English.into(),
|
|
FulltextAnalyzer::Chinese => Analyzer::Chinese.into(),
|
|
},
|
|
case_sensitive: options.case_sensitive,
|
|
backend: match options.backend {
|
|
FulltextBackend::Bloom => PbFulltextBackend::Bloom.into(),
|
|
FulltextBackend::Tantivy => PbFulltextBackend::Tantivy.into(),
|
|
},
|
|
granularity: options.granularity as u64,
|
|
false_positive_rate: options.false_positive_rate(),
|
|
})),
|
|
},
|
|
sql::statements::alter::SetIndexOperation::Inverted { column_name } => SetIndex {
|
|
options: Some(set_index::Options::Inverted(SetInverted {
|
|
column_name: column_name.value,
|
|
})),
|
|
},
|
|
sql::statements::alter::SetIndexOperation::Skipping {
|
|
column_name,
|
|
options,
|
|
} => SetIndex {
|
|
options: Some(set_index::Options::Skipping(SetSkipping {
|
|
column_name: column_name.value,
|
|
enable: true,
|
|
granularity: options.granularity as u64,
|
|
false_positive_rate: options.false_positive_rate(),
|
|
skipping_index_type: match options.index_type {
|
|
SkippingIndexType::BloomFilter => {
|
|
PbSkippingIndexType::BloomFilter.into()
|
|
}
|
|
},
|
|
})),
|
|
},
|
|
};
|
|
AlterTableKind::SetIndexes(SetIndexes {
|
|
set_indexes: vec![option],
|
|
})
|
|
}
|
|
AlterTableOperation::UnsetIndex { options } => {
|
|
let option = match options {
|
|
sql::statements::alter::UnsetIndexOperation::Fulltext { column_name } => {
|
|
UnsetIndex {
|
|
options: Some(unset_index::Options::Fulltext(UnsetFulltext {
|
|
column_name: column_name.value,
|
|
})),
|
|
}
|
|
}
|
|
sql::statements::alter::UnsetIndexOperation::Inverted { column_name } => {
|
|
UnsetIndex {
|
|
options: Some(unset_index::Options::Inverted(UnsetInverted {
|
|
column_name: column_name.value,
|
|
})),
|
|
}
|
|
}
|
|
sql::statements::alter::UnsetIndexOperation::Skipping { column_name } => {
|
|
UnsetIndex {
|
|
options: Some(unset_index::Options::Skipping(UnsetSkipping {
|
|
column_name: column_name.value,
|
|
})),
|
|
}
|
|
}
|
|
};
|
|
|
|
AlterTableKind::UnsetIndexes(UnsetIndexes {
|
|
unset_indexes: vec![option],
|
|
})
|
|
}
|
|
AlterTableOperation::DropDefaults { columns } => {
|
|
AlterTableKind::DropDefaults(DropDefaults {
|
|
drop_defaults: columns
|
|
.into_iter()
|
|
.map(|col| {
|
|
let column_name = col.0.to_string();
|
|
Ok(api::v1::DropDefault { column_name })
|
|
})
|
|
.collect::<Result<Vec<_>>>()?,
|
|
})
|
|
}
|
|
AlterTableOperation::SetDefaults { defaults } => AlterTableKind::SetDefaults(SetDefaults {
|
|
set_defaults: defaults
|
|
.into_iter()
|
|
.map(|col| {
|
|
let column_name = col.column_name.to_string();
|
|
let default_constraint = serde_json::to_string(&col.default_constraint)
|
|
.context(EncodeJsonSnafu)?
|
|
.into_bytes();
|
|
Ok(api::v1::SetDefault {
|
|
column_name,
|
|
default_constraint,
|
|
})
|
|
})
|
|
.collect::<Result<Vec<_>>>()?,
|
|
}),
|
|
};
|
|
|
|
Ok(AlterTableExpr {
|
|
catalog_name,
|
|
schema_name,
|
|
table_name,
|
|
kind: Some(kind),
|
|
})
|
|
}
|
|
|
|
/// Try to cast the `[AlterDatabase]` statement into gRPC `[AlterDatabaseExpr]`.
|
|
pub fn to_alter_database_expr(
|
|
alter_database: AlterDatabase,
|
|
query_ctx: &QueryContextRef,
|
|
) -> Result<AlterDatabaseExpr> {
|
|
let catalog = query_ctx.current_catalog();
|
|
let schema = alter_database.database_name;
|
|
|
|
let kind = match alter_database.alter_operation {
|
|
AlterDatabaseOperation::SetDatabaseOption { options } => {
|
|
let options = options.into_iter().map(Into::into).collect();
|
|
AlterDatabaseKind::SetDatabaseOptions(SetDatabaseOptions {
|
|
set_database_options: options,
|
|
})
|
|
}
|
|
AlterDatabaseOperation::UnsetDatabaseOption { keys } => {
|
|
AlterDatabaseKind::UnsetDatabaseOptions(UnsetDatabaseOptions { keys })
|
|
}
|
|
};
|
|
|
|
Ok(AlterDatabaseExpr {
|
|
catalog_name: catalog.to_string(),
|
|
schema_name: schema.to_string(),
|
|
kind: Some(kind),
|
|
})
|
|
}
|
|
|
|
/// Try to cast the `[CreateViewExpr]` statement into gRPC `[CreateViewExpr]`.
|
|
pub fn to_create_view_expr(
|
|
stmt: CreateView,
|
|
logical_plan: Vec<u8>,
|
|
table_names: Vec<TableName>,
|
|
columns: Vec<String>,
|
|
plan_columns: Vec<String>,
|
|
definition: String,
|
|
query_ctx: QueryContextRef,
|
|
) -> Result<CreateViewExpr> {
|
|
let (catalog_name, schema_name, view_name) = table_idents_to_full_name(&stmt.name, &query_ctx)
|
|
.map_err(BoxedError::new)
|
|
.context(ExternalSnafu)?;
|
|
|
|
let expr = CreateViewExpr {
|
|
catalog_name,
|
|
schema_name,
|
|
view_name,
|
|
logical_plan,
|
|
create_if_not_exists: stmt.if_not_exists,
|
|
or_replace: stmt.or_replace,
|
|
table_names,
|
|
columns,
|
|
plan_columns,
|
|
definition,
|
|
};
|
|
|
|
Ok(expr)
|
|
}
|
|
|
|
pub fn to_create_flow_task_expr(
|
|
create_flow: CreateFlow,
|
|
query_ctx: &QueryContextRef,
|
|
) -> Result<CreateFlowExpr> {
|
|
// retrieve sink table name
|
|
let sink_table_ref = object_name_to_table_reference(create_flow.sink_table_name.clone(), true)
|
|
.with_context(|_| ConvertIdentifierSnafu {
|
|
ident: create_flow.sink_table_name.to_string(),
|
|
})?;
|
|
let catalog = sink_table_ref
|
|
.catalog()
|
|
.unwrap_or(query_ctx.current_catalog())
|
|
.to_string();
|
|
let schema = sink_table_ref
|
|
.schema()
|
|
.map(|s| s.to_owned())
|
|
.unwrap_or(query_ctx.current_schema());
|
|
|
|
let sink_table_name = TableName {
|
|
catalog_name: catalog,
|
|
schema_name: schema,
|
|
table_name: sink_table_ref.table().to_string(),
|
|
};
|
|
|
|
let source_table_names = extract_tables_from_query(&create_flow.query)
|
|
.map(|name| {
|
|
let reference =
|
|
object_name_to_table_reference(name.clone(), true).with_context(|_| {
|
|
ConvertIdentifierSnafu {
|
|
ident: name.to_string(),
|
|
}
|
|
})?;
|
|
let catalog = reference
|
|
.catalog()
|
|
.unwrap_or(query_ctx.current_catalog())
|
|
.to_string();
|
|
let schema = reference
|
|
.schema()
|
|
.map(|s| s.to_string())
|
|
.unwrap_or(query_ctx.current_schema());
|
|
|
|
let table_name = TableName {
|
|
catalog_name: catalog,
|
|
schema_name: schema,
|
|
table_name: reference.table().to_string(),
|
|
};
|
|
Ok(table_name)
|
|
})
|
|
.collect::<Result<Vec<_>>>()?;
|
|
|
|
let eval_interval = create_flow.eval_interval;
|
|
|
|
Ok(CreateFlowExpr {
|
|
catalog_name: query_ctx.current_catalog().to_string(),
|
|
flow_name: sanitize_flow_name(create_flow.flow_name)?,
|
|
source_table_names,
|
|
sink_table_name: Some(sink_table_name),
|
|
or_replace: create_flow.or_replace,
|
|
create_if_not_exists: create_flow.if_not_exists,
|
|
expire_after: create_flow.expire_after.map(|value| ExpireAfter { value }),
|
|
eval_interval: eval_interval.map(|seconds| api::v1::EvalInterval { seconds }),
|
|
comment: create_flow.comment.unwrap_or_default(),
|
|
sql: create_flow.query.to_string(),
|
|
flow_options: Default::default(),
|
|
})
|
|
}
|
|
|
|
/// sanitize the flow name, remove possible quotes
|
|
fn sanitize_flow_name(mut flow_name: ObjectName) -> Result<String> {
|
|
ensure!(
|
|
flow_name.0.len() == 1,
|
|
InvalidFlowNameSnafu {
|
|
name: flow_name.to_string(),
|
|
}
|
|
);
|
|
// safety: we've checked flow_name.0 has exactly one element.
|
|
Ok(flow_name.0.swap_remove(0).to_string_unquoted())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use api::v1::{SetDatabaseOptions, UnsetDatabaseOptions};
|
|
use datatypes::value::Value;
|
|
use session::context::{QueryContext, QueryContextBuilder};
|
|
use sql::dialect::GreptimeDbDialect;
|
|
use sql::parser::{ParseOptions, ParserContext};
|
|
use sql::statements::statement::Statement;
|
|
use store_api::storage::ColumnDefaultConstraint;
|
|
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_create_flow_tql_expr() {
|
|
let sql = r#"
|
|
CREATE FLOW calc_reqs SINK TO cnt_reqs AS
|
|
TQL EVAL (0, 15, '5s') count_values("status_code", http_requests);"#;
|
|
let stmt =
|
|
ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default());
|
|
|
|
assert!(
|
|
stmt.is_err(),
|
|
"Expected error for invalid TQL EVAL parameters: {:#?}",
|
|
stmt
|
|
);
|
|
|
|
let sql = r#"
|
|
CREATE FLOW calc_reqs SINK TO cnt_reqs AS
|
|
TQL EVAL (now() - '15s'::interval, now(), '5s') count_values("status_code", http_requests);"#;
|
|
let stmt =
|
|
ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
|
|
.unwrap()
|
|
.pop()
|
|
.unwrap();
|
|
|
|
let Statement::CreateFlow(create_flow) = stmt else {
|
|
unreachable!()
|
|
};
|
|
let expr = to_create_flow_task_expr(create_flow, &QueryContext::arc()).unwrap();
|
|
|
|
let to_dot_sep =
|
|
|c: TableName| format!("{}.{}.{}", c.catalog_name, c.schema_name, c.table_name);
|
|
assert_eq!("calc_reqs", expr.flow_name);
|
|
assert_eq!("greptime", expr.catalog_name);
|
|
assert_eq!(
|
|
"greptime.public.cnt_reqs",
|
|
expr.sink_table_name.map(to_dot_sep).unwrap()
|
|
);
|
|
assert!(expr.source_table_names.is_empty());
|
|
assert_eq!(
|
|
r#"TQL EVAL (now() - '15s'::interval, now(), '5s') count_values("status_code", http_requests)"#,
|
|
expr.sql
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_create_flow_expr() {
|
|
let sql = r"
|
|
CREATE FLOW test_distinct_basic SINK TO out_distinct_basic AS
|
|
SELECT
|
|
DISTINCT number as dis
|
|
FROM
|
|
distinct_basic;";
|
|
let stmt =
|
|
ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
|
|
.unwrap()
|
|
.pop()
|
|
.unwrap();
|
|
|
|
let Statement::CreateFlow(create_flow) = stmt else {
|
|
unreachable!()
|
|
};
|
|
let expr = to_create_flow_task_expr(create_flow, &QueryContext::arc()).unwrap();
|
|
|
|
let to_dot_sep =
|
|
|c: TableName| format!("{}.{}.{}", c.catalog_name, c.schema_name, c.table_name);
|
|
assert_eq!("test_distinct_basic", expr.flow_name);
|
|
assert_eq!("greptime", expr.catalog_name);
|
|
assert_eq!(
|
|
"greptime.public.out_distinct_basic",
|
|
expr.sink_table_name.map(to_dot_sep).unwrap()
|
|
);
|
|
assert_eq!(1, expr.source_table_names.len());
|
|
assert_eq!(
|
|
"greptime.public.distinct_basic",
|
|
to_dot_sep(expr.source_table_names[0].clone())
|
|
);
|
|
assert_eq!(
|
|
r"SELECT
|
|
DISTINCT number as dis
|
|
FROM
|
|
distinct_basic",
|
|
expr.sql
|
|
);
|
|
|
|
let sql = r"
|
|
CREATE FLOW `task_2`
|
|
SINK TO schema_1.table_1
|
|
AS
|
|
SELECT max(c1), min(c2) FROM schema_2.table_2;";
|
|
let stmt =
|
|
ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
|
|
.unwrap()
|
|
.pop()
|
|
.unwrap();
|
|
|
|
let Statement::CreateFlow(create_flow) = stmt else {
|
|
unreachable!()
|
|
};
|
|
let expr = to_create_flow_task_expr(create_flow, &QueryContext::arc()).unwrap();
|
|
|
|
let to_dot_sep =
|
|
|c: TableName| format!("{}.{}.{}", c.catalog_name, c.schema_name, c.table_name);
|
|
assert_eq!("task_2", expr.flow_name);
|
|
assert_eq!("greptime", expr.catalog_name);
|
|
assert_eq!(
|
|
"greptime.schema_1.table_1",
|
|
expr.sink_table_name.map(to_dot_sep).unwrap()
|
|
);
|
|
assert_eq!(1, expr.source_table_names.len());
|
|
assert_eq!(
|
|
"greptime.schema_2.table_2",
|
|
to_dot_sep(expr.source_table_names[0].clone())
|
|
);
|
|
assert_eq!("SELECT max(c1), min(c2) FROM schema_2.table_2", expr.sql);
|
|
|
|
let sql = r"
|
|
CREATE FLOW abc.`task_2`
|
|
SINK TO schema_1.table_1
|
|
AS
|
|
SELECT max(c1), min(c2) FROM schema_2.table_2;";
|
|
let stmt =
|
|
ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
|
|
.unwrap()
|
|
.pop()
|
|
.unwrap();
|
|
|
|
let Statement::CreateFlow(create_flow) = stmt else {
|
|
unreachable!()
|
|
};
|
|
let res = to_create_flow_task_expr(create_flow, &QueryContext::arc());
|
|
|
|
assert!(res.is_err());
|
|
assert!(
|
|
res.unwrap_err()
|
|
.to_string()
|
|
.contains("Invalid flow name: abc.`task_2`")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_create_to_expr() {
|
|
let sql = "CREATE TABLE monitor (host STRING,ts TIMESTAMP,TIME INDEX (ts),PRIMARY KEY(host)) ENGINE=mito WITH(ttl='3days', write_buffer_size='1024KB');";
|
|
let stmt =
|
|
ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
|
|
.unwrap()
|
|
.pop()
|
|
.unwrap();
|
|
|
|
let Statement::CreateTable(create_table) = stmt else {
|
|
unreachable!()
|
|
};
|
|
let expr = create_to_expr(&create_table, &QueryContext::arc()).unwrap();
|
|
assert_eq!("3days", expr.table_options.get("ttl").unwrap());
|
|
assert_eq!(
|
|
"1.0MiB",
|
|
expr.table_options.get("write_buffer_size").unwrap()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_invalid_create_to_expr() {
|
|
let cases = [
|
|
// duplicate column declaration
|
|
"CREATE TABLE monitor (host STRING primary key, ts TIMESTAMP TIME INDEX, some_column text, some_column string);",
|
|
// duplicate primary key
|
|
"CREATE TABLE monitor (host STRING, ts TIMESTAMP TIME INDEX, some_column STRING, PRIMARY KEY (some_column, host, some_column));",
|
|
// time index is primary key
|
|
"CREATE TABLE monitor (host STRING, ts TIMESTAMP TIME INDEX, PRIMARY KEY (host, ts));",
|
|
];
|
|
|
|
for sql in cases {
|
|
let stmt = ParserContext::create_with_dialect(
|
|
sql,
|
|
&GreptimeDbDialect {},
|
|
ParseOptions::default(),
|
|
)
|
|
.unwrap()
|
|
.pop()
|
|
.unwrap();
|
|
let Statement::CreateTable(create_table) = stmt else {
|
|
unreachable!()
|
|
};
|
|
create_to_expr(&create_table, &QueryContext::arc()).unwrap_err();
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_create_to_expr_with_default_timestamp_value() {
|
|
let sql = "CREATE TABLE monitor (v double,ts TIMESTAMP default '2024-01-30T00:01:01',TIME INDEX (ts)) engine=mito;";
|
|
let stmt =
|
|
ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
|
|
.unwrap()
|
|
.pop()
|
|
.unwrap();
|
|
|
|
let Statement::CreateTable(create_table) = stmt else {
|
|
unreachable!()
|
|
};
|
|
|
|
// query context with system timezone UTC.
|
|
let expr = create_to_expr(&create_table, &QueryContext::arc()).unwrap();
|
|
let ts_column = &expr.column_defs[1];
|
|
let constraint = assert_ts_column(ts_column);
|
|
assert!(
|
|
matches!(constraint, ColumnDefaultConstraint::Value(Value::Timestamp(ts))
|
|
if ts.to_iso8601_string() == "2024-01-30 00:01:01+0000")
|
|
);
|
|
|
|
// query context with timezone `+08:00`
|
|
let ctx = QueryContextBuilder::default()
|
|
.timezone(Timezone::from_tz_string("+08:00").unwrap())
|
|
.build()
|
|
.into();
|
|
let expr = create_to_expr(&create_table, &ctx).unwrap();
|
|
let ts_column = &expr.column_defs[1];
|
|
let constraint = assert_ts_column(ts_column);
|
|
assert!(
|
|
matches!(constraint, ColumnDefaultConstraint::Value(Value::Timestamp(ts))
|
|
if ts.to_iso8601_string() == "2024-01-29 16:01:01+0000")
|
|
);
|
|
}
|
|
|
|
fn assert_ts_column(ts_column: &api::v1::ColumnDef) -> ColumnDefaultConstraint {
|
|
assert_eq!("ts", ts_column.name);
|
|
assert_eq!(
|
|
ColumnDataType::TimestampMillisecond as i32,
|
|
ts_column.data_type
|
|
);
|
|
assert!(!ts_column.default_constraint.is_empty());
|
|
|
|
ColumnDefaultConstraint::try_from(&ts_column.default_constraint[..]).unwrap()
|
|
}
|
|
|
|
#[test]
|
|
fn test_to_alter_expr() {
|
|
let sql = "ALTER DATABASE greptime SET key1='value1', key2='value2';";
|
|
let stmt =
|
|
ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
|
|
.unwrap()
|
|
.pop()
|
|
.unwrap();
|
|
|
|
let Statement::AlterDatabase(alter_database) = stmt else {
|
|
unreachable!()
|
|
};
|
|
|
|
let expr = to_alter_database_expr(alter_database, &QueryContext::arc()).unwrap();
|
|
let kind = expr.kind.unwrap();
|
|
|
|
let AlterDatabaseKind::SetDatabaseOptions(SetDatabaseOptions {
|
|
set_database_options,
|
|
}) = kind
|
|
else {
|
|
unreachable!()
|
|
};
|
|
|
|
assert_eq!(2, set_database_options.len());
|
|
assert_eq!("key1", set_database_options[0].key);
|
|
assert_eq!("value1", set_database_options[0].value);
|
|
assert_eq!("key2", set_database_options[1].key);
|
|
assert_eq!("value2", set_database_options[1].value);
|
|
|
|
let sql = "ALTER DATABASE greptime UNSET key1, key2;";
|
|
let stmt =
|
|
ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
|
|
.unwrap()
|
|
.pop()
|
|
.unwrap();
|
|
|
|
let Statement::AlterDatabase(alter_database) = stmt else {
|
|
unreachable!()
|
|
};
|
|
|
|
let expr = to_alter_database_expr(alter_database, &QueryContext::arc()).unwrap();
|
|
let kind = expr.kind.unwrap();
|
|
|
|
let AlterDatabaseKind::UnsetDatabaseOptions(UnsetDatabaseOptions { keys }) = kind else {
|
|
unreachable!()
|
|
};
|
|
|
|
assert_eq!(2, keys.len());
|
|
assert!(keys.contains(&"key1".to_string()));
|
|
assert!(keys.contains(&"key2".to_string()));
|
|
|
|
let sql = "ALTER TABLE monitor add column ts TIMESTAMP default '2024-01-30T00:01:01';";
|
|
let stmt =
|
|
ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
|
|
.unwrap()
|
|
.pop()
|
|
.unwrap();
|
|
|
|
let Statement::AlterTable(alter_table) = stmt else {
|
|
unreachable!()
|
|
};
|
|
|
|
// query context with system timezone UTC.
|
|
let expr = to_alter_table_expr(alter_table.clone(), &QueryContext::arc()).unwrap();
|
|
let kind = expr.kind.unwrap();
|
|
|
|
let AlterTableKind::AddColumns(AddColumns { add_columns, .. }) = kind else {
|
|
unreachable!()
|
|
};
|
|
|
|
assert_eq!(1, add_columns.len());
|
|
let ts_column = add_columns[0].column_def.clone().unwrap();
|
|
let constraint = assert_ts_column(&ts_column);
|
|
assert!(
|
|
matches!(constraint, ColumnDefaultConstraint::Value(Value::Timestamp(ts))
|
|
if ts.to_iso8601_string() == "2024-01-30 00:01:01+0000")
|
|
);
|
|
|
|
//
|
|
// query context with timezone `+08:00`
|
|
let ctx = QueryContextBuilder::default()
|
|
.timezone(Timezone::from_tz_string("+08:00").unwrap())
|
|
.build()
|
|
.into();
|
|
let expr = to_alter_table_expr(alter_table, &ctx).unwrap();
|
|
let kind = expr.kind.unwrap();
|
|
|
|
let AlterTableKind::AddColumns(AddColumns { add_columns, .. }) = kind else {
|
|
unreachable!()
|
|
};
|
|
|
|
assert_eq!(1, add_columns.len());
|
|
let ts_column = add_columns[0].column_def.clone().unwrap();
|
|
let constraint = assert_ts_column(&ts_column);
|
|
assert!(
|
|
matches!(constraint, ColumnDefaultConstraint::Value(Value::Timestamp(ts))
|
|
if ts.to_iso8601_string() == "2024-01-29 16:01:01+0000")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_to_alter_modify_column_type_expr() {
|
|
let sql = "ALTER TABLE monitor MODIFY COLUMN mem_usage STRING;";
|
|
let stmt =
|
|
ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
|
|
.unwrap()
|
|
.pop()
|
|
.unwrap();
|
|
|
|
let Statement::AlterTable(alter_table) = stmt else {
|
|
unreachable!()
|
|
};
|
|
|
|
// query context with system timezone UTC.
|
|
let expr = to_alter_table_expr(alter_table.clone(), &QueryContext::arc()).unwrap();
|
|
let kind = expr.kind.unwrap();
|
|
|
|
let AlterTableKind::ModifyColumnTypes(ModifyColumnTypes {
|
|
modify_column_types,
|
|
}) = kind
|
|
else {
|
|
unreachable!()
|
|
};
|
|
|
|
assert_eq!(1, modify_column_types.len());
|
|
let modify_column_type = &modify_column_types[0];
|
|
|
|
assert_eq!("mem_usage", modify_column_type.column_name);
|
|
assert_eq!(
|
|
ColumnDataType::String as i32,
|
|
modify_column_type.target_type
|
|
);
|
|
assert!(modify_column_type.target_type_extension.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn test_to_repartition_request() {
|
|
let sql = r#"
|
|
ALTER TABLE metrics REPARTITION (
|
|
device_id < 100
|
|
) INTO (
|
|
device_id < 100 AND area < 'South',
|
|
device_id < 100 AND area >= 'South'
|
|
);"#;
|
|
let stmt =
|
|
ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
|
|
.unwrap()
|
|
.pop()
|
|
.unwrap();
|
|
|
|
let Statement::AlterTable(alter_table) = stmt else {
|
|
unreachable!()
|
|
};
|
|
|
|
let request = to_repartition_request(alter_table, &QueryContext::arc()).unwrap();
|
|
assert_eq!("greptime", request.catalog_name);
|
|
assert_eq!("public", request.schema_name);
|
|
assert_eq!("metrics", request.table_name);
|
|
assert_eq!(
|
|
request
|
|
.from_exprs
|
|
.into_iter()
|
|
.map(|x| x.to_string())
|
|
.collect::<Vec<_>>(),
|
|
vec!["device_id < 100".to_string()]
|
|
);
|
|
assert_eq!(
|
|
request
|
|
.into_exprs
|
|
.into_iter()
|
|
.map(|x| x.to_string())
|
|
.collect::<Vec<_>>(),
|
|
vec![
|
|
"device_id < 100 AND area < 'South'".to_string(),
|
|
"device_id < 100 AND area >= 'South'".to_string()
|
|
]
|
|
);
|
|
}
|
|
|
|
fn new_test_table_names() -> Vec<TableName> {
|
|
vec![
|
|
TableName {
|
|
catalog_name: "greptime".to_string(),
|
|
schema_name: "public".to_string(),
|
|
table_name: "a_table".to_string(),
|
|
},
|
|
TableName {
|
|
catalog_name: "greptime".to_string(),
|
|
schema_name: "public".to_string(),
|
|
table_name: "b_table".to_string(),
|
|
},
|
|
]
|
|
}
|
|
|
|
#[test]
|
|
fn test_to_create_view_expr() {
|
|
let sql = "CREATE VIEW test AS SELECT * FROM NUMBERS";
|
|
let stmt =
|
|
ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
|
|
.unwrap()
|
|
.pop()
|
|
.unwrap();
|
|
|
|
let Statement::CreateView(stmt) = stmt else {
|
|
unreachable!()
|
|
};
|
|
|
|
let logical_plan = vec![1, 2, 3];
|
|
let table_names = new_test_table_names();
|
|
let columns = vec!["a".to_string()];
|
|
let plan_columns = vec!["number".to_string()];
|
|
|
|
let expr = to_create_view_expr(
|
|
stmt,
|
|
logical_plan.clone(),
|
|
table_names.clone(),
|
|
columns.clone(),
|
|
plan_columns.clone(),
|
|
sql.to_string(),
|
|
QueryContext::arc(),
|
|
)
|
|
.unwrap();
|
|
|
|
assert_eq!("greptime", expr.catalog_name);
|
|
assert_eq!("public", expr.schema_name);
|
|
assert_eq!("test", expr.view_name);
|
|
assert!(!expr.create_if_not_exists);
|
|
assert!(!expr.or_replace);
|
|
assert_eq!(logical_plan, expr.logical_plan);
|
|
assert_eq!(table_names, expr.table_names);
|
|
assert_eq!(sql, expr.definition);
|
|
assert_eq!(columns, expr.columns);
|
|
assert_eq!(plan_columns, expr.plan_columns);
|
|
}
|
|
|
|
#[test]
|
|
fn test_to_create_view_expr_complex() {
|
|
let sql = "CREATE OR REPLACE VIEW IF NOT EXISTS test.test_view AS SELECT * FROM NUMBERS";
|
|
let stmt =
|
|
ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
|
|
.unwrap()
|
|
.pop()
|
|
.unwrap();
|
|
|
|
let Statement::CreateView(stmt) = stmt else {
|
|
unreachable!()
|
|
};
|
|
|
|
let logical_plan = vec![1, 2, 3];
|
|
let table_names = new_test_table_names();
|
|
let columns = vec!["a".to_string()];
|
|
let plan_columns = vec!["number".to_string()];
|
|
|
|
let expr = to_create_view_expr(
|
|
stmt,
|
|
logical_plan.clone(),
|
|
table_names.clone(),
|
|
columns.clone(),
|
|
plan_columns.clone(),
|
|
sql.to_string(),
|
|
QueryContext::arc(),
|
|
)
|
|
.unwrap();
|
|
|
|
assert_eq!("greptime", expr.catalog_name);
|
|
assert_eq!("test", expr.schema_name);
|
|
assert_eq!("test_view", expr.view_name);
|
|
assert!(expr.create_if_not_exists);
|
|
assert!(expr.or_replace);
|
|
assert_eq!(logical_plan, expr.logical_plan);
|
|
assert_eq!(table_names, expr.table_names);
|
|
assert_eq!(sql, expr.definition);
|
|
assert_eq!(columns, expr.columns);
|
|
assert_eq!(plan_columns, expr.plan_columns);
|
|
}
|
|
|
|
#[test]
|
|
fn test_expr_to_create() {
|
|
let sql = r#"CREATE TABLE IF NOT EXISTS `tt` (
|
|
`timestamp` TIMESTAMP(9) NOT NULL,
|
|
`ip_address` STRING NULL SKIPPING INDEX WITH(false_positive_rate = '0.01', granularity = '10240', type = 'BLOOM'),
|
|
`username` STRING NULL,
|
|
`http_method` STRING NULL INVERTED INDEX,
|
|
`request_line` STRING NULL FULLTEXT INDEX WITH(analyzer = 'English', backend = 'bloom', case_sensitive = 'false', false_positive_rate = '0.01', granularity = '10240'),
|
|
`protocol` STRING NULL,
|
|
`status_code` INT NULL INVERTED INDEX,
|
|
`response_size` BIGINT NULL,
|
|
`message` STRING NULL,
|
|
TIME INDEX (`timestamp`),
|
|
PRIMARY KEY (`username`, `status_code`)
|
|
)
|
|
ENGINE=mito
|
|
WITH(
|
|
append_mode = 'true'
|
|
)"#;
|
|
let stmt =
|
|
ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
|
|
.unwrap()
|
|
.pop()
|
|
.unwrap();
|
|
|
|
let Statement::CreateTable(original_create) = stmt else {
|
|
unreachable!()
|
|
};
|
|
|
|
// Convert CreateTable -> CreateTableExpr -> CreateTable
|
|
let expr = create_to_expr(&original_create, &QueryContext::arc()).unwrap();
|
|
|
|
let create_table = expr_to_create(&expr, Some('`')).unwrap();
|
|
let new_sql = format!("{:#}", create_table);
|
|
assert_eq!(sql, new_sql);
|
|
}
|
|
}
|