Parse CHECK table constraints.

This commit is contained in:
Sebastian Jeltsch
2025-03-26 16:17:32 +01:00
parent 448c44aa2f
commit 682be1aa39
6 changed files with 122 additions and 99 deletions
+3
View File
@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type Check = { name: string | null, expr: string, };
+2 -1
View File
@@ -1,6 +1,7 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { Check } from "./Check";
import type { Column } from "./Column";
import type { ForeignKey } from "./ForeignKey";
import type { UniqueConstraint } from "./UniqueConstraint";
export type Table = { name: string, strict: boolean, columns: Array<Column>, foreign_keys: Array<ForeignKey>, unique: Array<UniqueConstraint>, virtual_table: boolean, temporary: boolean, };
export type Table = { name: string, strict: boolean, columns: Array<Column>, foreign_keys: Array<ForeignKey>, unique: Array<UniqueConstraint>, checks: Array<Check>, virtual_table: boolean, temporary: boolean, };
@@ -107,6 +107,7 @@ export function CreateAlterTableForm(props: {
// Table constraints: https://www.sqlite.org/syntax/table-constraint.html
unique: [],
foreign_keys: [],
checks: [],
virtual_table: false,
temporary: false,
} as Table),
@@ -149,6 +149,7 @@ mod tests {
],
foreign_keys: vec![],
unique: vec![],
checks: vec![],
virtual_table: false,
temporary: false,
},
@@ -161,6 +161,7 @@ mod tests {
}],
foreign_keys: vec![],
unique: vec![],
checks: vec![],
virtual_table: false,
temporary: false,
},
+114 -98
View File
@@ -72,6 +72,22 @@ impl ForeignKey {
}
}
#[derive(Clone, Debug, Serialize, Deserialize, TS, PartialEq)]
pub struct Check {
pub name: Option<String>,
pub expr: String,
}
impl Check {
fn to_fragment(&self) -> String {
return if let Some(ref name) = self.name {
format!("CONSTRAINT '{name}' CHECK({})", self.expr)
} else {
format!("CHECK({})", self.expr)
};
}
}
// TODO: Our table constraints are generally very incomplete:
// https://www.sqlite.org/syntax/table-constraint.html.
#[derive(Clone, Debug, Serialize, Deserialize, TS, PartialEq)]
@@ -427,6 +443,7 @@ pub struct Table {
// column-level constraints a.k.a. Column::options.
pub foreign_keys: Vec<ForeignKey>,
pub unique: Vec<UniqueConstraint>,
pub checks: Vec<Check>,
// NOTE: consider parsing "CREATE VIRTUAL TABLE" into a separate struct.
pub virtual_table: bool,
@@ -442,28 +459,16 @@ impl Table {
let mut column_defs_and_table_constraints: Vec<String> = vec![];
let column_defs = self
.columns
.iter()
.map(|c| c.to_fragment())
.collect::<Vec<_>>();
column_defs_and_table_constraints.extend(column_defs);
column_defs_and_table_constraints.extend(self.columns.iter().map(|c| c.to_fragment()));
// Example: UNIQUE (email),
let unique_table_constraints = self
.unique
.iter()
.map(|unique| unique.to_fragment())
.collect::<Vec<_>>();
column_defs_and_table_constraints.extend(unique_table_constraints);
column_defs_and_table_constraints.extend(self.unique.iter().map(|unique| unique.to_fragment()));
// Example: FOREIGN KEY(user_id) REFERENCES table(id) ON DELETE CASCADE
let fk_table_constraints = self
.foreign_keys
.iter()
.map(|fk| fk.to_fragment())
.collect::<Vec<_>>();
column_defs_and_table_constraints.extend(fk_table_constraints);
column_defs_and_table_constraints.extend(self.foreign_keys.iter().map(|fk| fk.to_fragment()));
// Example: CHECK('age' > 0)
column_defs_and_table_constraints.extend(self.checks.iter().map(|fk| fk.to_fragment()));
return format!(
"CREATE{temporary} TABLE '{name}' ({col_defs_and_constraints}){strict}",
@@ -565,6 +570,7 @@ impl TryFrom<sqlite3_parser::ast::Stmt> for Table {
let mut foreign_keys: Vec<ForeignKey> = vec![];
let mut unique: Vec<UniqueConstraint> = vec![];
let mut checks: Vec<Check> = vec![];
for constraint in constraints.unwrap_or_default() {
match constraint.constraint {
@@ -584,23 +590,21 @@ impl TryFrom<sqlite3_parser::ast::Stmt> for Table {
columns,
conflict_clause,
} => {
let name = constraint.name.map(|name| unquote_name(name.clone()));
unique.push(UniqueConstraint {
name,
columns: columns
.iter()
.map(|c| unquote_expr(c.expr.clone()))
.collect(),
name: constraint.name.map(unquote_name),
columns: columns.into_iter().map(|c| unquote_expr(c.expr)).collect(),
conflict_clause: conflict_clause.map(|c| c.into()),
});
}
TableConstraint::Check(expr) => {
checks.push(Check {
name: constraint.name.map(unquote_name),
expr: expr.to_string(),
});
}
TableConstraint::PrimaryKey { .. } => {
log::warn!("PK table constraint not implemented. Use column constraints.");
}
TableConstraint::Check(expr) => {
log::warn!("CHECK table constraint not yet implemented: {expr:?}");
}
}
}
@@ -641,6 +645,7 @@ impl TryFrom<sqlite3_parser::ast::Stmt> for Table {
columns,
foreign_keys,
unique,
checks,
virtual_table: false,
temporary,
})
@@ -655,6 +660,7 @@ impl TryFrom<sqlite3_parser::ast::Stmt> for Table {
columns: vec![],
foreign_keys: vec![],
unique: vec![],
checks: vec![],
virtual_table: true,
temporary: false,
}),
@@ -845,68 +851,6 @@ struct ColumnMapping {
referred_column: Option<ReferredColumn>,
}
fn build_foreign_key(
name: Option<Name>,
columns: Option<Vec<IndexedColumn>>,
clause: ForeignKeyClause,
deref_clause: Option<DeferSubclause>,
) -> ForeignKey {
if let Some(ref clause) = deref_clause {
// TOOD: Parse DEFERRABLE.
log::warn!("Unsupported DEFERRABLE in FK clause: {clause:?}");
}
let (on_update, on_delete) = unparse_fk_trigger(&clause.args);
return ForeignKey {
name: name.map(unquote_name),
foreign_table: unquote_name(clause.tbl_name.clone()),
columns: columns
.unwrap_or_default()
.into_iter()
.map(|c| unquote_name(c.col_name))
.collect(),
referred_columns: clause
.columns
.unwrap_or_default()
.into_iter()
.map(|c| unquote_name(c.col_name))
.collect(),
on_update,
on_delete,
};
}
fn unparse_fk_trigger(
args: &Vec<sqlite3_parser::ast::RefArg>,
) -> (Option<ReferentialAction>, Option<ReferentialAction>) {
use sqlite3_parser::ast::RefArg;
let mut on_update: Option<ReferentialAction> = None;
let mut on_delete: Option<ReferentialAction> = None;
for arg in args {
match arg {
RefArg::OnDelete(action) => {
on_delete = Some((*action).into());
}
RefArg::OnUpdate(action) => {
on_update = Some((*action).into());
}
RefArg::OnInsert(action) => {
log::error!("Unexpected ON INSERT in FK clause: {action:?}");
}
RefArg::Match(name) => {
// SQL supports FK MATCH clause, which is *not* supported by sqlite:
// https://www.sqlite.org/foreignkeys.html#fk_unsupported
log::warn!("Unsupported MATCH in FK clause: {name:?}");
}
}
}
return (on_update, on_delete);
}
fn try_extract_column_mapping(
select: sqlite3_parser::ast::Select,
tables: &[Table],
@@ -1134,6 +1078,68 @@ fn try_extract_column_mapping(
return Ok(Some(mapping));
}
fn build_foreign_key(
name: Option<Name>,
columns: Option<Vec<IndexedColumn>>,
clause: ForeignKeyClause,
deref_clause: Option<DeferSubclause>,
) -> ForeignKey {
if let Some(ref clause) = deref_clause {
// TOOD: Parse DEFERRABLE.
log::warn!("Unsupported DEFERRABLE in FK clause: {clause:?}");
}
let (on_update, on_delete) = unparse_fk_trigger(&clause.args);
return ForeignKey {
name: name.map(unquote_name),
foreign_table: unquote_name(clause.tbl_name.clone()),
columns: columns
.unwrap_or_default()
.into_iter()
.map(|c| unquote_name(c.col_name))
.collect(),
referred_columns: clause
.columns
.unwrap_or_default()
.into_iter()
.map(|c| unquote_name(c.col_name))
.collect(),
on_update,
on_delete,
};
}
fn unparse_fk_trigger(
args: &Vec<sqlite3_parser::ast::RefArg>,
) -> (Option<ReferentialAction>, Option<ReferentialAction>) {
use sqlite3_parser::ast::RefArg;
let mut on_update: Option<ReferentialAction> = None;
let mut on_delete: Option<ReferentialAction> = None;
for arg in args {
match arg {
RefArg::OnDelete(action) => {
on_delete = Some((*action).into());
}
RefArg::OnUpdate(action) => {
on_update = Some((*action).into());
}
RefArg::OnInsert(action) => {
log::error!("Unexpected ON INSERT in FK clause: {action:?}");
}
RefArg::Match(name) => {
// SQL supports FK MATCH clause, which is *not* supported by sqlite:
// https://www.sqlite.org/foreignkeys.html#fk_unsupported
log::warn!("Unsupported MATCH in FK clause: {name:?}");
}
}
}
return (on_update, on_delete);
}
#[inline]
pub(crate) fn quote(column_names: &[String]) -> String {
let mut s = String::new();
@@ -1251,7 +1257,7 @@ mod tests {
email TEXT NOT NULL,
email_visibility INTEGER DEFAULT FALSE NOT NULL,
username TEXT UNIQUE ON CONFLICT ABORT,
age INTEGER,
age INTEGER CHECK(age >= 0),
double_age INTEGER GENERATED ALWAYS AS (2 * 'age') VIRTUAL,
triple_age INTEGER AS (3 * age) STORED,
gen_text TEXT AS ('') VIRTUAL,
@@ -1260,8 +1266,8 @@ mod tests {
UNIQUE (email),
-- optional constraint name:
CONSTRAINT `unique` UNIQUE ([index]) ON CONFLICT FAIL,
CHECK(username != ''),
FOREIGN KEY(user_id) REFERENCES 'table'('index') ON DELETE CASCADE
FOREIGN KEY(user_id) REFERENCES 'table'('index') ON DELETE CASCADE,
CONSTRAINT `check` CHECK(username != '')
) STRICT;
"#
);
@@ -1286,11 +1292,19 @@ mod tests {
let table2: Table = statement2.clone().try_into().unwrap();
// let sql2 = StmtFormatter(statement2.clone()).to_string();
// let sql1 = StmtFormatter(statement1.clone()).to_string();
// // NOTE: Ideally we'd just compare the parsed sqlite3_parser ASTs, however it doesn't
// properly // parse out escape characters, so `statement1` and `statement2` will be escaped
// differently. assert_eq!(statement1, statement2, "Got: {sql2}\nExpected: {sql1}");
// NOTE: Ideally we'd just compare the parsed sqlite3_parser ASTs, however it doesn't properly
// parse out escape characters, so `statement1` and `statement2` will be escaped differently.
// So we're matching on strings instead with all quoting removed.
// assert_eq!(statement1, statement2, "Got: {sql2}\nExpected: {sql1}");
let pattern = ['\'', '"', '[', ']', '`'];
let sql2 = StmtFormatter(statement2.clone())
.to_string()
.replace(&pattern, "");
let sql1 = StmtFormatter(statement1.clone())
.to_string()
.replace(&pattern, "");
assert_eq!(sql2, sql1, "Got: {sql2}\nExpected: {sql1}");
assert_eq!(table1, table2, "generated stmt: {sql}");
}
@@ -1376,6 +1390,7 @@ mod tests {
],
foreign_keys: vec![],
unique: vec![],
checks: vec![],
virtual_table: false,
temporary: false,
},
@@ -1409,6 +1424,7 @@ mod tests {
],
foreign_keys: vec![],
unique: vec![],
checks: vec![],
virtual_table: false,
temporary: false,
},