mirror of
https://github.com/GreptimeTeam/greptimedb.git
synced 2026-05-18 14:00:39 +00:00
feat: show create table (#1336)
* temp commit * feat: impl Display for CreateTable statement * feat: impl show create table for standalone * fix: forgot show.rs * feat: clean code * fix: typo * feat: impl show create table for distributed * test: add show create table sqlness test * fix: typo * fix: sqlness tests * feat: render partition rules for distributed table * Update src/sql/src/statements.rs Co-authored-by: Yingwen <realevenyag@gmail.com> * Update src/sql/src/statements.rs Co-authored-by: Yingwen <realevenyag@gmail.com> * Update src/sql/src/statements.rs Co-authored-by: Yingwen <realevenyag@gmail.com> * Update src/sql/src/statements/create.rs Co-authored-by: Yingwen <realevenyag@gmail.com> * chore: by CR comments * fix: compile error * fix: missing column comments and extra table options * test: add show create table test * test: add show create table test * chore: timestamp precision * fix: test --------- Co-authored-by: Yingwen <realevenyag@gmail.com>
This commit is contained in:
@@ -17,12 +17,12 @@ use std::any::Any;
|
||||
use common_error::prelude::*;
|
||||
use common_time::timestamp::TimeUnit;
|
||||
use common_time::Timestamp;
|
||||
use datatypes::prelude::ConcreteDataType;
|
||||
use datatypes::prelude::{ConcreteDataType, Value};
|
||||
use snafu::Location;
|
||||
use sqlparser::parser::ParserError;
|
||||
use sqlparser::tokenizer::TokenizerError;
|
||||
|
||||
use crate::ast::Expr;
|
||||
use crate::ast::{Expr, Value as SqlValue};
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
@@ -145,6 +145,16 @@ pub enum Error {
|
||||
statement: String,
|
||||
location: Location,
|
||||
},
|
||||
|
||||
#[snafu(display("Unable to convert sql value {} to datatype {:?}", value, datatype))]
|
||||
ConvertSqlValue {
|
||||
value: SqlValue,
|
||||
datatype: ConcreteDataType,
|
||||
location: Location,
|
||||
},
|
||||
|
||||
#[snafu(display("Unable to convert value {} to sql value", value))]
|
||||
ConvertValue { value: Value, location: Location },
|
||||
}
|
||||
|
||||
impl ErrorExt for Error {
|
||||
@@ -175,6 +185,7 @@ impl ErrorExt for Error {
|
||||
SerializeColumnDefaultConstraint { source, .. } => source.status_code(),
|
||||
ConvertToGrpcDataType { source, .. } => source.status_code(),
|
||||
ConvertToDfStatement { .. } => StatusCode::Internal,
|
||||
ConvertSqlValue { .. } | ConvertValue { .. } => StatusCode::Unsupported,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ use sqlparser::keywords::Keyword;
|
||||
use sqlparser::parser::{Parser, ParserError};
|
||||
use sqlparser::tokenizer::{Token, TokenWithLocation};
|
||||
|
||||
use crate::ast::{Expr, ObjectName};
|
||||
use crate::error::{self, InvalidDatabaseNameSnafu, InvalidTableNameSnafu, Result, SyntaxSnafu};
|
||||
use crate::parsers::tql_parser;
|
||||
use crate::statements::describe::DescribeTable;
|
||||
@@ -64,6 +65,17 @@ impl<'a> ParserContext<'a> {
|
||||
Ok(stmts)
|
||||
}
|
||||
|
||||
pub fn parse_function(sql: &'a str, dialect: &dyn Dialect) -> Result<Expr> {
|
||||
let mut parser = Parser::new(dialect)
|
||||
.try_with_sql(sql)
|
||||
.context(SyntaxSnafu { sql })?;
|
||||
|
||||
let function_name = parser.parse_identifier().context(SyntaxSnafu { sql })?;
|
||||
parser
|
||||
.parse_function(ObjectName(vec![function_name]))
|
||||
.context(SyntaxSnafu { sql })
|
||||
}
|
||||
|
||||
/// Parses parser context to a set of statements.
|
||||
pub fn parse_statement(&mut self) -> Result<Statement> {
|
||||
match self.parser.peek_token().token {
|
||||
@@ -174,9 +186,7 @@ impl<'a> ParserContext<'a> {
|
||||
name: table_name.to_string(),
|
||||
}
|
||||
);
|
||||
Ok(Statement::ShowCreateTable(ShowCreateTable {
|
||||
table_name: table_name.to_string(),
|
||||
}))
|
||||
Ok(Statement::ShowCreateTable(ShowCreateTable { table_name }))
|
||||
}
|
||||
|
||||
fn parse_show_tables(&mut self) -> Result<Statement> {
|
||||
@@ -659,4 +669,11 @@ mod tests {
|
||||
ConcreteDataType::timestamp_millisecond_datatype(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_function() {
|
||||
let expr =
|
||||
ParserContext::parse_function("current_timestamp()", &GenericDialect {}).unwrap();
|
||||
assert!(matches!(expr, Expr::Function(_)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,18 +31,19 @@ use api::helper::ColumnDataTypeWrapper;
|
||||
use common_base::bytes::Bytes;
|
||||
use common_time::Timestamp;
|
||||
use datatypes::prelude::ConcreteDataType;
|
||||
use datatypes::schema::{ColumnDefaultConstraint, ColumnSchema};
|
||||
use datatypes::schema::{ColumnDefaultConstraint, ColumnSchema, COMMENT_KEY};
|
||||
use datatypes::types::TimestampType;
|
||||
use datatypes::value::Value;
|
||||
use snafu::{ensure, OptionExt, ResultExt};
|
||||
|
||||
use crate::ast::{
|
||||
ColumnDef, ColumnOption, ColumnOptionDef, DataType as SqlDataType, Expr, Value as SqlValue,
|
||||
ColumnDef, ColumnOption, ColumnOptionDef, DataType as SqlDataType, Expr, TimezoneInfo,
|
||||
Value as SqlValue,
|
||||
};
|
||||
use crate::error::{
|
||||
self, ColumnTypeMismatchSnafu, ConvertToGrpcDataTypeSnafu, InvalidSqlValueSnafu,
|
||||
ParseSqlValueSnafu, Result, SerializeColumnDefaultConstraintSnafu, TimestampOverflowSnafu,
|
||||
UnsupportedDefaultValueSnafu,
|
||||
self, ColumnTypeMismatchSnafu, ConvertSqlValueSnafu, ConvertToGrpcDataTypeSnafu,
|
||||
ConvertValueSnafu, InvalidSqlValueSnafu, ParseSqlValueSnafu, Result,
|
||||
SerializeColumnDefaultConstraintSnafu, TimestampOverflowSnafu, UnsupportedDefaultValueSnafu,
|
||||
};
|
||||
|
||||
fn parse_string_to_value(
|
||||
@@ -197,7 +198,38 @@ pub fn sql_value_to_value(
|
||||
}
|
||||
SqlValue::HexStringLiteral(s) => parse_hex_string(s)?,
|
||||
SqlValue::Placeholder(s) => return InvalidSqlValueSnafu { value: s }.fail(),
|
||||
_ => todo!("Other sql value"),
|
||||
|
||||
// TODO(dennis): supports binary string
|
||||
_ => {
|
||||
return ConvertSqlValueSnafu {
|
||||
value: sql_val.clone(),
|
||||
datatype: data_type.clone(),
|
||||
}
|
||||
.fail()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn value_to_sql_value(val: &Value) -> Result<SqlValue> {
|
||||
Ok(match val {
|
||||
Value::Int8(v) => SqlValue::Number(v.to_string(), false),
|
||||
Value::UInt8(v) => SqlValue::Number(v.to_string(), false),
|
||||
Value::Int16(v) => SqlValue::Number(v.to_string(), false),
|
||||
Value::UInt16(v) => SqlValue::Number(v.to_string(), false),
|
||||
Value::Int32(v) => SqlValue::Number(v.to_string(), false),
|
||||
Value::UInt32(v) => SqlValue::Number(v.to_string(), false),
|
||||
Value::Int64(v) => SqlValue::Number(v.to_string(), false),
|
||||
Value::UInt64(v) => SqlValue::Number(v.to_string(), false),
|
||||
Value::Float32(v) => SqlValue::Number(v.to_string(), false),
|
||||
Value::Float64(v) => SqlValue::Number(v.to_string(), false),
|
||||
Value::Boolean(b) => SqlValue::Boolean(*b),
|
||||
Value::Date(d) => SqlValue::SingleQuotedString(d.to_string()),
|
||||
Value::DateTime(d) => SqlValue::SingleQuotedString(d.to_string()),
|
||||
Value::Timestamp(ts) => SqlValue::SingleQuotedString(ts.to_iso8601_string()),
|
||||
Value::String(s) => SqlValue::SingleQuotedString(s.as_utf8().to_string()),
|
||||
Value::Null => SqlValue::Null,
|
||||
// TODO(dennis): supports binary
|
||||
_ => return ConvertValueSnafu { value: val.clone() }.fail(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -249,12 +281,26 @@ pub fn column_def_to_schema(column_def: &ColumnDef, is_time_index: bool) -> Resu
|
||||
let default_constraint =
|
||||
parse_column_default_constraint(&name, &data_type, &column_def.options)?;
|
||||
|
||||
ColumnSchema::new(name, data_type, is_nullable)
|
||||
let mut column_schema = ColumnSchema::new(name, data_type, is_nullable)
|
||||
.with_time_index(is_time_index)
|
||||
.with_default_constraint(default_constraint)
|
||||
.context(error::InvalidDefaultSnafu {
|
||||
column: &column_def.name.value,
|
||||
})
|
||||
})?;
|
||||
|
||||
if let Some(ColumnOption::Comment(c)) = column_def.options.iter().find_map(|o| {
|
||||
if matches!(o.option, ColumnOption::Comment(_)) {
|
||||
Some(&o.option)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}) {
|
||||
column_schema
|
||||
.mut_metadata()
|
||||
.insert(COMMENT_KEY.to_string(), c.to_string());
|
||||
}
|
||||
|
||||
Ok(column_schema)
|
||||
}
|
||||
|
||||
/// Convert `ColumnDef` in sqlparser to `ColumnDef` in gRPC proto.
|
||||
@@ -324,6 +370,33 @@ pub fn sql_data_type_to_concrete_data_type(data_type: &SqlDataType) -> Result<Co
|
||||
}
|
||||
}
|
||||
|
||||
pub fn concrete_data_type_to_sql_data_type(data_type: &ConcreteDataType) -> Result<SqlDataType> {
|
||||
match data_type {
|
||||
ConcreteDataType::Int64(_) => Ok(SqlDataType::BigInt(None)),
|
||||
ConcreteDataType::UInt64(_) => Ok(SqlDataType::UnsignedBigInt(None)),
|
||||
ConcreteDataType::Int32(_) => Ok(SqlDataType::Int(None)),
|
||||
ConcreteDataType::UInt32(_) => Ok(SqlDataType::UnsignedInt(None)),
|
||||
ConcreteDataType::Int16(_) => Ok(SqlDataType::SmallInt(None)),
|
||||
ConcreteDataType::UInt16(_) => Ok(SqlDataType::UnsignedSmallInt(None)),
|
||||
ConcreteDataType::Int8(_) => Ok(SqlDataType::TinyInt(None)),
|
||||
ConcreteDataType::UInt8(_) => Ok(SqlDataType::UnsignedTinyInt(None)),
|
||||
ConcreteDataType::String(_) => Ok(SqlDataType::String),
|
||||
ConcreteDataType::Float32(_) => Ok(SqlDataType::Float(None)),
|
||||
ConcreteDataType::Float64(_) => Ok(SqlDataType::Double),
|
||||
ConcreteDataType::Boolean(_) => Ok(SqlDataType::Boolean),
|
||||
ConcreteDataType::Date(_) => Ok(SqlDataType::Date),
|
||||
ConcreteDataType::DateTime(_) => Ok(SqlDataType::Datetime(None)),
|
||||
ConcreteDataType::Timestamp(ts_type) => Ok(SqlDataType::Timestamp(
|
||||
Some(ts_type.precision()),
|
||||
TimezoneInfo::None,
|
||||
)),
|
||||
ConcreteDataType::Binary(_) => Ok(SqlDataType::Varbinary(None)),
|
||||
ConcreteDataType::Null(_) | ConcreteDataType::List(_) | ConcreteDataType::Dictionary(_) => {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::assert_matches::assert_matches;
|
||||
@@ -676,10 +749,16 @@ mod tests {
|
||||
name: "col2".into(),
|
||||
data_type: SqlDataType::String,
|
||||
collation: None,
|
||||
options: vec![ColumnOptionDef {
|
||||
name: None,
|
||||
option: ColumnOption::NotNull,
|
||||
}],
|
||||
options: vec![
|
||||
ColumnOptionDef {
|
||||
name: None,
|
||||
option: ColumnOption::NotNull,
|
||||
},
|
||||
ColumnOptionDef {
|
||||
name: None,
|
||||
option: ColumnOption::Comment("test comment".to_string()),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
let column_schema = column_def_to_schema(&column_def, false).unwrap();
|
||||
@@ -688,6 +767,10 @@ mod tests {
|
||||
assert_eq!(ConcreteDataType::string_datatype(), column_schema.data_type);
|
||||
assert!(!column_schema.is_nullable());
|
||||
assert!(!column_schema.is_time_index());
|
||||
assert_eq!(
|
||||
column_schema.metadata().get(COMMENT_KEY),
|
||||
Some(&"test comment".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -13,12 +13,49 @@
|
||||
// limitations under the License.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
use itertools::Itertools;
|
||||
|
||||
use crate::ast::{ColumnDef, Ident, ObjectName, SqlOption, TableConstraint, Value as SqlValue};
|
||||
|
||||
const LINE_SEP: &str = ",\n";
|
||||
const COMMA_SEP: &str = ", ";
|
||||
const INDENT: usize = 2;
|
||||
|
||||
macro_rules! format_indent {
|
||||
($fmt: expr, $arg: expr) => {
|
||||
format!($fmt, format_args!("{: >1$}", "", INDENT), $arg)
|
||||
};
|
||||
($arg: expr) => {
|
||||
format_indent!("{}{}", $arg)
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! format_list_indent {
|
||||
($list: expr) => {
|
||||
$list.iter().map(|e| format_indent!(e)).join(LINE_SEP)
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! format_list_comma {
|
||||
($list: expr) => {
|
||||
$list.iter().map(|e| format!("{}", e)).join(COMMA_SEP)
|
||||
};
|
||||
}
|
||||
|
||||
/// Time index name, used in table constraints.
|
||||
pub const TIME_INDEX: &str = "__time_index";
|
||||
|
||||
#[inline]
|
||||
pub fn is_time_index(constraint: &TableConstraint) -> bool {
|
||||
matches!(constraint, TableConstraint::Unique {
|
||||
name: Some(name),
|
||||
is_primary: false,
|
||||
..
|
||||
} if name.value == TIME_INDEX)
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub struct CreateTable {
|
||||
/// Create if not exists
|
||||
@@ -34,6 +71,55 @@ pub struct CreateTable {
|
||||
pub partitions: Option<Partitions>,
|
||||
}
|
||||
|
||||
impl CreateTable {
|
||||
fn format_constraints(&self) -> String {
|
||||
self.constraints
|
||||
.iter()
|
||||
.map(|c| {
|
||||
if is_time_index(c) {
|
||||
let TableConstraint::Unique { columns, ..} = c else { unreachable!() };
|
||||
|
||||
format_indent!("{}TIME INDEX ({})", format_list_comma!(columns))
|
||||
} else {
|
||||
format_indent!(c)
|
||||
}
|
||||
})
|
||||
.join(LINE_SEP)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn format_partitions(&self) -> String {
|
||||
if let Some(partitions) = &self.partitions {
|
||||
format!("{}\n", partitions)
|
||||
} else {
|
||||
"".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn format_if_not_exits(&self) -> &str {
|
||||
if self.if_not_exists {
|
||||
"IF NOT EXISTS"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn format_options(&self) -> String {
|
||||
if self.options.is_empty() {
|
||||
"".to_string()
|
||||
} else {
|
||||
let options = format_list_indent!(self.options);
|
||||
format!(
|
||||
r#"WITH(
|
||||
{options}
|
||||
)"#
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub struct Partitions {
|
||||
pub column_list: Vec<Ident>,
|
||||
@@ -46,6 +132,52 @@ pub struct PartitionEntry {
|
||||
pub value_list: Vec<SqlValue>,
|
||||
}
|
||||
|
||||
impl Display for PartitionEntry {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"PARTITION {} VALUES LESS THAN ({})",
|
||||
self.name,
|
||||
format_list_comma!(self.value_list),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Partitions {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
r#"PARTITION BY RANGE COLUMNS ({}) (
|
||||
{}
|
||||
)"#,
|
||||
format_list_comma!(self.column_list),
|
||||
format_list_indent!(self.entries),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for CreateTable {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
let if_not_exists = self.format_if_not_exits();
|
||||
let name = &self.name;
|
||||
let columns = format_list_indent!(self.columns);
|
||||
let constraints = self.format_constraints();
|
||||
let partitions = self.format_partitions();
|
||||
let engine = &self.engine;
|
||||
let options = self.format_options();
|
||||
|
||||
write!(
|
||||
f,
|
||||
r#"CREATE TABLE {if_not_exists} {name} (
|
||||
{columns},
|
||||
{constraints}
|
||||
)
|
||||
{partitions}ENGINE={engine}
|
||||
{options}"#
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub struct CreateDatabase {
|
||||
pub name: ObjectName,
|
||||
@@ -64,3 +196,66 @@ pub struct CreateExternalTable {
|
||||
/// TODO(weny): unify the key's case styling.
|
||||
pub options: HashMap<String, String>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use sqlparser::dialect::GenericDialect;
|
||||
|
||||
use crate::parser::ParserContext;
|
||||
use crate::statements::statement::Statement;
|
||||
|
||||
#[test]
|
||||
fn test_display_create_table() {
|
||||
let sql = r"create table if not exists demo(
|
||||
host string,
|
||||
ts bigint,
|
||||
cpu double default 0,
|
||||
memory double,
|
||||
TIME INDEX (ts),
|
||||
PRIMARY KEY(ts, host)
|
||||
)
|
||||
PARTITION BY RANGE COLUMNS (ts) (
|
||||
PARTITION r0 VALUES LESS THAN (5),
|
||||
PARTITION r1 VALUES LESS THAN (9),
|
||||
PARTITION r2 VALUES LESS THAN (MAXVALUE),
|
||||
)
|
||||
engine=mito
|
||||
with(regions=1, ttl='7d');
|
||||
";
|
||||
let result = ParserContext::create_with_dialect(sql, &GenericDialect {}).unwrap();
|
||||
assert_eq!(1, result.len());
|
||||
|
||||
match &result[0] {
|
||||
Statement::CreateTable(c) => {
|
||||
let new_sql = format!("\n{}", c);
|
||||
assert_eq!(
|
||||
r#"
|
||||
CREATE TABLE IF NOT EXISTS demo (
|
||||
host STRING,
|
||||
ts BIGINT,
|
||||
cpu DOUBLE DEFAULT 0,
|
||||
memory DOUBLE,
|
||||
TIME INDEX (ts),
|
||||
PRIMARY KEY (ts, host)
|
||||
)
|
||||
PARTITION BY RANGE COLUMNS (ts) (
|
||||
PARTITION r0 VALUES LESS THAN (5),
|
||||
PARTITION r1 VALUES LESS THAN (9),
|
||||
PARTITION r2 VALUES LESS THAN (MAXVALUE)
|
||||
)
|
||||
ENGINE=mito
|
||||
WITH(
|
||||
regions = 1,
|
||||
ttl = '7d'
|
||||
)"#,
|
||||
&new_sql
|
||||
);
|
||||
|
||||
let new_result =
|
||||
ParserContext::create_with_dialect(&new_sql, &GenericDialect {}).unwrap();
|
||||
assert_eq!(result, new_result);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
use std::fmt;
|
||||
|
||||
use crate::ast::{Expr, Ident};
|
||||
use crate::ast::{Expr, Ident, ObjectName};
|
||||
|
||||
/// Show kind for SQL expressions like `SHOW DATABASE` or `SHOW TABLE`
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
@@ -57,7 +57,7 @@ pub struct ShowTables {
|
||||
/// SQL structure for `SHOW CREATE TABLE`.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ShowCreateTable {
|
||||
pub table_name: String,
|
||||
pub table_name: ObjectName,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -124,7 +124,7 @@ mod tests {
|
||||
assert_matches!(&stmts[0], Statement::ShowCreateTable { .. });
|
||||
match &stmts[0] {
|
||||
Statement::ShowCreateTable(show) => {
|
||||
let table_name = show.table_name.as_str();
|
||||
let table_name = show.table_name.to_string();
|
||||
assert_eq!(table_name, "test");
|
||||
}
|
||||
_ => {
|
||||
|
||||
Reference in New Issue
Block a user