Skip to content

Commit 234182b

Browse files
MartinNowakserprex
authored andcommitted
Add support for DuckDB's CREATE MACRO statements (apache#897)
1 parent 86ac436 commit 234182b

File tree

5 files changed

+208
-0
lines changed

5 files changed

+208
-0
lines changed

src/ast/mod.rs

+85
Original file line numberDiff line numberDiff line change
@@ -1697,6 +1697,19 @@ pub enum Statement {
16971697
params: CreateFunctionBody,
16981698
},
16991699
/// ```sql
1700+
/// CREATE MACRO
1701+
/// ```
1702+
///
1703+
/// Supported variants:
1704+
/// 1. [DuckDB](https://duckdb.org/docs/sql/statements/create_macro)
1705+
CreateMacro {
1706+
or_replace: bool,
1707+
temporary: bool,
1708+
name: ObjectName,
1709+
args: Option<Vec<MacroArg>>,
1710+
definition: MacroDefinition,
1711+
},
1712+
/// ```sql
17001713
/// CREATE STAGE
17011714
/// ```
17021715
/// See <https://docs.snowflake.com/en/sql-reference/sql/create-stage>
@@ -2288,6 +2301,28 @@ impl fmt::Display for Statement {
22882301
write!(f, "{params}")?;
22892302
Ok(())
22902303
}
2304+
Statement::CreateMacro {
2305+
or_replace,
2306+
temporary,
2307+
name,
2308+
args,
2309+
definition,
2310+
} => {
2311+
write!(
2312+
f,
2313+
"CREATE {or_replace}{temp}MACRO {name}",
2314+
temp = if *temporary { "TEMPORARY " } else { "" },
2315+
or_replace = if *or_replace { "OR REPLACE " } else { "" },
2316+
)?;
2317+
if let Some(args) = args {
2318+
write!(f, "({})", display_comma_separated(args))?;
2319+
}
2320+
match definition {
2321+
MacroDefinition::Expr(expr) => write!(f, " AS {expr}")?,
2322+
MacroDefinition::Table(query) => write!(f, " AS TABLE {query}")?,
2323+
}
2324+
Ok(())
2325+
}
22912326
Statement::CreateView {
22922327
name,
22932328
or_replace,
@@ -4585,6 +4620,56 @@ impl fmt::Display for CreateFunctionUsing {
45854620
}
45864621
}
45874622

4623+
/// `NAME = <EXPR>` arguments for DuckDB macros
4624+
///
4625+
/// See [Create Macro - DuckDB](https://duckdb.org/docs/sql/statements/create_macro)
4626+
/// for more details
4627+
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
4628+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
4629+
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
4630+
pub struct MacroArg {
4631+
pub name: Ident,
4632+
pub default_expr: Option<Expr>,
4633+
}
4634+
4635+
impl MacroArg {
4636+
/// Returns an argument with name.
4637+
pub fn new(name: &str) -> Self {
4638+
Self {
4639+
name: name.into(),
4640+
default_expr: None,
4641+
}
4642+
}
4643+
}
4644+
4645+
impl fmt::Display for MacroArg {
4646+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
4647+
write!(f, "{}", self.name)?;
4648+
if let Some(default_expr) = &self.default_expr {
4649+
write!(f, " := {default_expr}")?;
4650+
}
4651+
Ok(())
4652+
}
4653+
}
4654+
4655+
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
4656+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
4657+
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
4658+
pub enum MacroDefinition {
4659+
Expr(Expr),
4660+
Table(Query),
4661+
}
4662+
4663+
impl fmt::Display for MacroDefinition {
4664+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
4665+
match self {
4666+
MacroDefinition::Expr(expr) => write!(f, "{expr}")?,
4667+
MacroDefinition::Table(query) => write!(f, "{query}")?,
4668+
}
4669+
Ok(())
4670+
}
4671+
}
4672+
45884673
/// Schema possible naming variants ([1]).
45894674
///
45904675
/// [1]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#schema-definition

