Add support for constraint characteristics clause (#1099)

This commit is contained in:
Daniel Imfeld 2024-01-24 09:26:19 -10:00 committed by GitHub
parent 1fb9f3efdf
commit c86508bae5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 480 additions and 22 deletions

View file

@ -2555,7 +2555,10 @@ fn parse_create_table() {
},
ColumnOptionDef {
name: Some("pkey".into()),
option: ColumnOption::Unique { is_primary: true },
option: ColumnOption::Unique {
is_primary: true,
characteristics: None
},
},
ColumnOptionDef {
name: None,
@ -2563,7 +2566,10 @@ fn parse_create_table() {
},
ColumnOptionDef {
name: None,
option: ColumnOption::Unique { is_primary: false },
option: ColumnOption::Unique {
is_primary: false,
characteristics: None
},
},
ColumnOptionDef {
name: None,
@ -2582,6 +2588,7 @@ fn parse_create_table() {
referred_columns: vec!["a".into(), "b".into()],
on_delete: None,
on_update: None,
characteristics: None,
},
}],
},
@ -2596,6 +2603,7 @@ fn parse_create_table() {
referred_columns: vec![],
on_delete: Some(ReferentialAction::Cascade),
on_update: Some(ReferentialAction::NoAction),
characteristics: None,
},
},],
},
@ -2611,6 +2619,7 @@ fn parse_create_table() {
referred_columns: vec!["lat".into()],
on_delete: Some(ReferentialAction::Restrict),
on_update: None,
characteristics: None,
},
TableConstraint::ForeignKey {
name: Some("fkey2".into()),
@ -2619,6 +2628,7 @@ fn parse_create_table() {
referred_columns: vec!["lat".into()],
on_delete: Some(ReferentialAction::NoAction),
on_update: Some(ReferentialAction::Restrict),
characteristics: None,
},
TableConstraint::ForeignKey {
name: None,
@ -2627,6 +2637,7 @@ fn parse_create_table() {
referred_columns: vec!["lat".into()],
on_delete: Some(ReferentialAction::Cascade),
on_update: Some(ReferentialAction::SetDefault),
characteristics: None,
},
TableConstraint::ForeignKey {
name: None,
@ -2635,6 +2646,7 @@ fn parse_create_table() {
referred_columns: vec!["longitude".into()],
on_delete: None,
on_update: Some(ReferentialAction::SetNull),
characteristics: None,
},
]
);
@ -2656,6 +2668,269 @@ fn parse_create_table() {
.contains("Expected constraint details after CONSTRAINT <name>"));
}
#[test]
fn parse_create_table_with_constraint_characteristics() {
let sql = "CREATE TABLE uk_cities (\
name VARCHAR(100) NOT NULL,\
lat DOUBLE NULL,\
lng DOUBLE,
constraint fkey foreign key (lat) references othertable3 (lat) on delete restrict deferrable initially deferred,\
constraint fkey2 foreign key (lat) references othertable4(lat) on delete no action on update restrict deferrable initially immediate, \
foreign key (lat) references othertable4(lat) on update set default on delete cascade not deferrable initially deferred not enforced, \
FOREIGN KEY (lng) REFERENCES othertable4 (longitude) ON UPDATE SET NULL enforced not deferrable initially immediate
)";
let ast = one_statement_parses_to(
sql,
"CREATE TABLE uk_cities (\
name VARCHAR(100) NOT NULL, \
lat DOUBLE NULL, \
lng DOUBLE, \
CONSTRAINT fkey FOREIGN KEY (lat) REFERENCES othertable3(lat) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED, \
CONSTRAINT fkey2 FOREIGN KEY (lat) REFERENCES othertable4(lat) ON DELETE NO ACTION ON UPDATE RESTRICT DEFERRABLE INITIALLY IMMEDIATE, \
FOREIGN KEY (lat) REFERENCES othertable4(lat) ON DELETE CASCADE ON UPDATE SET DEFAULT NOT DEFERRABLE INITIALLY DEFERRED NOT ENFORCED, \
FOREIGN KEY (lng) REFERENCES othertable4(longitude) ON UPDATE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE ENFORCED)",
);
match ast {
Statement::CreateTable {
name,
columns,
constraints,
with_options,
if_not_exists: false,
external: false,
file_format: None,
location: None,
..
} => {
assert_eq!("uk_cities", name.to_string());
assert_eq!(
columns,
vec![
ColumnDef {
name: "name".into(),
data_type: DataType::Varchar(Some(CharacterLength::IntegerLength {
length: 100,
unit: None,
})),
collation: None,
options: vec![ColumnOptionDef {
name: None,
option: ColumnOption::NotNull,
}],
},
ColumnDef {
name: "lat".into(),
data_type: DataType::Double,
collation: None,
options: vec![ColumnOptionDef {
name: None,
option: ColumnOption::Null,
}],
},
ColumnDef {
name: "lng".into(),
data_type: DataType::Double,
collation: None,
options: vec![],
},
]
);
assert_eq!(
constraints,
vec![
TableConstraint::ForeignKey {
name: Some("fkey".into()),
columns: vec!["lat".into()],
foreign_table: ObjectName(vec!["othertable3".into()]),
referred_columns: vec!["lat".into()],
on_delete: Some(ReferentialAction::Restrict),
on_update: None,
characteristics: Some(ConstraintCharacteristics {
deferrable: Some(true),
initially: Some(DeferrableInitial::Deferred),
enforced: None
}),
},
TableConstraint::ForeignKey {
name: Some("fkey2".into()),
columns: vec!["lat".into()],
foreign_table: ObjectName(vec!["othertable4".into()]),
referred_columns: vec!["lat".into()],
on_delete: Some(ReferentialAction::NoAction),
on_update: Some(ReferentialAction::Restrict),
characteristics: Some(ConstraintCharacteristics {
deferrable: Some(true),
initially: Some(DeferrableInitial::Immediate),
enforced: None,
}),
},
TableConstraint::ForeignKey {
name: None,
columns: vec!["lat".into()],
foreign_table: ObjectName(vec!["othertable4".into()]),
referred_columns: vec!["lat".into()],
on_delete: Some(ReferentialAction::Cascade),
on_update: Some(ReferentialAction::SetDefault),
characteristics: Some(ConstraintCharacteristics {
deferrable: Some(false),
initially: Some(DeferrableInitial::Deferred),
enforced: Some(false),
}),
},
TableConstraint::ForeignKey {
name: None,
columns: vec!["lng".into()],
foreign_table: ObjectName(vec!["othertable4".into()]),
referred_columns: vec!["longitude".into()],
on_delete: None,
on_update: Some(ReferentialAction::SetNull),
characteristics: Some(ConstraintCharacteristics {
deferrable: Some(false),
initially: Some(DeferrableInitial::Immediate),
enforced: Some(true),
}),
},
]
);
assert_eq!(with_options, vec![]);
}
_ => unreachable!(),
}
let res = parse_sql_statements("CREATE TABLE t (
a int NOT NULL,
FOREIGN KEY (a) REFERENCES othertable4(a) ON DELETE CASCADE ON UPDATE SET DEFAULT DEFERRABLE INITIALLY IMMEDIATE NOT DEFERRABLE, \
)");
assert!(res
.unwrap_err()
.to_string()
.contains("Expected \',\' or \')\' after column definition, found: NOT"));
let res = parse_sql_statements("CREATE TABLE t (
a int NOT NULL,
FOREIGN KEY (a) REFERENCES othertable4(a) ON DELETE CASCADE ON UPDATE SET DEFAULT NOT ENFORCED INITIALLY DEFERRED ENFORCED, \
)");
assert!(res
.unwrap_err()
.to_string()
.contains("Expected \',\' or \')\' after column definition, found: ENFORCED"));
let res = parse_sql_statements("CREATE TABLE t (
a int NOT NULL,
FOREIGN KEY (lat) REFERENCES othertable4(lat) ON DELETE CASCADE ON UPDATE SET DEFAULT INITIALLY DEFERRED INITIALLY IMMEDIATE, \
)");
assert!(res
.unwrap_err()
.to_string()
.contains("Expected \',\' or \')\' after column definition, found: INITIALLY"));
}
#[test]
fn parse_create_table_column_constraint_characteristics() {
fn test_combo(
syntax: &str,
deferrable: Option<bool>,
initially: Option<DeferrableInitial>,
enforced: Option<bool>,
) {
let message = if syntax.is_empty() {
"No clause"
} else {
syntax
};
let sql = format!("CREATE TABLE t (a int UNIQUE {})", syntax);
let expected_clause = if syntax.is_empty() {
String::new()
} else {
format!(" {syntax}")
};
let expected = format!("CREATE TABLE t (a INT UNIQUE{})", expected_clause);
let ast = one_statement_parses_to(&sql, &expected);
let expected_value = if deferrable.is_some() || initially.is_some() || enforced.is_some() {
Some(ConstraintCharacteristics {
deferrable,
initially,
enforced,
})
} else {
None
};
match ast {
Statement::CreateTable { columns, .. } => {
assert_eq!(
columns,
vec![ColumnDef {
name: "a".into(),
data_type: DataType::Int(None),
collation: None,
options: vec![ColumnOptionDef {
name: None,
option: ColumnOption::Unique {
is_primary: false,
characteristics: expected_value
}
}]
}],
"{message}"
)
}
_ => unreachable!(),
}
}
for deferrable in [None, Some(true), Some(false)] {
for initially in [
None,
Some(DeferrableInitial::Immediate),
Some(DeferrableInitial::Deferred),
] {
for enforced in [None, Some(true), Some(false)] {
let deferrable_text =
deferrable.map(|d| if d { "DEFERRABLE" } else { "NOT DEFERRABLE" });
let initially_text = initially.map(|i| match i {
DeferrableInitial::Immediate => "INITIALLY IMMEDIATE",
DeferrableInitial::Deferred => "INITIALLY DEFERRED",
});
let enforced_text = enforced.map(|e| if e { "ENFORCED" } else { "NOT ENFORCED" });
let syntax = [deferrable_text, initially_text, enforced_text]
.into_iter()
.flatten()
.collect::<Vec<_>>()
.join(" ");
test_combo(&syntax, deferrable, initially, enforced);
}
}
}
let res = parse_sql_statements(
"CREATE TABLE t (a int NOT NULL UNIQUE DEFERRABLE INITIALLY BADVALUE)",
);
assert!(res
.unwrap_err()
.to_string()
.contains("Expected one of DEFERRED or IMMEDIATE, found: BADVALUE"));
let res = parse_sql_statements(
"CREATE TABLE t (a int NOT NULL UNIQUE INITIALLY IMMEDIATE DEFERRABLE INITIALLY DEFERRED)",
);
res.expect_err("INITIALLY {IMMEDIATE|DEFERRED} setting should only be allowed once");
let res = parse_sql_statements(
"CREATE TABLE t (a int NOT NULL UNIQUE DEFERRABLE INITIALLY DEFERRED NOT DEFERRABLE)",
);
res.expect_err("[NOT] DEFERRABLE setting should only be allowed once");
let res = parse_sql_statements(
"CREATE TABLE t (a int NOT NULL UNIQUE DEFERRABLE INITIALLY DEFERRED ENFORCED NOT ENFORCED)",
);
res.expect_err("[NOT] ENFORCED setting should only be allowed once");
}
#[test]
fn parse_create_table_hive_array() {
// Parsing [] type arrays does not work in MsSql since [ is used in is_delimited_identifier_start