sqrust_rules/lint/
create_table_without_primary_key.rs1use sqrust_core::{Diagnostic, FileContext, Rule};
2use sqlparser::ast::{ColumnOption, Statement, TableConstraint};
3
4pub struct CreateTableWithoutPrimaryKey;
5
6impl Rule for CreateTableWithoutPrimaryKey {
7 fn name(&self) -> &'static str {
8 "Lint/CreateTableWithoutPrimaryKey"
9 }
10
11 fn check(&self, ctx: &FileContext) -> Vec<Diagnostic> {
12 if !ctx.parse_errors.is_empty() {
14 return Vec::new();
15 }
16
17 let mut diags = Vec::new();
18 let source = &ctx.source;
19 let source_upper = source.to_uppercase();
20
21 for stmt in &ctx.statements {
22 if let Statement::CreateTable(create_table) = stmt {
23 let has_pk = has_primary_key(create_table);
24 if !has_pk {
25 let (line, col) =
26 find_keyword_position(source, &source_upper, "CREATE");
27 diags.push(Diagnostic {
28 rule: self.name(),
29 message: "CREATE TABLE has no PRIMARY KEY defined".to_string(),
30 line,
31 col,
32 });
33 }
34 }
35 }
36
37 diags
38 }
39}
40
41fn has_primary_key(create_table: &sqlparser::ast::CreateTable) -> bool {
45 for col in &create_table.columns {
47 for option_def in &col.options {
48 if let ColumnOption::Unique { is_primary, .. } = &option_def.option {
49 if *is_primary {
50 return true;
51 }
52 }
53 }
54 }
55
56 for constraint in &create_table.constraints {
58 if matches!(constraint, TableConstraint::PrimaryKey { .. }) {
59 return true;
60 }
61 }
62
63 false
64}
65
66fn find_keyword_position(source: &str, source_upper: &str, keyword: &str) -> (usize, usize) {
70 let kw_len = keyword.len();
71 let bytes = source_upper.as_bytes();
72 let text_len = bytes.len();
73
74 let mut search_from = 0usize;
75 while search_from < text_len {
76 let Some(rel) = source_upper[search_from..].find(keyword) else {
77 break;
78 };
79 let abs = search_from + rel;
80
81 let before_ok = abs == 0
82 || {
83 let b = bytes[abs - 1];
84 !b.is_ascii_alphanumeric() && b != b'_'
85 };
86 let after = abs + kw_len;
87 let after_ok = after >= text_len
88 || {
89 let b = bytes[after];
90 !b.is_ascii_alphanumeric() && b != b'_'
91 };
92
93 if before_ok && after_ok {
94 return offset_to_line_col(source, abs);
95 }
96 search_from = abs + 1;
97 }
98
99 (1, 1)
100}
101
102fn offset_to_line_col(source: &str, offset: usize) -> (usize, usize) {
104 let before = &source[..offset];
105 let line = before.chars().filter(|&c| c == '\n').count() + 1;
106 let col = before.rfind('\n').map(|p| offset - p - 1).unwrap_or(offset) + 1;
107 (line, col)
108}