1use anyhow::{Result, ensure};
3
4const RUST_KEYWORDS: &[&str] = &[
5 "as", "async", "await", "break", "const", "continue", "crate", "dyn", "else", "enum", "extern",
6 "false", "fn", "for", "if", "impl", "in", "let", "loop", "match", "mod", "move", "mut", "pub",
7 "ref", "return", "self", "Self", "static", "struct", "super", "trait", "true", "type",
8 "unsafe", "use", "where", "while", "abstract", "become", "box", "do", "final", "gen", "macro",
9 "override", "priv", "try", "typeof", "unsized", "virtual", "yield", "union",
10];
11
12const POSTGRES_RESERVED: &[&str] = &[
19 "all",
20 "analyse",
21 "analyze",
22 "and",
23 "any",
24 "array",
25 "as",
26 "asc",
27 "asymmetric",
28 "authorization",
29 "binary",
30 "both",
31 "case",
32 "cast",
33 "check",
34 "collate",
35 "collation",
36 "column",
37 "concurrently",
38 "constraint",
39 "create",
40 "cross",
41 "current_catalog",
42 "current_date",
43 "current_role",
44 "current_schema",
45 "current_time",
46 "current_timestamp",
47 "current_user",
48 "default",
49 "deferrable",
50 "desc",
51 "distinct",
52 "do",
53 "else",
54 "end",
55 "except",
56 "false",
57 "fetch",
58 "for",
59 "foreign",
60 "freeze",
61 "from",
62 "full",
63 "grant",
64 "group",
65 "having",
66 "ilike",
67 "in",
68 "initially",
69 "inner",
70 "intersect",
71 "into",
72 "is",
73 "isnull",
74 "join",
75 "lateral",
76 "leading",
77 "left",
78 "like",
79 "limit",
80 "localtime",
81 "localtimestamp",
82 "natural",
83 "not",
84 "notnull",
85 "null",
86 "offset",
87 "on",
88 "only",
89 "or",
90 "order",
91 "outer",
92 "overlaps",
93 "placing",
94 "primary",
95 "references",
96 "returning",
97 "right",
98 "select",
99 "session_user",
100 "similar",
101 "some",
102 "symmetric",
103 "system_user",
104 "table",
105 "tablesample",
106 "then",
107 "to",
108 "trailing",
109 "true",
110 "union",
111 "unique",
112 "user",
113 "using",
114 "variadic",
115 "verbose",
116 "when",
117 "where",
118 "window",
119 "with",
120];
121const SQLITE_RESERVED: &[&str] = &[
122 "add",
123 "all",
124 "alter",
125 "and",
126 "as",
127 "autoincrement",
128 "between",
129 "case",
130 "check",
131 "collate",
132 "commit",
133 "constraint",
134 "create",
135 "default",
136 "deferrable",
137 "delete",
138 "distinct",
139 "drop",
140 "else",
141 "escape",
142 "except",
143 "exists",
144 "foreign",
145 "from",
146 "group",
147 "having",
148 "if",
149 "in",
150 "index",
151 "insert",
152 "intersect",
153 "into",
154 "is",
155 "isnull",
156 "join",
157 "limit",
158 "not",
159 "nothing",
160 "notnull",
161 "null",
162 "on",
163 "or",
164 "order",
165 "primary",
166 "references",
167 "returning",
168 "select",
169 "set",
170 "table",
171 "then",
172 "to",
173 "transaction",
174 "union",
175 "unique",
176 "update",
177 "using",
178 "values",
179 "when",
180 "where",
181];
182const MYSQL_RESERVED: &[&str] = &[
183 "accessible",
184 "add",
185 "all",
186 "alter",
187 "analyze",
188 "and",
189 "as",
190 "asc",
191 "asensitive",
192 "before",
193 "between",
194 "bigint",
195 "binary",
196 "blob",
197 "both",
198 "by",
199 "call",
200 "cascade",
201 "case",
202 "cast",
203 "change",
204 "char",
205 "character",
206 "check",
207 "collate",
208 "column",
209 "condition",
210 "constraint",
211 "continue",
212 "convert",
213 "create",
214 "cross",
215 "cube",
216 "cume_dist",
217 "current_date",
218 "current_time",
219 "current_timestamp",
220 "current_user",
221 "cursor",
222 "database",
223 "databases",
224 "day_hour",
225 "day_microsecond",
226 "day_minute",
227 "day_second",
228 "dec",
229 "decimal",
230 "declare",
231 "default",
232 "delayed",
233 "delete",
234 "dense_rank",
235 "desc",
236 "describe",
237 "deterministic",
238 "distinct",
239 "distinctrow",
240 "div",
241 "double",
242 "drop",
243 "dual",
244 "each",
245 "else",
246 "elseif",
247 "empty",
248 "enclosed",
249 "escaped",
250 "except",
251 "exists",
252 "exit",
253 "explain",
254 "extract",
255 "false",
256 "fetch",
257 "first_value",
258 "float",
259 "float4",
260 "float8",
261 "for",
262 "force",
263 "foreign",
264 "from",
265 "fulltext",
266 "function",
267 "generated",
268 "get",
269 "grant",
270 "group",
271 "grouping",
272 "groups",
273 "having",
274 "high_priority",
275 "hour_microsecond",
276 "hour_minute",
277 "hour_second",
278 "if",
279 "ignore",
280 "in",
281 "index",
282 "infile",
283 "inner",
284 "inout",
285 "insensitive",
286 "insert",
287 "int",
288 "int1",
289 "int2",
290 "int3",
291 "int4",
292 "int8",
293 "integer",
294 "intersect",
295 "interval",
296 "into",
297 "io_after_gtids",
298 "io_before_gtids",
299 "is",
300 "iterate",
301 "join",
302 "json_table",
303 "key",
304 "keys",
305 "kill",
306 "lag",
307 "last_value",
308 "lateral",
309 "lead",
310 "leading",
311 "leave",
312 "left",
313 "like",
314 "limit",
315 "linear",
316 "lines",
317 "load",
318 "localtime",
319 "localtimestamp",
320 "lock",
321 "long",
322 "longblob",
323 "longtext",
324 "loop",
325 "low_priority",
326 "manual",
327 "master_bind",
328 "master_ssl_verify_server_cert",
329 "match",
330 "maxvalue",
331 "mediumblob",
332 "mediumint",
333 "mediumtext",
334 "middleint",
335 "minute_microsecond",
336 "minute_second",
337 "mod",
338 "modifies",
339 "natural",
340 "no_write_to_binlog",
341 "not",
342 "nth_value",
343 "ntile",
344 "null",
345 "numeric",
346 "of",
347 "on",
348 "optimize",
349 "optimizer_costs",
350 "option",
351 "optionally",
352 "or",
353 "order",
354 "out",
355 "outer",
356 "outfile",
357 "over",
358 "parallel",
359 "partition",
360 "percent_rank",
361 "position",
362 "precision",
363 "primary",
364 "procedure",
365 "purge",
366 "qualify",
367 "range",
368 "rank",
369 "read",
370 "read_write",
371 "reads",
372 "real",
373 "recursive",
374 "references",
375 "regexp",
376 "release",
377 "rename",
378 "repeat",
379 "replace",
380 "require",
381 "resignal",
382 "restrict",
383 "return",
384 "revoke",
385 "right",
386 "rlike",
387 "row",
388 "row_number",
389 "rows",
390 "schema",
391 "schemas",
392 "second_microsecond",
393 "select",
394 "sensitive",
395 "separator",
396 "set",
397 "show",
398 "signal",
399 "smallint",
400 "spatial",
401 "specific",
402 "sql",
403 "sql_big_result",
404 "sql_calc_found_rows",
405 "sql_small_result",
406 "sqlexception",
407 "sqlstate",
408 "sqlwarning",
409 "ssl",
410 "starting",
411 "stored",
412 "straight_join",
413 "substring",
414 "system",
415 "table",
416 "tablesample",
417 "terminated",
418 "then",
419 "tinyblob",
420 "tinyint",
421 "tinytext",
422 "to",
423 "trailing",
424 "trigger",
425 "trim",
426 "true",
427 "undo",
428 "union",
429 "unique",
430 "unlock",
431 "unsigned",
432 "update",
433 "usage",
434 "use",
435 "using",
436 "utc_date",
437 "utc_time",
438 "utc_timestamp",
439 "values",
440 "varbinary",
441 "varchar",
442 "varcharacter",
443 "varying",
444 "virtual",
445 "when",
446 "where",
447 "while",
448 "window",
449 "with",
450 "write",
451 "xor",
452 "year_month",
453 "zerofill",
454];
455
456pub fn validate_identifier(name: &str, kind: &str) -> Result<()> {
457 ensure!(
458 name != "_"
459 && !name.is_empty()
460 && name
461 .bytes()
462 .next()
463 .is_some_and(|b| b.is_ascii_lowercase() || b == b'_')
464 && name
465 .bytes()
466 .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'_'),
467 "invalid {kind} `{name}`: use a lowercase ASCII snake_case identifier, starting with a letter or underscore"
468 );
469 ensure!(
470 name.len() <= 63,
471 "invalid {kind} `{name}`: maximum identifier length is 63 bytes"
472 );
473 ensure!(
474 !RUST_KEYWORDS.contains(&name),
475 "invalid {kind} `{name}`: reserved Rust keyword"
476 );
477 Ok(())
478}
479
480pub fn validate_sql_identifier_for_dialect(
482 name: &str,
483 kind: &str,
484 dialect: crate::Dialect,
485) -> Result<()> {
486 validate_identifier(name, kind)?;
487 let reserved = match dialect {
488 crate::Dialect::Postgres => POSTGRES_RESERVED,
489 crate::Dialect::Sqlite => SQLITE_RESERVED,
490 crate::Dialect::MySql => MYSQL_RESERVED,
491 };
492 ensure!(
493 !reserved.contains(&name),
494 "invalid {kind} `{name}`: reserved SQL keyword for {dialect:?}; choose another name"
495 );
496 Ok(())
497}
498
499pub fn validate_column(name: &str) -> Result<()> {
500 validate_identifier(name, "field")?;
501 ensure!(
502 !["id", "created_at", "updated_at"].contains(&name),
503 "invalid field `{name}`: automatically generated by Gize"
504 );
505 Ok(())
506}
507
508pub fn validate_model_name(name: &str) -> Result<()> {
509 ensure!(
510 !RUST_KEYWORDS.contains(&name),
511 "invalid model `{name}`: reserved Rust keyword"
512 );
513 ensure!(
514 name.bytes().next().is_some_and(|b| b.is_ascii_uppercase())
515 && name.bytes().all(|b| b.is_ascii_alphanumeric()),
516 "invalid model `{name}`: use an ASCII PascalCase name, such as BlogPost"
517 );
518 ensure!(
519 ![
520 "String",
521 "Vec",
522 "Result",
523 "Error",
524 "State",
525 "AppState",
526 "StatusCode",
527 "Json",
528 "Serialize",
529 "Deserialize"
530 ]
531 .contains(&name),
532 "invalid model `{name}`: conflicts with a type used by generated code"
533 );
534 validate_identifier(&crate::naming::table_name(name), "generated table")
535}
536
537#[cfg(test)]
538mod tests {
539 use super::*;
540 use crate::Dialect;
541
542 #[test]
543 fn reserved_words_follow_the_selected_database() {
544 for (dialect, rejected, accepted) in [
546 (
547 Dialect::Postgres,
548 &[
549 "array", "only", "cast", "analyze", "binary", "user", "order",
550 ][..],
551 &["key", "index", "database", "values", "rows", "range"][..],
552 ),
553 (
554 Dialect::MySql,
555 &[
556 "range", "interval", "schema", "read", "lock", "show", "rank", "rows",
557 ][..],
558 &["user", "returning", "offset", "array", "only"][..],
559 ),
560 (
561 Dialect::Sqlite,
562 &["index", "values", "order", "transaction", "commit"][..],
563 &["key", "database", "user", "rows", "range", "offset"][..],
564 ),
565 ] {
566 for name in rejected {
567 let error = validate_sql_identifier_for_dialect(name, "field", dialect)
568 .unwrap_err()
569 .to_string();
570 assert!(
571 error.contains("reserved SQL keyword"),
572 "{dialect:?}: {error}"
573 );
574 }
575 for name in accepted {
576 validate_sql_identifier_for_dialect(name, "field", dialect)
577 .unwrap_or_else(|e| panic!("{dialect:?}: {e}"));
578 }
579 }
580 }
581
582 #[test]
583 fn reserved_lists_are_sorted_lowercase_identifiers() {
584 for list in [POSTGRES_RESERVED, SQLITE_RESERVED, MYSQL_RESERVED] {
585 assert!(list.windows(2).all(|w| w[0] < w[1]));
586 assert!(list.iter().all(|w| validate_identifier(w, "keyword").is_ok()
587 || RUST_KEYWORDS.contains(w)));
588 }
589 }
590}