Skip to content

Commit 0ba09b6

Browse files
committed
feat: Support CREATE/DROP SECRET for duckdb.
1 parent 4472789 commit 0ba09b6

File tree

4 files changed

+246
-1
lines changed

4 files changed

+246
-1
lines changed

src/ast/mod.rs

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1999,6 +1999,19 @@ pub enum Statement {
19991999
authorization_owner: Option<ObjectName>,
20002000
},
20012001
/// ```sql
2002+
/// CREATE SECRET
2003+
/// ```
2004+
/// See [duckdb](https://duckdb.org/docs/sql/statements/create_secret.html)
2005+
CreateSecret {
2006+
or_replace: bool,
2007+
temporary: Option<bool>,
2008+
if_not_exists: bool,
2009+
name: Option<Ident>,
2010+
storage_specifier: Option<Ident>,
2011+
secret_type: Ident,
2012+
options: Vec<SecretOption>,
2013+
},
2014+
/// ```sql
20022015
/// ALTER TABLE
20032016
/// ```
20042017
AlterTable {
@@ -2080,6 +2093,15 @@ pub enum Statement {
20802093
option: Option<ReferentialAction>,
20812094
},
20822095
/// ```sql
2096+
/// DROP SECRET
2097+
/// ```
2098+
DropSecret {
2099+
if_exists: bool,
2100+
temporary: Option<bool>,
2101+
name: Ident,
2102+
storage_specifier: Option<Ident>,
2103+
},
2104+
/// ```sql
20832105
/// DECLARE
20842106
/// ```
20852107
/// Declare Cursor Variables
@@ -3515,6 +3537,48 @@ impl fmt::Display for Statement {
35153537
}
35163538
Ok(())
35173539
}
3540+
Statement::CreateSecret {
3541+
or_replace,
3542+
temporary,
3543+
if_not_exists,
3544+
name,
3545+
storage_specifier,
3546+
secret_type,
3547+
options,
3548+
} => {
3549+
write!(
3550+
f,
3551+
"CREATE {or_replace}",
3552+
or_replace = if *or_replace { "OR REPLACE " } else { "" },
3553+
)?;
3554+
if let Some(t) = temporary {
3555+
write!(f, "{}", if *t { "TEMPORARY " } else { "PERSISTENT " })?;
3556+
}
3557+
write!(
3558+
f,
3559+
"SECRET {if_not_exists}",
3560+
if_not_exists = if *if_not_exists { "IF NOT EXISTS " } else { "" },
3561+
)?;
3562+
if let Some(n) = name {
3563+
write!(f, "{n} ")?;
3564+
};
3565+
if let Some(s) = storage_specifier {
3566+
write!(f, "IN {s} ")?;
3567+
}
3568+
write!(
3569+
f,
3570+
"( TYPE {secret_type}",
3571+
)?;
3572+
if !options.is_empty() {
3573+
write!(
3574+
f,
3575+
", {o}",
3576+
o = display_comma_separated(options)
3577+
)?;
3578+
}
3579+
write!(f, " )")?;
3580+
Ok(())
3581+
}
35183582
Statement::AlterTable {
35193583
name,
35203584
if_exists,
@@ -3595,6 +3659,21 @@ impl fmt::Display for Statement {
35953659
}
35963660
Ok(())
35973661
}
3662+
Statement::DropSecret { if_exists, temporary, name, storage_specifier } => {
3663+
write!(f, "DROP ")?;
3664+
if let Some(t) = temporary {
3665+
write!(f, "{}", if *t { "TEMPORARY " } else { "PERSISTENT " })?;
3666+
}
3667+
write!(
3668+
f,
3669+
"SECRET {if_exists}{name}",
3670+
if_exists = if *if_exists { "IF EXISTS " } else { "" },
3671+
)?;
3672+
if let Some(s) = storage_specifier {
3673+
write!(f, " FROM {s}")?;
3674+
}
3675+
Ok(())
3676+
}
35983677
Statement::Discard { object_type } => {
35993678
write!(f, "DISCARD {object_type}")?;
36003679
Ok(())
@@ -5026,6 +5105,20 @@ impl fmt::Display for SqlOption {
50265105
}
50275106
}
50285107

5108+
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
5109+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
5110+
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
5111+
pub struct SecretOption {
5112+
pub key: Ident,
5113+
pub value: Ident,
5114+
}
5115+
5116+
impl fmt::Display for SecretOption {
5117+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
5118+
write!(f, "{} {}", self.key, self.value)
5119+
}
5120+
}
5121+
50295122
#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
50305123
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
50315124
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]

