Skip to content

Commit 1bed87a

Browse files
authored
Suppor postgres TRUNCATE syntax (#1406)
1 parent 4d52ee7 commit 1bed87a

File tree

4 files changed

+185
-11
lines changed

4 files changed

+185
-11
lines changed

src/ast/mod.rs

+73-4
Original file line numberDiff line numberDiff line change
@@ -2011,11 +2011,19 @@ pub enum Statement {
20112011
/// ```
20122012
/// Truncate (Hive)
20132013
Truncate {
2014-
#[cfg_attr(feature = "visitor", visit(with = "visit_relation"))]
2015-
table_name: ObjectName,
2014+
table_names: Vec<TruncateTableTarget>,
20162015
partitions: Option<Vec<Expr>>,
20172016
/// TABLE - optional keyword;
20182017
table: bool,
2018+
/// Postgres-specific option
2019+
/// [ TRUNCATE TABLE ONLY ]
2020+
only: bool,
2021+
/// Postgres-specific option
2022+
/// [ RESTART IDENTITY | CONTINUE IDENTITY ]
2023+
identity: Option<TruncateIdentityOption>,
2024+
/// Postgres-specific option
2025+
/// [ CASCADE | RESTRICT ]
2026+
cascade: Option<TruncateCascadeOption>,
20192027
},
20202028
/// ```sql
20212029
/// MSCK
@@ -3131,12 +3139,35 @@ impl fmt::Display for Statement {
31313139
Ok(())
31323140
}
31333141
Statement::Truncate {
3134-
table_name,
3142+
table_names,
31353143
partitions,
31363144
table,
3145+
only,
3146+
identity,
3147+
cascade,
31373148
} => {
31383149
let table = if *table { "TABLE " } else { "" };
3139-
write!(f, "TRUNCATE {table}{table_name}")?;
3150+
let only = if *only { "ONLY " } else { "" };
3151+
3152+
write!(
3153+
f,
3154+
"TRUNCATE {table}{only}{table_names}",
3155+
table_names = display_comma_separated(table_names)
3156+
)?;
3157+
3158+
if let Some(identity) = identity {
3159+
match identity {
3160+
TruncateIdentityOption::Restart => write!(f, " RESTART IDENTITY")?,
3161+
TruncateIdentityOption::Continue => write!(f, " CONTINUE IDENTITY")?,
3162+
}
3163+
}
3164+
if let Some(cascade) = cascade {
3165+
match cascade {
3166+
TruncateCascadeOption::Cascade => write!(f, " CASCADE")?,
3167+
TruncateCascadeOption::Restrict => write!(f, " RESTRICT")?,
3168+
}
3169+
}
3170+
31403171
if let Some(ref parts) = partitions {
31413172
if !parts.is_empty() {
31423173
write!(f, " PARTITION ({})", display_comma_separated(parts))?;
@@ -4587,6 +4618,44 @@ impl fmt::Display for SequenceOptions {
45874618
}
45884619
}
45894620

4621+
/// Target of a `TRUNCATE TABLE` command
4622+
///
4623+
/// Note this is its own struct because `visit_relation` requires an `ObjectName` (not a `Vec<ObjectName>`)
4624+
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
4625+
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
4626+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
4627+
pub struct TruncateTableTarget {
4628+
/// name of the table being truncated
4629+
#[cfg_attr(feature = "visitor", visit(with = "visit_relation"))]
4630+
pub name: ObjectName,
4631+
}
4632+
4633+
impl fmt::Display for TruncateTableTarget {
4634+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
4635+
write!(f, "{}", self.name)
4636+
}
4637+
}
4638+
4639+
/// PostgreSQL identity option for TRUNCATE table
4640+
/// [ RESTART IDENTITY | CONTINUE IDENTITY ]
4641+
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
4642+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
4643+
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
4644+
pub enum TruncateIdentityOption {
4645+
Restart,
4646+
Continue,
4647+
}
4648+
4649+
/// PostgreSQL cascade option for TRUNCATE table
4650+
/// [ CASCADE | RESTRICT ]
4651+
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
4652+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
4653+
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
4654+
pub enum TruncateCascadeOption {
4655+
Cascade,
4656+
Restrict,
4657+
}
4658+
45904659
/// Can use to describe options in create sequence or table column type identity
45914660
/// [ MINVALUE minvalue | NO MINVALUE ] [ MAXVALUE maxvalue | NO MAXVALUE ]
45924661
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]

src/keywords.rs

+1
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ define_keywords!(
177177
CONNECTION,
178178
CONSTRAINT,
179179
CONTAINS,
180+
CONTINUE,
180181
CONVERT,
181182
COPY,
182183
COPY_OPTIONS,

src/parser/mod.rs

+34-2
Original file line numberDiff line numberDiff line change
@@ -681,17 +681,49 @@ impl<'a> Parser<'a> {
681681

682682
pub fn parse_truncate(&mut self) -> Result<Statement, ParserError> {
683683
let table = self.parse_keyword(Keyword::TABLE);
684-
let table_name = self.parse_object_name(false)?;
684+
let only = self.parse_keyword(Keyword::ONLY);
685+
686+
let table_names = self
687+
.parse_comma_separated(|p| p.parse_object_name(false))?
688+
.into_iter()
689+
.map(|n| TruncateTableTarget { name: n })
690+
.collect();
691+
685692
let mut partitions = None;
686693
if self.parse_keyword(Keyword::PARTITION) {
687694
self.expect_token(&Token::LParen)?;
688695
partitions = Some(self.parse_comma_separated(Parser::parse_expr)?);
689696
self.expect_token(&Token::RParen)?;
690697
}
698+
699+
let mut identity = None;
700+
let mut cascade = None;
701+
702+
if dialect_of!(self is PostgreSqlDialect | GenericDialect) {
703+
identity = if self.parse_keywords(&[Keyword::RESTART, Keyword::IDENTITY]) {
704+
Some(TruncateIdentityOption::Restart)
705+
} else if self.parse_keywords(&[Keyword::CONTINUE, Keyword::IDENTITY]) {
706+
Some(TruncateIdentityOption::Continue)
707+
} else {
708+
None
709+
};
710+
711+
cascade = if self.parse_keyword(Keyword::CASCADE) {
712+
Some(TruncateCascadeOption::Cascade)
713+
} else if self.parse_keyword(Keyword::RESTRICT) {
714+
Some(TruncateCascadeOption::Restrict)
715+
} else {
716+
None
717+
};
718+
};
719+
691720
Ok(Statement::Truncate {
692-
table_name,
721+
table_names,
693722
partitions,
694723
table,
724+
only,
725+
identity,
726+
cascade,
695727
})
696728
}
697729

tests/sqlparser_postgres.rs

+77-5
Original file line numberDiff line numberDiff line change
@@ -571,6 +571,10 @@ fn parse_alter_table_constraints_rename() {
571571
fn parse_alter_table_disable() {
572572
pg_and_generic().verified_stmt("ALTER TABLE tab DISABLE ROW LEVEL SECURITY");
573573
pg_and_generic().verified_stmt("ALTER TABLE tab DISABLE RULE rule_name");
574+
}
575+
576+
#[test]
577+
fn parse_alter_table_disable_trigger() {
574578
pg_and_generic().verified_stmt("ALTER TABLE tab DISABLE TRIGGER ALL");
575579
pg_and_generic().verified_stmt("ALTER TABLE tab DISABLE TRIGGER USER");
576580
pg_and_generic().verified_stmt("ALTER TABLE tab DISABLE TRIGGER trigger_name");
@@ -589,6 +593,13 @@ fn parse_alter_table_enable() {
589593
pg_and_generic().verified_stmt("ALTER TABLE tab ENABLE TRIGGER trigger_name");
590594
}
591595

596+
#[test]
597+
fn parse_truncate_table() {
598+
pg_and_generic()
599+
.verified_stmt("TRUNCATE TABLE \"users\", \"orders\" RESTART IDENTITY RESTRICT");
600+
pg_and_generic().verified_stmt("TRUNCATE users, orders RESTART IDENTITY");
601+
}
602+
592603
#[test]
593604
fn parse_create_extension() {
594605
pg_and_generic().verified_stmt("CREATE EXTENSION extension_name");
@@ -3967,11 +3978,72 @@ fn parse_select_group_by_cube() {
39673978
#[test]
39683979
fn parse_truncate() {
39693980
let truncate = pg_and_generic().verified_stmt("TRUNCATE db.table_name");
3981+
let table_name = ObjectName(vec![Ident::new("db"), Ident::new("table_name")]);
3982+
let table_names = vec![TruncateTableTarget {
3983+
name: table_name.clone(),
3984+
}];
3985+
assert_eq!(
3986+
Statement::Truncate {
3987+
table_names,
3988+
partitions: None,
3989+
table: false,
3990+
only: false,
3991+
identity: None,
3992+
cascade: None,
3993+
},
3994+
truncate
3995+
);
3996+
}
3997+
3998+
#[test]
3999+
fn parse_truncate_with_options() {
4000+
let truncate = pg_and_generic()
4001+
.verified_stmt("TRUNCATE TABLE ONLY db.table_name RESTART IDENTITY CASCADE");
4002+
4003+
let table_name = ObjectName(vec![Ident::new("db"), Ident::new("table_name")]);
4004+
let table_names = vec![TruncateTableTarget {
4005+
name: table_name.clone(),
4006+
}];
4007+
39704008
assert_eq!(
39714009
Statement::Truncate {
3972-
table_name: ObjectName(vec![Ident::new("db"), Ident::new("table_name")]),
4010+
table_names,
39734011
partitions: None,
3974-
table: false
4012+
table: true,
4013+
only: true,
4014+
identity: Some(TruncateIdentityOption::Restart),
4015+
cascade: Some(TruncateCascadeOption::Cascade)
4016+
},
4017+
truncate
4018+
);
4019+
}
4020+
4021+
#[test]
4022+
fn parse_truncate_with_table_list() {
4023+
let truncate = pg().verified_stmt(
4024+
"TRUNCATE TABLE db.table_name, db.other_table_name RESTART IDENTITY CASCADE",
4025+
);
4026+
4027+
let table_name_a = ObjectName(vec![Ident::new("db"), Ident::new("table_name")]);
4028+
let table_name_b = ObjectName(vec![Ident::new("db"), Ident::new("other_table_name")]);
4029+
4030+
let table_names = vec![
4031+
TruncateTableTarget {
4032+
name: table_name_a.clone(),
4033+
},
4034+
TruncateTableTarget {
4035+
name: table_name_b.clone(),
4036+
},
4037+
];
4038+
4039+
assert_eq!(
4040+
Statement::Truncate {
4041+
table_names,
4042+
partitions: None,
4043+
table: true,
4044+
only: false,
4045+
identity: Some(TruncateIdentityOption::Restart),
4046+
cascade: Some(TruncateCascadeOption::Cascade)
39754047
},
39764048
truncate
39774049
);
@@ -4745,12 +4817,12 @@ fn parse_trigger_related_functions() {
47454817
IF NEW.salary IS NULL THEN
47464818
RAISE EXCEPTION '% cannot have null salary', NEW.empname;
47474819
END IF;
4748-
4820+
47494821
-- Who works for us when they must pay for it?
47504822
IF NEW.salary < 0 THEN
47514823
RAISE EXCEPTION '% cannot have a negative salary', NEW.empname;
47524824
END IF;
4753-
4825+
47544826
-- Remember who changed the payroll when
47554827
NEW.last_date := current_timestamp;
47564828
NEW.last_user := current_user;
@@ -4883,7 +4955,7 @@ fn parse_trigger_related_functions() {
48834955
Expr::Value(
48844956
Value::DollarQuotedString(
48854957
DollarQuotedString {
4886-
value: "\n BEGIN\n -- Check that empname and salary are given\n IF NEW.empname IS NULL THEN\n RAISE EXCEPTION 'empname cannot be null';\n END IF;\n IF NEW.salary IS NULL THEN\n RAISE EXCEPTION '% cannot have null salary', NEW.empname;\n END IF;\n \n -- Who works for us when they must pay for it?\n IF NEW.salary < 0 THEN\n RAISE EXCEPTION '% cannot have a negative salary', NEW.empname;\n END IF;\n \n -- Remember who changed the payroll when\n NEW.last_date := current_timestamp;\n NEW.last_user := current_user;\n RETURN NEW;\n END;\n ".to_owned(),
4958+
value: "\n BEGIN\n -- Check that empname and salary are given\n IF NEW.empname IS NULL THEN\n RAISE EXCEPTION 'empname cannot be null';\n END IF;\n IF NEW.salary IS NULL THEN\n RAISE EXCEPTION '% cannot have null salary', NEW.empname;\n END IF;\n\n -- Who works for us when they must pay for it?\n IF NEW.salary < 0 THEN\n RAISE EXCEPTION '% cannot have a negative salary', NEW.empname;\n END IF;\n\n -- Remember who changed the payroll when\n NEW.last_date := current_timestamp;\n NEW.last_user := current_user;\n RETURN NEW;\n END;\n ".to_owned(),
48874959
tag: Some(
48884960
"emp_stamp".to_owned(),
48894961
),

0 commit comments

Comments
 (0)