use anyhow::{Result, ensure};
const RUST_KEYWORDS: &[&str] = &[
"as", "async", "await", "break", "const", "continue", "crate", "dyn", "else", "enum", "extern",
"false", "fn", "for", "if", "impl", "in", "let", "loop", "match", "mod", "move", "mut", "pub",
"ref", "return", "self", "Self", "static", "struct", "super", "trait", "true", "type",
"unsafe", "use", "where", "while", "abstract", "become", "box", "do", "final", "gen", "macro",
"override", "priv", "try", "typeof", "unsized", "virtual", "yield", "union",
];
const POSTGRES_RESERVED: &[&str] = &[
"all",
"analyse",
"analyze",
"and",
"any",
"array",
"as",
"asc",
"asymmetric",
"authorization",
"binary",
"both",
"case",
"cast",
"check",
"collate",
"collation",
"column",
"concurrently",
"constraint",
"create",
"cross",
"current_catalog",
"current_date",
"current_role",
"current_schema",
"current_time",
"current_timestamp",
"current_user",
"default",
"deferrable",
"desc",
"distinct",
"do",
"else",
"end",
"except",
"false",
"fetch",
"for",
"foreign",
"freeze",
"from",
"full",
"grant",
"group",
"having",
"ilike",
"in",
"initially",
"inner",
"intersect",
"into",
"is",
"isnull",
"join",
"lateral",
"leading",
"left",
"like",
"limit",
"localtime",
"localtimestamp",
"natural",
"not",
"notnull",
"null",
"offset",
"on",
"only",
"or",
"order",
"outer",
"overlaps",
"placing",
"primary",
"references",
"returning",
"right",
"select",
"session_user",
"similar",
"some",
"symmetric",
"system_user",
"table",
"tablesample",
"then",
"to",
"trailing",
"true",
"union",
"unique",
"user",
"using",
"variadic",
"verbose",
"when",
"where",
"window",
"with",
];
const SQLITE_RESERVED: &[&str] = &[
"add",
"all",
"alter",
"and",
"as",
"autoincrement",
"between",
"case",
"check",
"collate",
"commit",
"constraint",
"create",
"default",
"deferrable",
"delete",
"distinct",
"drop",
"else",
"escape",
"except",
"exists",
"foreign",
"from",
"group",
"having",
"if",
"in",
"index",
"insert",
"intersect",
"into",
"is",
"isnull",
"join",
"limit",
"not",
"nothing",
"notnull",
"null",
"on",
"or",
"order",
"primary",
"references",
"returning",
"select",
"set",
"table",
"then",
"to",
"transaction",
"union",
"unique",
"update",
"using",
"values",
"when",
"where",
];
const MYSQL_RESERVED: &[&str] = &[
"accessible",
"add",
"all",
"alter",
"analyze",
"and",
"as",
"asc",
"asensitive",
"before",
"between",
"bigint",
"binary",
"blob",
"both",
"by",
"call",
"cascade",
"case",
"cast",
"change",
"char",
"character",
"check",
"collate",
"column",
"condition",
"constraint",
"continue",
"convert",
"create",
"cross",
"cube",
"cume_dist",
"current_date",
"current_time",
"current_timestamp",
"current_user",
"cursor",
"database",
"databases",
"day_hour",
"day_microsecond",
"day_minute",
"day_second",
"dec",
"decimal",
"declare",
"default",
"delayed",
"delete",
"dense_rank",
"desc",
"describe",
"deterministic",
"distinct",
"distinctrow",
"div",
"double",
"drop",
"dual",
"each",
"else",
"elseif",
"empty",
"enclosed",
"escaped",
"except",
"exists",
"exit",
"explain",
"extract",
"false",
"fetch",
"first_value",
"float",
"float4",
"float8",
"for",
"force",
"foreign",
"from",
"fulltext",
"function",
"generated",
"get",
"grant",
"group",
"grouping",
"groups",
"having",
"high_priority",
"hour_microsecond",
"hour_minute",
"hour_second",
"if",
"ignore",
"in",
"index",
"infile",
"inner",
"inout",
"insensitive",
"insert",
"int",
"int1",
"int2",
"int3",
"int4",
"int8",
"integer",
"intersect",
"interval",
"into",
"io_after_gtids",
"io_before_gtids",
"is",
"iterate",
"join",
"json_table",
"key",
"keys",
"kill",
"lag",
"last_value",
"lateral",
"lead",
"leading",
"leave",
"left",
"like",
"limit",
"linear",
"lines",
"load",
"localtime",
"localtimestamp",
"lock",
"long",
"longblob",
"longtext",
"loop",
"low_priority",
"manual",
"master_bind",
"master_ssl_verify_server_cert",
"match",
"maxvalue",
"mediumblob",
"mediumint",
"mediumtext",
"middleint",
"minute_microsecond",
"minute_second",
"mod",
"modifies",
"natural",
"no_write_to_binlog",
"not",
"nth_value",
"ntile",
"null",
"numeric",
"of",
"on",
"optimize",
"optimizer_costs",
"option",
"optionally",
"or",
"order",
"out",
"outer",
"outfile",
"over",
"parallel",
"partition",
"percent_rank",
"position",
"precision",
"primary",
"procedure",
"purge",
"qualify",
"range",
"rank",
"read",
"read_write",
"reads",
"real",
"recursive",
"references",
"regexp",
"release",
"rename",
"repeat",
"replace",
"require",
"resignal",
"restrict",
"return",
"revoke",
"right",
"rlike",
"row",
"row_number",
"rows",
"schema",
"schemas",
"second_microsecond",
"select",
"sensitive",
"separator",
"set",
"show",
"signal",
"smallint",
"spatial",
"specific",
"sql",
"sql_big_result",
"sql_calc_found_rows",
"sql_small_result",
"sqlexception",
"sqlstate",
"sqlwarning",
"ssl",
"starting",
"stored",
"straight_join",
"substring",
"system",
"table",
"tablesample",
"terminated",
"then",
"tinyblob",
"tinyint",
"tinytext",
"to",
"trailing",
"trigger",
"trim",
"true",
"undo",
"union",
"unique",
"unlock",
"unsigned",
"update",
"usage",
"use",
"using",
"utc_date",
"utc_time",
"utc_timestamp",
"values",
"varbinary",
"varchar",
"varcharacter",
"varying",
"virtual",
"when",
"where",
"while",
"window",
"with",
"write",
"xor",
"year_month",
"zerofill",
];
pub fn validate_identifier(name: &str, kind: &str) -> Result<()> {
ensure!(
name != "_"
&& !name.is_empty()
&& name
.bytes()
.next()
.is_some_and(|b| b.is_ascii_lowercase() || b == b'_')
&& name
.bytes()
.all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'_'),
"invalid {kind} `{name}`: use a lowercase ASCII snake_case identifier, starting with a letter or underscore"
);
ensure!(
name.len() <= 63,
"invalid {kind} `{name}`: maximum identifier length is 63 bytes"
);
ensure!(
!RUST_KEYWORDS.contains(&name),
"invalid {kind} `{name}`: reserved Rust keyword"
);
Ok(())
}
pub fn validate_sql_identifier_for_dialect(
name: &str,
kind: &str,
dialect: crate::Dialect,
) -> Result<()> {
validate_identifier(name, kind)?;
let reserved = match dialect {
crate::Dialect::Postgres => POSTGRES_RESERVED,
crate::Dialect::Sqlite => SQLITE_RESERVED,
crate::Dialect::MySql => MYSQL_RESERVED,
};
ensure!(
!reserved.contains(&name),
"invalid {kind} `{name}`: reserved SQL keyword for {dialect:?}; choose another name"
);
Ok(())
}
pub fn validate_column(name: &str) -> Result<()> {
validate_identifier(name, "field")?;
ensure!(
!["id", "created_at", "updated_at"].contains(&name),
"invalid field `{name}`: automatically generated by Gize"
);
Ok(())
}
pub fn validate_model_name(name: &str) -> Result<()> {
ensure!(
!RUST_KEYWORDS.contains(&name),
"invalid model `{name}`: reserved Rust keyword"
);
ensure!(
name.bytes().next().is_some_and(|b| b.is_ascii_uppercase())
&& name.bytes().all(|b| b.is_ascii_alphanumeric()),
"invalid model `{name}`: use an ASCII PascalCase name, such as BlogPost"
);
ensure!(
![
"String",
"Vec",
"Result",
"Error",
"State",
"AppState",
"StatusCode",
"Json",
"Serialize",
"Deserialize"
]
.contains(&name),
"invalid model `{name}`: conflicts with a type used by generated code"
);
validate_identifier(&crate::naming::table_name(name), "generated table")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Dialect;
#[test]
fn reserved_words_follow_the_selected_database() {
for (dialect, rejected, accepted) in [
(
Dialect::Postgres,
&[
"array", "only", "cast", "analyze", "binary", "user", "order",
][..],
&["key", "index", "database", "values", "rows", "range"][..],
),
(
Dialect::MySql,
&[
"range", "interval", "schema", "read", "lock", "show", "rank", "rows",
][..],
&["user", "returning", "offset", "array", "only"][..],
),
(
Dialect::Sqlite,
&["index", "values", "order", "transaction", "commit"][..],
&["key", "database", "user", "rows", "range", "offset"][..],
),
] {
for name in rejected {
let error = validate_sql_identifier_for_dialect(name, "field", dialect)
.unwrap_err()
.to_string();
assert!(
error.contains("reserved SQL keyword"),
"{dialect:?}: {error}"
);
}
for name in accepted {
validate_sql_identifier_for_dialect(name, "field", dialect)
.unwrap_or_else(|e| panic!("{dialect:?}: {e}"));
}
}
}
#[test]
fn reserved_lists_are_sorted_lowercase_identifiers() {
for list in [POSTGRES_RESERVED, SQLITE_RESERVED, MYSQL_RESERVED] {
assert!(list.windows(2).all(|w| w[0] < w[1]));
assert!(list.iter().all(|w| validate_identifier(w, "keyword").is_ok()
|| RUST_KEYWORDS.contains(w)));
}
}
}