Add support for "on delete cascade" column option (#170)

Specifically, `FOREIGN KEY REFERENCES <foreign_table> (<referred_columns>)`
can now be followed by `ON DELETE <referential_action>` and/or by
`ON UPDATE <referential_action>`.
This commit is contained in:
Christoph Müller 2020-05-27 17:24:23 +02:00 committed by GitHub
parent 789fcc8521
commit 98f97d09db
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 131 additions and 11 deletions

View file

@ -155,10 +155,15 @@ pub enum ColumnOption {
is_primary: bool,
},
/// A referential integrity constraint (`[FOREIGN KEY REFERENCES
/// <foreign_table> (<referred_columns>)`).
/// <foreign_table> (<referred_columns>)
/// { [ON DELETE <referential_action>] [ON UPDATE <referential_action>] |
/// [ON UPDATE <referential_action>] [ON DELETE <referential_action>]
/// }`).
ForeignKey {
foreign_table: ObjectName,
referred_columns: Vec<Ident>,
on_delete: Option<ReferentialAction>,
on_update: Option<ReferentialAction>,
},
// `CHECK (<expr>)`
Check(Expr),
@ -177,12 +182,21 @@ impl fmt::Display for ColumnOption {
ForeignKey {
foreign_table,
referred_columns,
} => write!(
f,
"REFERENCES {} ({})",
foreign_table,
display_comma_separated(referred_columns)
),
on_delete,
on_update,
} => {
write!(f, "REFERENCES {}", foreign_table)?;
if !referred_columns.is_empty() {
write!(f, " ({})", display_comma_separated(referred_columns))?;
}
if let Some(action) = on_delete {
write!(f, " ON DELETE {}", action)?;
}
if let Some(action) = on_update {
write!(f, " ON UPDATE {}", action)?;
}
Ok(())
}
Check(expr) => write!(f, "CHECK ({})", expr),
}
}
@ -200,3 +214,28 @@ fn display_constraint_name<'a>(name: &'a Option<Ident>) -> impl fmt::Display + '
}
ConstraintName(name)
}
/// `<referential_action> =
/// { RESTRICT | CASCADE | SET NULL | NO ACTION | SET DEFAULT }`
///
/// Used in foreign key constraints in `ON UPDATE` and `ON DELETE` options.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum ReferentialAction {
Restrict,
Cascade,
SetNull,
NoAction,
SetDefault,
}
impl fmt::Display for ReferentialAction {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str(match self {
ReferentialAction::Restrict => "RESTRICT",
ReferentialAction::Cascade => "CASCADE",
ReferentialAction::SetNull => "SET NULL",
ReferentialAction::NoAction => "NO ACTION",
ReferentialAction::SetDefault => "SET DEFAULT",
})
}
}

View file