src/keywords.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -511,6 +511,7 @@ define_keywords!(
511511
PERCENTILE_DISC,
512512
PERCENT_RANK,
513513
PERIOD,
514+
PERSISTENT,
514515
PIVOT,
515516
PLACING,
516517
PLANS,
@@ -596,6 +597,7 @@ define_keywords!(
596597
SCROLL,
597598
SEARCH,
598599
SECOND,
600+
SECRET,
599601
SECURITY,
600602
SELECT,
601603
SEMI,

src/parser/mod.rs

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3031,6 +3031,7 @@ impl<'a> Parser<'a> {
30313031
let temporary = self
30323032
.parse_one_of_keywords(&[Keyword::TEMP, Keyword::TEMPORARY])
30333033
.is_some();
3034+
let persistent = dialect_of!(self is DuckDbDialect) && self.parse_one_of_keywords(&[Keyword::PERSISTENT]).is_some();
30343035
if self.parse_keyword(Keyword::TABLE) {
30353036
self.parse_create_table(or_replace, temporary, global, transient)
30363037
} else if self.parse_keyword(Keyword::MATERIALIZED) || self.parse_keyword(Keyword::VIEW) {
@@ -3042,6 +3043,8 @@ impl<'a> Parser<'a> {
30423043
self.parse_create_function(or_replace, temporary)
30433044
} else if self.parse_keyword(Keyword::MACRO) {
30443045
self.parse_create_macro(or_replace, temporary)
3046+
} else if self.parse_keyword(Keyword::SECRET) {
3047+
self.parse_create_secret(or_replace, temporary, persistent)
30453048
} else if or_replace {
30463049
self.expected(
30473050
"[EXTERNAL] TABLE or [MATERIALIZED] VIEW or FUNCTION after CREATE OR REPLACE",
@@ -3072,6 +3075,62 @@ impl<'a> Parser<'a> {
30723075
}
30733076
}
30743077

3078+
/// See [DuckDB Docs](https://duckdb.org/docs/sql/statements/create_secret.html) for more details.
3079+
pub fn parse_create_secret(
3080+
&mut self,
3081+
or_replace: bool,
3082+
temporary: bool,
3083+
persistent: bool,
3084+
) -> Result<Statement, ParserError> {
3085+
let if_not_exists = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]);
3086+
3087+
let mut storage_specifier = None;
3088+
let mut name = None;
3089+
if self.peek_token() != Token::LParen {
3090+
if self.parse_keyword(Keyword::IN) {
3091+
storage_specifier = self.parse_identifier(false).ok()
3092+
} else {
3093+
name = self.parse_identifier(false).ok();
3094+
}
3095+
3096+
// Storage specifier may follow the name
3097+
if storage_specifier.is_none() && self.peek_token() != Token::LParen && self.parse_keyword(Keyword::IN) {
3098+
storage_specifier = self.parse_identifier(false).ok();
3099+
}
3100+
}
3101+
3102+
self.expect_token(&Token::LParen)?;
3103+
self.expect_keyword(Keyword::TYPE)?;
3104+
let secret_type = self.parse_identifier(false)?;
3105+
3106+
let mut options = Vec::new();
3107+
if self.consume_token(&Token::Comma) {
3108+
options.append(&mut self.parse_comma_separated(|p| {
3109+
let key = p.parse_identifier(false)?;
3110+
let value = p.parse_identifier(false)?;
3111+
Ok(SecretOption { key, value })
3112+
})?);
3113+
}
3114+
self.expect_token(&Token::RParen)?;
3115+
3116+
let temp = match (temporary, persistent) {
3117+
(true, false) => Some(true),
3118+
(false, true) => Some(false),
3119+
(false, false) => None,
3120+
_ => self.expected("TEMPORARY or PERSISTENT", self.peek_token())?,
3121+
};
3122+
3123+
Ok(Statement::CreateSecret {
3124+
or_replace,
3125+
temporary: temp,
3126+
if_not_exists,
3127+
name,
3128+
storage_specifier,
3129+
secret_type,
3130+
options,
3131+
})
3132+
}
3133+
30753134
/// Parse a CACHE TABLE statement
30763135
pub fn parse_cache_table(&mut self) -> Result<Statement, ParserError> {
30773136
let (mut table_flag, mut options, mut has_as, mut query) = (None, vec![], false, None);
@@ -3805,8 +3864,9 @@ impl<'a> Parser<'a> {
38053864

38063865
pub fn parse_drop(&mut self) -> Result<Statement, ParserError> {
38073866
// MySQL dialect supports `TEMPORARY`
3808-
let temporary = dialect_of!(self is MySqlDialect | GenericDialect)
3867+
let temporary = dialect_of!(self is MySqlDialect | GenericDialect | DuckDbDialect)
38093868
&& self.parse_keyword(Keyword::TEMPORARY);
3869+
let persistent = dialect_of!(self is DuckDbDialect) && self.parse_one_of_keywords(&[Keyword::PERSISTENT]).is_some();
38103870

38113871
let object_type = if self.parse_keyword(Keyword::TABLE) {
38123872
ObjectType::Table
@@ -3824,6 +3884,8 @@ impl<'a> Parser<'a> {
38243884
ObjectType::Stage
38253885
} else if self.parse_keyword(Keyword::FUNCTION) {
38263886
return self.parse_drop_function();
3887+
} else if self.parse_keyword(Keyword::SECRET) {
3888+
return self.parse_drop_secret(temporary, persistent);
38273889
} else {
38283890
return self.expected(
38293891
"TABLE, VIEW, INDEX, ROLE, SCHEMA, FUNCTION, STAGE or SEQUENCE after DROP",
@@ -3896,6 +3958,25 @@ impl<'a> Parser<'a> {
38963958
Ok(DropFunctionDesc { name, args })
38973959
}
38983960

3961+
/// See [DuckDB Docs](https://duckdb.org/docs/sql/statements/create_secret.html) for more details.
3962+
fn parse_drop_secret(&mut self, temporary: bool, persistent: bool) -> Result<Statement, ParserError> {
3963+
let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]);
3964+
let name = self.parse_identifier(false)?;
3965+
let storage_specifier = if self.parse_keyword(Keyword::FROM) {
3966+
self.parse_identifier(false).ok()
3967+
} else {
3968+
None
3969+
};
3970+
let temp = match (temporary, persistent) {
3971+
(true, false) => Some(true),
3972+
(false, true) => Some(false),
3973+
(false, false) => None,
3974+
_ => self.expected("TEMPORARY or PERSISTENT", self.peek_token())?,
3975+
};
3976+
3977+
Ok(Statement::DropSecret { if_exists, temporary: temp, name, storage_specifier})
3978+
}
3979+
38993980
/// Parse a `DECLARE` statement.
39003981
///
39013982
/// ```sql

tests/sqlparser_duckdb.rs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,3 +246,72 @@ fn test_duckdb_load_extension() {
246246
stmt
247247
);
248248
}
249+
250+
#[test]
251+
fn test_create_secret() {
252+
let sql = r#"CREATE OR REPLACE PERSISTENT SECRET IF NOT EXISTS name IN storage ( TYPE type, key1 value1, key2 value2 )"#;
253+
let stmt = duckdb().verified_stmt(sql);
254+
assert_eq!(
255+
Statement::CreateSecret {
256+
or_replace: true,
257+
temporary: Some(false),
258+
if_not_exists: true,
259+
name: Some(Ident::new("name")),
260+
storage_specifier: Some(Ident::new("storage")),
261+
secret_type: Ident::new("type"),
262+
options: vec![
263+
SecretOption {
264+
key: Ident::new("key1"),
265+
value: Ident::new("value1"),
266+
},
267+
SecretOption {
268+
key: Ident::new("key2"),
269+
value: Ident::new("value2"),
270+
}
271+
]
272+
},
273+
stmt
274+
);
275+
}
276+
277+
#[test]
278+
fn test_create_secret_simple() {
279+
let sql = r#"CREATE SECRET ( TYPE type )"#;
280+
let stmt = duckdb().verified_stmt(sql);
281+
assert_eq!(
282+
Statement::CreateSecret {
283+
or_replace: false,
284+
temporary: None,
285+
if_not_exists: false,
286+
name: None,
287+
storage_specifier: None,
288+
secret_type: Ident::new("type"),
289+
options: vec![]
290+
},
291+
stmt
292+
);
293+
}
294+
295+
#[test]
296+
fn test_drop_secret() {
297+
let sql = r#"DROP PERSISTENT SECRET IF EXISTS secret FROM storage"#;
298+
let stmt = duckdb().verified_stmt(sql);
299+
assert_eq!(
300+
Statement::DropSecret {
301+
if_exists: true, temporary: Some(false), name: Ident::new("secret"), storage_specifier: Some(Ident::new("storage")) }
302+
,
303+
stmt
304+
);
305+
}
306+
307+
#[test]
308+
fn test_drop_secret_simple() {
309+
let sql = r#"DROP SECRET secret"#;
310+
let stmt = duckdb().verified_stmt(sql);
311+
assert_eq!(
312+
Statement::DropSecret {
313+
if_exists: false, temporary: None, name: Ident::new("secret"), storage_specifier: None }
314+
,
315+
stmt
316+
);
317+
}

0 commit comments

Comments
 (0)