mirror of
https://github.com/GreptimeTeam/greptimedb.git
synced 2026-06-02 05:10:40 +00:00
feat: show create view and creating view with columns (#4086)
* feat: parse column names when creating view * feat: save the view definition into view info * feat: supports view columns and show create view * feat: save plan columns for validation * fix: typo * chore: comments and style * chore: apply suggestions * test: assert CreateView display result * chore: style Co-authored-by: Weny Xu <wenymedia@gmail.com> * chore: avoid the clone Co-authored-by: Weny Xu <wenymedia@gmail.com> * fix: compile error after rebeasing --------- Co-authored-by: Weny Xu <wenymedia@gmail.com>
This commit is contained in:
@@ -24,7 +24,7 @@ use sqlparser::ast::{ColumnOption, ColumnOptionDef, DataType, Expr, KeyOrIndexDi
|
||||
use sqlparser::dialect::keywords::Keyword;
|
||||
use sqlparser::keywords::ALL_KEYWORDS;
|
||||
use sqlparser::parser::IsOptional::Mandatory;
|
||||
use sqlparser::parser::Parser;
|
||||
use sqlparser::parser::{Parser, ParserError};
|
||||
use sqlparser::tokenizer::{Token, TokenWithLocation, Word};
|
||||
use table::requests::validate_table_option;
|
||||
|
||||
@@ -125,6 +125,8 @@ impl<'a> ParserContext<'a> {
|
||||
let if_not_exists = self.parse_if_not_exist()?;
|
||||
let view_name = self.intern_parse_table_name()?;
|
||||
|
||||
let columns = self.parse_view_columns()?;
|
||||
|
||||
self.parser
|
||||
.expect_keyword(Keyword::AS)
|
||||
.context(SyntaxSnafu)?;
|
||||
@@ -133,12 +135,36 @@ impl<'a> ParserContext<'a> {
|
||||
|
||||
Ok(Statement::CreateView(CreateView {
|
||||
name: view_name,
|
||||
columns,
|
||||
or_replace,
|
||||
query: Box::new(query),
|
||||
if_not_exists,
|
||||
}))
|
||||
}
|
||||
|
||||
fn parse_view_columns(&mut self) -> Result<Vec<Ident>> {
|
||||
let mut columns = vec![];
|
||||
if !self.parser.consume_token(&Token::LParen) || self.parser.consume_token(&Token::RParen) {
|
||||
return Ok(columns);
|
||||
}
|
||||
|
||||
loop {
|
||||
let name = self.parse_column_name().context(SyntaxSnafu)?;
|
||||
|
||||
columns.push(name);
|
||||
|
||||
let comma = self.parser.consume_token(&Token::Comma);
|
||||
if self.parser.consume_token(&Token::RParen) {
|
||||
// allow a trailing comma, even though it's not in standard
|
||||
break;
|
||||
} else if !comma {
|
||||
return self.expected("',' or ')' after column name", self.parser.peek_token());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(columns)
|
||||
}
|
||||
|
||||
fn parse_create_external_table(&mut self) -> Result<Statement> {
|
||||
let _ = self.parser.next_token();
|
||||
self.parser
|
||||
@@ -547,10 +573,26 @@ impl<'a> ParserContext<'a> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Parse the column name and check if it's valid.
|
||||
fn parse_column_name(&mut self) -> std::result::Result<Ident, ParserError> {
|
||||
let name = self.parser.parse_identifier(false)?;
|
||||
if name.quote_style.is_none() &&
|
||||
// "ALL_KEYWORDS" are sorted.
|
||||
ALL_KEYWORDS.binary_search(&name.value.to_uppercase().as_str()).is_ok()
|
||||
{
|
||||
return Err(ParserError::ParserError(format!(
|
||||
"Cannot use keyword '{}' as column name. Hint: add quotes to the name.",
|
||||
&name.value
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(name)
|
||||
}
|
||||
|
||||
pub fn parse_column_def(&mut self) -> Result<Column> {
|
||||
let name = self.parse_column_name().context(SyntaxSnafu)?;
|
||||
let parser = &mut self.parser;
|
||||
|
||||
let name = parser.parse_identifier(false).context(SyntaxSnafu)?;
|
||||
ensure!(
|
||||
!(name.quote_style.is_none() &&
|
||||
// "ALL_KEYWORDS" are sorted.
|
||||
@@ -2009,4 +2051,85 @@ CREATE TABLE log (
|
||||
.to_string()
|
||||
.contains("invalid FULLTEXT option"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_create_view_with_columns() {
|
||||
let sql = "CREATE VIEW test () AS SELECT * FROM NUMBERS";
|
||||
let result =
|
||||
ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
|
||||
.unwrap();
|
||||
|
||||
match &result[0] {
|
||||
Statement::CreateView(c) => {
|
||||
assert_eq!(c.to_string(), "CREATE VIEW test AS SELECT * FROM NUMBERS");
|
||||
assert!(!c.or_replace);
|
||||
assert!(!c.if_not_exists);
|
||||
assert_eq!("test", c.name.to_string());
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
assert_eq!(
|
||||
"CREATE VIEW test AS SELECT * FROM NUMBERS",
|
||||
result[0].to_string()
|
||||
);
|
||||
|
||||
let sql = "CREATE VIEW test (n1) AS SELECT * FROM NUMBERS";
|
||||
let result =
|
||||
ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
|
||||
.unwrap();
|
||||
|
||||
match &result[0] {
|
||||
Statement::CreateView(c) => {
|
||||
assert_eq!(c.to_string(), sql);
|
||||
assert!(!c.or_replace);
|
||||
assert!(!c.if_not_exists);
|
||||
assert_eq!("test", c.name.to_string());
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
assert_eq!(sql, result[0].to_string());
|
||||
|
||||
let sql = "CREATE VIEW test (n1, n2) AS SELECT * FROM NUMBERS";
|
||||
let result =
|
||||
ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
|
||||
.unwrap();
|
||||
|
||||
match &result[0] {
|
||||
Statement::CreateView(c) => {
|
||||
assert_eq!(c.to_string(), sql);
|
||||
assert!(!c.or_replace);
|
||||
assert!(!c.if_not_exists);
|
||||
assert_eq!("test", c.name.to_string());
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
assert_eq!(sql, result[0].to_string());
|
||||
|
||||
// Some invalid syntax cases
|
||||
let sql = "CREATE VIEW test (n1 AS select * from demo";
|
||||
let result =
|
||||
ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default());
|
||||
assert!(result.is_err());
|
||||
|
||||
let sql = "CREATE VIEW test (n1, AS select * from demo";
|
||||
let result =
|
||||
ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default());
|
||||
assert!(result.is_err());
|
||||
|
||||
let sql = "CREATE VIEW test n1,n2) AS select * from demo";
|
||||
let result =
|
||||
ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default());
|
||||
assert!(result.is_err());
|
||||
|
||||
let sql = "CREATE VIEW test (1) AS select * from demo";
|
||||
let result =
|
||||
ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default());
|
||||
assert!(result.is_err());
|
||||
|
||||
// keyword
|
||||
let sql = "CREATE VIEW test (n1, select) AS select * from demo";
|
||||
let result =
|
||||
ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default());
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,8 +21,8 @@ use crate::error::{
|
||||
};
|
||||
use crate::parser::ParserContext;
|
||||
use crate::statements::show::{
|
||||
ShowColumns, ShowCreateFlow, ShowCreateTable, ShowDatabases, ShowIndex, ShowKind, ShowStatus,
|
||||
ShowTableStatus, ShowTables, ShowVariables,
|
||||
ShowColumns, ShowCreateFlow, ShowCreateTable, ShowCreateView, ShowDatabases, ShowIndex,
|
||||
ShowKind, ShowStatus, ShowTableStatus, ShowTables, ShowVariables,
|
||||
};
|
||||
use crate::statements::statement::Statement;
|
||||
|
||||
@@ -74,6 +74,8 @@ impl<'a> ParserContext<'a> {
|
||||
self.parse_show_create_table()
|
||||
} else if self.consume_token("FLOW") {
|
||||
self.parse_show_create_flow()
|
||||
} else if self.consume_token("VIEW") {
|
||||
self.parse_show_create_view()
|
||||
} else {
|
||||
self.unsupported(self.peek_token_as_string())
|
||||
}
|
||||
@@ -141,6 +143,24 @@ impl<'a> ParserContext<'a> {
|
||||
Ok(Statement::ShowCreateFlow(ShowCreateFlow { flow_name }))
|
||||
}
|
||||
|
||||
fn parse_show_create_view(&mut self) -> Result<Statement> {
|
||||
let raw_view_name = self
|
||||
.parse_object_name()
|
||||
.with_context(|_| error::UnexpectedSnafu {
|
||||
sql: self.sql,
|
||||
expected: "a view name",
|
||||
actual: self.peek_token_as_string(),
|
||||
})?;
|
||||
let view_name = Self::canonicalize_object_name(raw_view_name);
|
||||
ensure!(
|
||||
!view_name.0.is_empty(),
|
||||
InvalidTableNameSnafu {
|
||||
name: view_name.to_string(),
|
||||
}
|
||||
);
|
||||
Ok(Statement::ShowCreateView(ShowCreateView { view_name }))
|
||||
}
|
||||
|
||||
fn parse_show_table_name(&mut self) -> Result<String> {
|
||||
self.parser.next_token();
|
||||
let table_name = self
|
||||
@@ -906,4 +926,20 @@ mod tests {
|
||||
assert!(matches!(stmt.kind, ShowKind::Where(_)));
|
||||
assert_eq!(sql, stmt.to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn test_show_create_view() {
|
||||
let sql = "SHOW CREATE VIEW test";
|
||||
let result =
|
||||
ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default());
|
||||
let stmts = result.unwrap();
|
||||
assert_eq!(1, stmts.len());
|
||||
assert_eq!(
|
||||
stmts[0],
|
||||
Statement::ShowCreateView(ShowCreateView {
|
||||
view_name: ObjectName(vec![Ident::new("test")]),
|
||||
})
|
||||
);
|
||||
assert_eq!(sql, stmts[0].to_string());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -385,6 +385,8 @@ impl Display for CreateFlow {
|
||||
pub struct CreateView {
|
||||
/// View name
|
||||
pub name: ObjectName,
|
||||
/// An optional list of names to be used for columns of the view
|
||||
pub columns: Vec<Ident>,
|
||||
/// The clause after `As` that defines the VIEW.
|
||||
/// Can only be either [Statement::Query] or [Statement::Tql].
|
||||
pub query: Box<Statement>,
|
||||
@@ -405,6 +407,9 @@ impl Display for CreateView {
|
||||
write!(f, "IF NOT EXISTS ")?;
|
||||
}
|
||||
write!(f, "{} ", &self.name)?;
|
||||
if !self.columns.is_empty() {
|
||||
write!(f, "({}) ", format_list_comma!(self.columns))?;
|
||||
}
|
||||
write!(f, "AS {}", &self.query)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,6 +187,19 @@ impl Display for ShowCreateFlow {
|
||||
}
|
||||
}
|
||||
|
||||
/// SQL structure for `SHOW CREATE VIEW`.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Visit, VisitMut)]
|
||||
pub struct ShowCreateView {
|
||||
pub view_name: ObjectName,
|
||||
}
|
||||
|
||||
impl Display for ShowCreateView {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let view_name = &self.view_name;
|
||||
write!(f, "SHOW CREATE VIEW {view_name}")
|
||||
}
|
||||
}
|
||||
|
||||
/// SQL structure for `SHOW VARIABLES xxx`.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Visit, VisitMut)]
|
||||
pub struct ShowVariables {
|
||||
|
||||
@@ -31,8 +31,8 @@ use crate::statements::insert::Insert;
|
||||
use crate::statements::query::Query;
|
||||
use crate::statements::set_variables::SetVariables;
|
||||
use crate::statements::show::{
|
||||
ShowColumns, ShowCreateFlow, ShowCreateTable, ShowDatabases, ShowIndex, ShowKind, ShowStatus,
|
||||
ShowTableStatus, ShowTables, ShowVariables,
|
||||
ShowColumns, ShowCreateFlow, ShowCreateTable, ShowCreateView, ShowDatabases, ShowIndex,
|
||||
ShowKind, ShowStatus, ShowTableStatus, ShowTables, ShowVariables,
|
||||
};
|
||||
use crate::statements::tql::Tql;
|
||||
use crate::statements::truncate::TruncateTable;
|
||||
@@ -85,6 +85,8 @@ pub enum Statement {
|
||||
ShowCreateTable(ShowCreateTable),
|
||||
// SHOW CREATE FLOW
|
||||
ShowCreateFlow(ShowCreateFlow),
|
||||
// SHOW CREATE VIEW
|
||||
ShowCreateView(ShowCreateView),
|
||||
// SHOW STATUS
|
||||
ShowStatus(ShowStatus),
|
||||
// DESCRIBE TABLE
|
||||
@@ -126,6 +128,7 @@ impl Display for Statement {
|
||||
Statement::ShowIndex(s) => s.fmt(f),
|
||||
Statement::ShowCreateTable(s) => s.fmt(f),
|
||||
Statement::ShowCreateFlow(s) => s.fmt(f),
|
||||
Statement::ShowCreateView(s) => s.fmt(f),
|
||||
Statement::ShowStatus(s) => s.fmt(f),
|
||||
Statement::DescribeTable(s) => s.fmt(f),
|
||||
Statement::Explain(s) => s.fmt(f),
|
||||
|
||||
Reference in New Issue
Block a user