@ -22,7 +22,8 @@ use std::fmt;
pub use self::data_type::DataType;
pub use self::ddl::{
AlterTableOperation, ColumnDef, ColumnOption, ColumnOptionDef, TableConstraint,
AlterTableOperation, ColumnDef, ColumnOption, ColumnOptionDef, ReferentialAction,
TableConstraint,
};
pub use self::operator::{BinaryOperator, UnaryOperator};
pub use self::query::{

View file

@ -51,6 +51,7 @@ macro_rules! define_keywords {
define_keywords!(
ABS,
ACTION,
ADD,
ASC,
ALL,

View file

@ -1038,10 +1038,25 @@ impl Parser {
ColumnOption::Unique { is_primary: false }
} else if self.parse_keyword("REFERENCES") {
let foreign_table = self.parse_object_name()?;
let referred_columns = self.parse_parenthesized_column_list(Mandatory)?;
// PostgreSQL allows omitting the column list and
// uses the primary key column of the foreign table by default
let referred_columns = self.parse_parenthesized_column_list(Optional)?;
let mut on_delete = None;
let mut on_update = None;
loop {
if on_delete.is_none() && self.parse_keywords(vec!["ON", "DELETE"]) {
on_delete = Some(self.parse_referential_action()?);
} else if on_update.is_none() && self.parse_keywords(vec!["ON", "UPDATE"]) {
on_update = Some(self.parse_referential_action()?);
} else {
break;
}
}
ColumnOption::ForeignKey {
foreign_table,
referred_columns,
on_delete,
on_update,
}
} else if self.parse_keyword("CHECK") {
self.expect_token(&Token::LParen)?;
@ -1055,6 +1070,25 @@ impl Parser {
Ok(ColumnOptionDef { name, option })
}
pub fn parse_referential_action(&mut self) -> Result<ReferentialAction, ParserError> {
if self.parse_keyword("RESTRICT") {
Ok(ReferentialAction::Restrict)
} else if self.parse_keyword("CASCADE") {
Ok(ReferentialAction::Cascade)
} else if self.parse_keywords(vec!["SET", "NULL"]) {
Ok(ReferentialAction::SetNull)
} else if self.parse_keywords(vec!["NO", "ACTION"]) {
Ok(ReferentialAction::NoAction)
} else if self.parse_keywords(vec!["SET", "DEFAULT"]) {
Ok(ReferentialAction::SetDefault)
} else {
self.expected(
"one of RESTRICT, CASCADE, SET NULL, NO ACTION or SET DEFAULT",
self.peek_token(),
)
}
}
pub fn parse_optional_table_constraint(
&mut self,
) -> Result<Option<TableConstraint>, ParserError> {

View file

@ -893,7 +893,9 @@ fn parse_create_table() {
lat DOUBLE NULL,\
lng DOUBLE,
constrained INT NULL CONSTRAINT pkey PRIMARY KEY NOT NULL UNIQUE CHECK (constrained > 0),
ref INT REFERENCES othertable (a, b))";
ref INT REFERENCES othertable (a, b),\
ref2 INT references othertable2 on delete cascade on update no action\
)";
let ast = one_statement_parses_to(
sql,
"CREATE TABLE uk_cities (\
@ -901,7 +903,8 @@ fn parse_create_table() {
lat double NULL, \
lng double, \
constrained int NULL CONSTRAINT pkey PRIMARY KEY NOT NULL UNIQUE CHECK (constrained > 0), \
ref int REFERENCES othertable (a, b))",
ref int REFERENCES othertable (a, b), \
ref2 int REFERENCES othertable2 ON DELETE CASCADE ON UPDATE NO ACTION)",
);
match ast {
Statement::CreateTable {
@ -978,8 +981,24 @@ fn parse_create_table() {
option: ColumnOption::ForeignKey {
foreign_table: ObjectName(vec!["othertable".into()]),
referred_columns: vec!["a".into(), "b".into(),],
on_delete: None,
on_update: None,
}
}]
},
ColumnDef {
name: "ref2".into(),
data_type: DataType::Int,
collation: None,
options: vec![ColumnOptionDef {
name: None,
option: ColumnOption::ForeignKey {
foreign_table: ObjectName(vec!["othertable2".into()]),
referred_columns: vec![],
on_delete: Some(ReferentialAction::Cascade),
on_update: Some(ReferentialAction::NoAction),
}
},]
}
]
);
@ -996,6 +1015,32 @@ fn parse_create_table() {
.contains("Expected column option, found: GARBAGE"));
}
#[test]
fn parse_create_table_with_multiple_on_delete_fails() {
parse_sql_statements(
"\
create table X (\
y_id int references Y (id) \
on delete cascade on update cascade on delete no action\
)",
)
.expect_err("should have failed");
}
#[test]
fn parse_create_table_with_on_delete_on_update_2in_any_order() -> Result<(), ParserError> {
let sql = |options: &str| -> String {
format!("create table X (y_id int references Y (id) {})", options)
};
parse_sql_statements(&sql("on update cascade on delete no action"))?;
parse_sql_statements(&sql("on delete cascade on update cascade"))?;
parse_sql_statements(&sql("on update no action"))?;
parse_sql_statements(&sql("on delete restrict"))?;
Ok(())
}
#[test]
fn parse_create_table_with_options() {
let sql = "CREATE TABLE t (c int) WITH (foo = 'bar', a = 123)";