src/keywords.rs

+1
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,7 @@ define_keywords!(
351351
LOCKED,
352352
LOGIN,
353353
LOWER,
354+
MACRO,
354355
MANAGEDLOCATION,
355356
MAPPING,
356357
MATCH,

src/parser.rs

+51
Original file line numberDiff line numberDiff line change
@@ -2350,6 +2350,8 @@ impl<'a> Parser<'a> {
23502350
self.parse_create_external_table(or_replace)
23512351
} else if self.parse_keyword(Keyword::FUNCTION) {
23522352
self.parse_create_function(or_replace, temporary)
2353+
} else if self.parse_keyword(Keyword::MACRO) {
2354+
self.parse_create_macro(or_replace, temporary)
23532355
} else if or_replace {
23542356
self.expected(
23552357
"[EXTERNAL] TABLE or [MATERIALIZED] VIEW or FUNCTION after CREATE OR REPLACE",
@@ -2632,6 +2634,8 @@ impl<'a> Parser<'a> {
26322634
return_type,
26332635
params,
26342636
})
2637+
} else if dialect_of!(self is DuckDbDialect) {
2638+
self.parse_create_macro(or_replace, temporary)
26352639
} else {
26362640
self.prev_token();
26372641
self.expected("an object type after CREATE", self.peek_token())
@@ -2707,6 +2711,53 @@ impl<'a> Parser<'a> {
27072711
}
27082712
}
27092713

2714+
pub fn parse_create_macro(
2715+
&mut self,
2716+
or_replace: bool,
2717+
temporary: bool,
2718+
) -> Result<Statement, ParserError> {
2719+
if dialect_of!(self is DuckDbDialect | GenericDialect) {
2720+
let name = self.parse_object_name()?;
2721+
self.expect_token(&Token::LParen)?;
2722+
let args = if self.consume_token(&Token::RParen) {
2723+
self.prev_token();
2724+
None
2725+
} else {
2726+
Some(self.parse_comma_separated(Parser::parse_macro_arg)?)
2727+
};
2728+
2729+
self.expect_token(&Token::RParen)?;
2730+
self.expect_keyword(Keyword::AS)?;
2731+
2732+
Ok(Statement::CreateMacro {
2733+
or_replace,
2734+
temporary,
2735+
name,
2736+
args,
2737+
definition: if self.parse_keyword(Keyword::TABLE) {
2738+
MacroDefinition::Table(self.parse_query()?)
2739+
} else {
2740+
MacroDefinition::Expr(self.parse_expr()?)
2741+
},
2742+
})
2743+
} else {
2744+
self.prev_token();
2745+
self.expected("an object type after CREATE", self.peek_token())
2746+
}
2747+
}
2748+
2749+
fn parse_macro_arg(&mut self) -> Result<MacroArg, ParserError> {
2750+
let name = self.parse_identifier()?;
2751+
2752+
let default_expr =
2753+
if self.consume_token(&Token::DuckAssignment) || self.consume_token(&Token::RArrow) {
2754+
Some(self.parse_expr()?)
2755+
} else {
2756+
None
2757+
};
2758+
Ok(MacroArg { name, default_expr })
2759+
}
2760+
27102761
pub fn parse_create_external_table(
27112762
&mut self,
27122763
or_replace: bool,

src/tokenizer.rs

+4
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,8 @@ pub enum Token {
114114
Colon,
115115
/// DoubleColon `::` (used for casting in postgresql)
116116
DoubleColon,
117+
/// Assignment `:=` (used for keyword argument in DuckDB macros)
118+
DuckAssignment,
117119
/// SemiColon `;` used as separator for COPY and payload
118120
SemiColon,
119121
/// Backslash `\` used in terminating the COPY payload with `\.`
@@ -222,6 +224,7 @@ impl fmt::Display for Token {
222224
Token::Period => f.write_str("."),
223225
Token::Colon => f.write_str(":"),
224226
Token::DoubleColon => f.write_str("::"),
227+
Token::DuckAssignment => f.write_str(":="),
225228
Token::SemiColon => f.write_str(";"),
226229
Token::Backslash => f.write_str("\\"),
227230
Token::LBracket => f.write_str("["),
@@ -847,6 +850,7 @@ impl<'a> Tokenizer<'a> {
847850
chars.next();
848851
match chars.peek() {
849852
Some(':') => self.consume_and_return(chars, Token::DoubleColon),
853+
Some('=') => self.consume_and_return(chars, Token::DuckAssignment),
850854
_ => Ok(Some(Token::Colon)),
851855
}
852856
}

tests/sqlparser_duckdb.rs

+67
Original file line numberDiff line numberDiff line change
@@ -68,3 +68,70 @@ fn test_select_wildcard_with_exclude() {
6868
fn parse_div_infix() {
6969
duckdb_and_generic().verified_stmt(r#"SELECT 5 // 2"#);
7070
}
71+
72+
#[test]
73+
fn test_create_macro() {
74+
let macro_ = duckdb().verified_stmt("CREATE MACRO schema.add(a, b) AS a + b");
75+
let expected = Statement::CreateMacro {
76+
or_replace: false,
77+
temporary: false,
78+
name: ObjectName(vec![Ident::new("schema"), Ident::new("add")]),
79+
args: Some(vec![MacroArg::new("a"), MacroArg::new("b")]),
80+
definition: MacroDefinition::Expr(Expr::BinaryOp {
81+
left: Box::new(Expr::Identifier(Ident::new("a"))),
82+
op: BinaryOperator::Plus,
83+
right: Box::new(Expr::Identifier(Ident::new("b"))),
84+
}),
85+
};
86+
assert_eq!(expected, macro_);
87+
}
88+
89+
#[test]
90+
fn test_create_macro_default_args() {
91+
let macro_ = duckdb().verified_stmt("CREATE MACRO add_default(a, b := 5) AS a + b");
92+
let expected = Statement::CreateMacro {
93+
or_replace: false,
94+
temporary: false,
95+
name: ObjectName(vec![Ident::new("add_default")]),
96+
args: Some(vec![
97+
MacroArg::new("a"),
98+
MacroArg {
99+
name: Ident::new("b"),
100+
default_expr: Some(Expr::Value(Value::Number(
101+
#[cfg(not(feature = "bigdecimal"))]
102+
5.to_string(),
103+
#[cfg(feature = "bigdecimal")]
104+
bigdecimal::BigDecimal::from(5),
105+
false,
106+
))),
107+
},
108+
]),
109+
definition: MacroDefinition::Expr(Expr::BinaryOp {
110+
left: Box::new(Expr::Identifier(Ident::new("a"))),
111+
op: BinaryOperator::Plus,
112+
right: Box::new(Expr::Identifier(Ident::new("b"))),
113+
}),
114+
};
115+
assert_eq!(expected, macro_);
116+
}
117+
118+
#[test]
119+
fn test_create_table_macro() {
120+
let query = "SELECT col1_value AS column1, col2_value AS column2 UNION ALL SELECT 'Hello' AS col1_value, 456 AS col2_value";
121+
let macro_ = duckdb().verified_stmt(
122+
&("CREATE OR REPLACE TEMPORARY MACRO dynamic_table(col1_value, col2_value) AS TABLE "
123+
.to_string()
124+
+ query),
125+
);
126+
let expected = Statement::CreateMacro {
127+
or_replace: true,
128+
temporary: true,
129+
name: ObjectName(vec![Ident::new("dynamic_table")]),
130+
args: Some(vec![
131+
MacroArg::new("col1_value"),
132+
MacroArg::new("col2_value"),
133+
]),
134+
definition: MacroDefinition::Table(duckdb().verified_query(query)),
135+
};
136+
assert_eq!(expected, macro_);
137+
}

0 commit comments

Comments
 (0)