use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use vimltui::EditorAction;
use crate::ui::state::AppState;
use crate::ui::tabs::{SubView, TabKind};
use super::Action;
pub(super) fn handle_tab_editor(state: &mut AppState, key: KeyEvent) -> Action {
let tab_idx = state.active_tab_idx;
if tab_idx >= state.tabs.len() {
return Action::None;
}
let tab = &mut state.tabs[tab_idx];
let tab_id = tab.id;
let is_script = matches!(tab.kind, TabKind::Script { .. });
let db_type = state.conn.db_type;
let is_source_tab = matches!(
tab.kind,
TabKind::Package { .. } | TabKind::Function { .. } | TabKind::Procedure { .. }
);
let in_insert = tab
.active_editor()
.is_some_and(|e| matches!(e.mode, vimltui::VimMode::Insert | vimltui::VimMode::Replace));
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
if in_insert {
if ctrl {
match key.code {
KeyCode::Char(' ') => {
update_completion_impl(state, true);
return Action::Render;
}
KeyCode::Char('n') => {
if let Some(ref mut cmp) = state.engine.completion {
cmp.next();
}
return Action::Render;
}
KeyCode::Char('p') => {
if let Some(ref mut cmp) = state.engine.completion {
cmp.prev();
}
return Action::Render;
}
KeyCode::Char('y') => {
if let Some(cmp) = state.engine.completion.take() {
accept_completion(state, &cmp);
if let Some(action) = update_completion_impl(state, false) {
return action;
}
}
return Action::Render;
}
_ => {}
}
}
if key.code == KeyCode::Enter && state.engine.completion.is_some() {
if let Some(cmp) = state.engine.completion.take() {
accept_completion(state, &cmp);
if let Some(action) = update_completion_impl(state, false) {
return action;
}
}
return Action::Render;
}
if key.code == KeyCode::Esc && state.engine.completion.is_some() {
state.engine.completion = None;
}
}
if !in_insert {
state.engine.diagnostic_hover = None;
if key.modifiers.contains(KeyModifiers::CONTROL)
&& matches!(key.code, KeyCode::Char(']') | KeyCode::Char('['))
&& !state.engine.diagnostics.is_empty()
{
let cur = state.engine.diagnostic_list_cursor;
let len = state.engine.diagnostics.len();
let idx = if matches!(key.code, KeyCode::Char(']')) {
if cur + 1 < len { cur + 1 } else { 0 }
} else if cur == 0 {
len - 1
} else {
cur - 1
};
state.engine.diagnostic_list_cursor = idx;
let target_row = state.engine.diagnostics[idx].row;
let target_col = state.engine.diagnostics[idx].col_start;
if let Some(editor) = state.tabs[tab_idx].active_editor_mut() {
editor.cursor_row = target_row;
editor.cursor_col = target_col;
editor.ensure_cursor_visible();
}
return Action::Render;
}
}
let (action, still_insert, needs_diag) = {
let tab = &mut state.tabs[tab_idx];
if let Some(editor) = tab.active_editor_mut() {
let action = match editor.handle_key(key) {
EditorAction::Handled => Action::Render,
EditorAction::Unhandled(_) => Action::None,
EditorAction::Save => {
if is_source_tab {
Action::ValidateAndSave { tab_id }
} else {
Action::SaveScript
}
}
EditorAction::Close => Action::CloseTab,
EditorAction::ForceClose => Action::CloseTab,
EditorAction::SaveAndClose => {
if is_script {
return Action::SaveScript;
}
Action::CloseTab
}
EditorAction::ToggleComment => {
toggle_line_comment(editor, db_type);
Action::Render
}
EditorAction::ToggleBlockComment { start_row, end_row } => {
toggle_block_comment(editor, db_type, start_row, end_row);
Action::Render
}
EditorAction::Hover => {
let row = editor.cursor_row;
if let Some(diag) = state.engine.diagnostics.iter().find(|d| d.row == row) {
state.engine.diagnostic_hover =
Some((row, format!("[{}] {}", diag.source.label(), diag.message)));
}
Action::Render
}
EditorAction::GoToDefinition => {
Action::None
}
};
if matches!(editor.mode, vimltui::VimMode::Insert)
&& let KeyCode::Char(ch) = key.code
&& !key.modifiers.contains(KeyModifiers::CONTROL)
{
let close = match ch {
'(' => Some(')'),
'[' => Some(']'),
'{' => Some('}'),
'\'' => Some('\''),
_ => None,
};
if let Some(c) = close {
let row = editor.cursor_row;
let col = editor.cursor_col;
if row < editor.lines.len() {
let line = &mut editor.lines[row];
if col <= line.len() {
line.insert(col, c);
}
}
}
}
let still_insert = matches!(editor.mode, vimltui::VimMode::Insert);
let needs_diag = !still_insert && in_insert && editor.modified && state.metadata_ready;
(action, still_insert, needs_diag)
} else {
return Action::None;
}
};
if !still_insert && in_insert {
state.tabs[tab_idx].check_modified();
}
{
let tab = &state.tabs[tab_idx];
let original = match &tab.active_sub_view {
Some(SubView::PackageDeclaration) | Some(SubView::TypeDeclaration) => {
tab.original_decl.clone()
}
Some(SubView::PackageBody) | Some(SubView::TypeBody) => tab.original_body.clone(),
None if matches!(
tab.kind,
TabKind::Function { .. } | TabKind::Procedure { .. }
) =>
{
tab.original_source.clone()
}
_ => None,
};
if let Some(orig) = original {
let tab = &mut state.tabs[tab_idx];
if let Some(editor) = tab.active_editor_mut() {
let signs = super::compute_diff_signs(&orig, &editor.lines);
if signs.is_empty() {
editor.gutter = None;
} else {
let mut config = editor.gutter.take().unwrap_or_default();
config.signs = signs;
editor.gutter = Some(config);
}
}
}
}
if still_insert {
if let Some(cache_action) = update_completion(state) {
return cache_action;
}
} else {
state.engine.completion = None;
}
if needs_diag {
let tab = &state.tabs[tab_idx];
let is_plsql = matches!(
tab.kind,
TabKind::Package { .. } | TabKind::Function { .. } | TabKind::Procedure { .. }
);
if is_plsql {
state.engine.diagnostics.clear();
} else {
let lines = tab
.active_editor()
.map(|e| e.lines.clone())
.unwrap_or_default();
let eff_conn = state
.tabs
.get(tab_idx)
.and_then(|t| t.kind.conn_name().map(|s| s.to_string()))
.or_else(|| state.conn.name.clone());
let empty_idx = crate::sql_engine::metadata::MetadataIndex::new();
let metadata_idx = eff_conn
.as_ref()
.and_then(|cn| state.engine.metadata_indexes.get(cn))
.unwrap_or(&empty_idx);
let db_type = metadata_idx.db_type();
let dialect_box = db_type
.map(crate::sql_engine::dialect::dialect_for)
.unwrap_or_else(|| Box::new(crate::sql_engine::dialect::OracleDialect));
let provider = crate::sql_engine::diagnostics::DiagnosticProvider::new(
dialect_box.as_ref(),
metadata_idx,
);
let engine_diags = provider.check_local(&lines);
state.engine.diagnostics = engine_diags
.into_iter()
.map(crate::ui::diagnostics::Diagnostic::from_engine)
.collect();
apply_diagnostic_gutter_signs(state, tab_idx);
}
}
action
}
fn apply_diagnostic_gutter_signs(state: &mut AppState, tab_idx: usize) {
use crate::ui::diagnostics::Severity;
use std::collections::HashMap;
let mut diag_signs: HashMap<usize, vimltui::Diagnostic> = HashMap::new();
for d in &state.engine.diagnostics {
let sev = match d.severity {
Severity::Error => vimltui::DiagnosticSeverity::Error,
Severity::Warning | Severity::Info | Severity::Hint => {
vimltui::DiagnosticSeverity::Warning
}
};
diag_signs
.entry(d.row)
.and_modify(|existing| {
if sev == vimltui::DiagnosticSeverity::Error {
existing.severity = vimltui::DiagnosticSeverity::Error;
existing.message = Some(d.message.clone());
}
})
.or_insert(vimltui::Diagnostic {
severity: sev,
message: Some(d.message.clone()),
});
}
if let Some(tab) = state.tabs.get_mut(tab_idx)
&& let Some(editor) = tab.active_editor_mut()
{
if diag_signs.is_empty() && editor.gutter.is_none() {
return;
}
let mut config = editor.gutter.take().unwrap_or_default();
config.diagnostics = diag_signs;
if config.signs.is_empty() && config.diagnostics.is_empty() {
editor.gutter = None;
} else {
editor.gutter = Some(config);
}
}
}
pub(super) fn update_completion(state: &mut AppState) -> Option<Action> {
update_completion_impl(state, false)
}
pub(super) fn update_completion_impl(state: &mut AppState, force: bool) -> Option<Action> {
use crate::sql_engine::analyzer::SemanticAnalyzer;
use crate::sql_engine::completion::{CompletionItemKind, CompletionProvider};
use crate::sql_engine::dialect;
use crate::sql_engine::tokenizer;
use crate::ui::completion::{CompletionItem, CompletionKind, CompletionState};
let tab = match state.tabs.get(state.active_tab_idx) {
Some(t) => t,
None => {
state.engine.completion = None;
return None;
}
};
let editor = match tab.active_editor() {
Some(e) => e,
None => {
state.engine.completion = None;
return None;
}
};
let row = editor.cursor_row;
let col = editor.cursor_col;
let line = editor.current_line();
let bytes = line.as_bytes();
let end = col.min(bytes.len());
let mut pstart = end;
while pstart > 0 && (bytes[pstart - 1].is_ascii_alphanumeric() || bytes[pstart - 1] == b'_') {
pstart -= 1;
}
let prefix = line[pstart..end].to_string();
let start_col = pstart;
let dot_mode = prefix.is_empty() && col > 0 && bytes.get(col - 1) == Some(&b'.');
let star_mode = prefix.is_empty() && col > 0 && bytes.get(col - 1) == Some(&b'*');
if prefix.is_empty() && !dot_mode && !star_mode && !force {
if let Some(cmp) = state.engine.completion.take() {
let old_prefix_upper = cmp.prefix.to_uppercase();
if !old_prefix_upper.is_empty() {
let exact: Vec<_> = cmp
.items
.iter()
.filter(|item| item.label.to_uppercase() == old_prefix_upper)
.collect();
if exact.len() == 1 && exact[0].label != cmp.prefix {
let tab = state.tabs.get_mut(state.active_tab_idx);
if let Some(editor) = tab.and_then(|t| t.active_editor_mut()) {
let r = cmp.origin_row;
if r < editor.lines.len() {
let line = &editor.lines[r];
let start = cmp.origin_col.min(line.len());
let end = (start + cmp.prefix.len()).min(line.len());
let mut new_line = String::with_capacity(line.len());
new_line.push_str(&line[..start]);
new_line.push_str(&exact[0].label);
new_line.push_str(&line[end..]);
editor.lines[r] = new_line;
let diff = exact[0].label.len() as isize - cmp.prefix.len() as isize;
if editor.cursor_row == r && editor.cursor_col > start {
editor.cursor_col =
(editor.cursor_col as isize + diff).max(0) as usize;
}
}
}
}
}
}
return None;
}
let editor_row = row;
let total_lines = editor.lines.len();
let mut block_start = row;
while block_start > 0 && !editor.lines[block_start - 1].trim().is_empty() {
block_start -= 1;
}
let mut block_end = row + 1;
while block_end < total_lines && !editor.lines[block_end].trim().is_empty() {
block_end += 1;
}
let lines: Vec<String> = editor.lines[block_start..block_end].to_vec();
let block_row = row - block_start;
let eff_conn = state
.active_tab()
.and_then(|t| t.kind.conn_name().map(|s| s.to_string()))
.or_else(|| state.conn.name.clone());
let empty_idx = crate::sql_engine::metadata::MetadataIndex::new();
let metadata_idx = eff_conn
.as_ref()
.and_then(|cn| state.engine.metadata_indexes.get(cn))
.unwrap_or(&empty_idx);
let db_type = metadata_idx.db_type();
let dialect_box = db_type
.map(dialect::dialect_for)
.unwrap_or_else(|| Box::new(dialect::OracleDialect));
let analyzer = SemanticAnalyzer::new(dialect_box.as_ref(), metadata_idx);
let ctx = analyzer.analyze(&lines, block_row, col);
let provider = CompletionProvider::new(dialect_box.as_ref(), metadata_idx);
let scored_items = provider.complete(&ctx);
let is_table_ref = matches!(
ctx.cursor_context,
crate::sql_engine::context::CursorContext::TableRef
);
let existing_aliases: Vec<String> = ctx.aliases.keys().cloned().collect();
let items: Vec<CompletionItem> = scored_items
.into_iter()
.map(|si| {
let kind = match si.kind {
CompletionItemKind::Keyword => CompletionKind::Keyword,
CompletionItemKind::Schema => CompletionKind::Schema,
CompletionItemKind::Table => CompletionKind::Table,
CompletionItemKind::View => CompletionKind::View,
CompletionItemKind::Column => CompletionKind::Column,
CompletionItemKind::Package => CompletionKind::Package,
CompletionItemKind::Function => CompletionKind::Function,
CompletionItemKind::Procedure => CompletionKind::Procedure,
CompletionItemKind::Alias => CompletionKind::Alias,
CompletionItemKind::ForeignKeyJoin => CompletionKind::Table,
};
CompletionItem {
label: si.label,
kind,
}
})
.collect();
let has_dot = dot_mode || {
let before = &lines[block_row][..col.min(lines[block_row].len())];
tokenizer::identifier_before_dot(before).is_some()
};
let set_table_cache =
if let crate::sql_engine::context::CursorContext::SetClause { ref target_table } =
ctx.cursor_context
{
let has_cols = ctx
.columns_for(&target_table.name, &|s| dialect_box.normalize_identifier(s))
.is_empty();
if has_cols {
resolve_table_for_cache(state, &lines, block_row, &target_table.name).and_then(
|(schema, table)| {
let key = format!("{}.{}", schema.to_uppercase(), table.to_uppercase());
if !state.engine.column_cache.contains_key(&key)
&& !metadata_idx.has_columns_cached(&schema, &table)
{
Some(Action::CacheColumns { schema, table })
} else {
None
}
},
)
} else {
None
}
} else {
None
};
let cache_action = if items.is_empty() && has_dot {
let before = &lines[block_row][..col.min(lines[block_row].len())];
if let Some((table_ref, _)) = tokenizer::identifier_before_dot(before) {
if metadata_idx.is_known_schema(table_ref) {
Some(Action::CacheSchemaObjects {
schema: table_ref.to_string(),
})
} else {
resolve_table_for_cache(state, &lines, block_row, table_ref).and_then(
|(schema, table)| {
let key = format!("{}.{}", schema.to_uppercase(), table.to_uppercase());
if !state.engine.column_cache.contains_key(&key)
&& !metadata_idx.has_columns_cached(&schema, &table)
{
Some(Action::CacheColumns { schema, table })
} else {
None
}
},
)
}
} else {
None
}
} else {
None
};
let cache_action = cache_action.or(set_table_cache);
if items.is_empty() {
state.engine.completion = None;
return cache_action;
}
let prev_cursor = state
.engine
.completion
.as_ref()
.map(|c| c.cursor.min(items.len().saturating_sub(1)))
.unwrap_or(0);
state.engine.completion = Some(CompletionState {
items,
cursor: prev_cursor,
prefix,
origin_row: editor_row,
origin_col: start_col,
table_ref_context: is_table_ref,
existing_aliases,
});
cache_action
}
pub(super) fn resolve_table_for_cache(
state: &AppState,
lines: &[String],
_row: usize,
table_ref: &str,
) -> Option<(String, String)> {
let block: Vec<String> = lines.to_vec();
let resolved = crate::ui::completion::resolve_table_name(&block, table_ref);
let table_name = resolved.as_deref().unwrap_or(table_ref);
let eff_conn = state
.active_tab()
.and_then(|t| t.kind.conn_name().map(|s| s.to_string()))
.or_else(|| state.conn.name.clone());
let empty_idx = crate::sql_engine::metadata::MetadataIndex::new();
let metadata_idx = eff_conn
.as_ref()
.and_then(|cn| state.engine.metadata_indexes.get(cn))
.unwrap_or(&empty_idx);
if let Some(schema) = metadata_idx.resolve_schema_for(table_name) {
return Some((schema.to_string(), table_name.to_string()));
}
let schema = crate::ui::completion::find_schema_for_table(state, table_name)?;
Some((schema, table_name.to_string()))
}
fn toggle_line_comment(
editor: &mut vimltui::VimEditor,
_db_type: Option<crate::core::models::DatabaseType>,
) {
let row = editor.cursor_row;
if row >= editor.lines.len() {
return;
}
editor.save_undo();
let line = &editor.lines[row];
let trimmed = line.trim_start();
if trimmed.starts_with("-- ") {
if let Some(pos) = line.find("-- ") {
let mut new_line = String::with_capacity(line.len());
new_line.push_str(&line[..pos]);
new_line.push_str(&line[pos + 3..]);
editor.lines[row] = new_line;
editor.cursor_col = editor.cursor_col.saturating_sub(3);
}
} else if trimmed.starts_with("--") {
if let Some(pos) = line.find("--") {
let mut new_line = String::with_capacity(line.len());
new_line.push_str(&line[..pos]);
new_line.push_str(&line[pos + 2..]);
editor.lines[row] = new_line;
editor.cursor_col = editor.cursor_col.saturating_sub(2);
}
} else {
let indent = line.len() - trimmed.len();
let mut new_line = String::with_capacity(line.len() + 3);
new_line.push_str(&line[..indent]);
new_line.push_str("-- ");
new_line.push_str(trimmed);
editor.lines[row] = new_line;
editor.cursor_col += 3;
}
editor.modified = true;
}
fn toggle_block_comment(
editor: &mut vimltui::VimEditor,
_db_type: Option<crate::core::models::DatabaseType>,
start_row: usize,
end_row: usize,
) {
let end = end_row.min(editor.lines.len().saturating_sub(1));
if start_row > end {
return;
}
editor.save_undo();
let all_commented = (start_row..=end).all(|r| {
let trimmed = editor.lines[r].trim_start();
trimmed.starts_with("--")
});
for r in start_row..=end {
let line = &editor.lines[r];
let trimmed = line.trim_start();
let indent = line.len() - trimmed.len();
if all_commented {
let uncommented = trimmed
.strip_prefix("-- ")
.or_else(|| trimmed.strip_prefix("--"));
if let Some(rest) = uncommented {
let mut new_line = String::with_capacity(line.len());
new_line.push_str(&line[..indent]);
new_line.push_str(rest);
editor.lines[r] = new_line;
}
} else {
if !trimmed.starts_with("--") {
let mut new_line = String::with_capacity(line.len() + 3);
new_line.push_str(&line[..indent]);
new_line.push_str("-- ");
new_line.push_str(trimmed);
editor.lines[r] = new_line;
}
}
}
editor.modified = true;
}
const RESERVED_ALIAS_WORDS: &[&str] = &[
"SELECT",
"FROM",
"WHERE",
"AND",
"OR",
"NOT",
"IN",
"ON",
"AS",
"IS",
"BY",
"IF",
"DO",
"SET",
"ALL",
"ANY",
"FOR",
"TOP",
"END",
"ASC",
"DESC",
"JOIN",
"LEFT",
"RIGHT",
"FULL",
"INTO",
"NULL",
"LIKE",
"CASE",
"WHEN",
"THEN",
"ELSE",
"WITH",
"OVER",
"ORDER",
"GROUP",
"HAVING",
"LIMIT",
"UNION",
"UPDATE",
"DELETE",
"INSERT",
"CREATE",
"ALTER",
"DROP",
"TABLE",
"VIEW",
"INDEX",
"BETWEEN",
"EXISTS",
"DISTINCT",
"VALUES",
"INNER",
"OUTER",
"CROSS",
"NATURAL",
"USING",
"OFFSET",
"EXCEPT",
"INTERSECT",
"PRIMARY",
"FOREIGN",
"REFERENCES",
"CONSTRAINT",
"DEFAULT",
"CHECK",
"UNIQUE",
"CASCADE",
"TRUNCATE",
"GRANT",
"REVOKE",
"BEGIN",
"COMMIT",
"ROLLBACK",
"DECLARE",
"FUNCTION",
"PROCEDURE",
"TRIGGER",
"RETURN",
"TO",
"NO",
"GO",
];
fn generate_table_alias(table_name: &str, existing: &[String]) -> String {
let lower = table_name.to_lowercase();
let parts: Vec<&str> = lower.split('_').filter(|p| !p.is_empty()).collect();
let conflicts = |candidate: &str| -> bool {
existing.iter().any(|a| a.eq_ignore_ascii_case(candidate))
|| RESERVED_ALIAS_WORDS
.iter()
.any(|r| r.eq_ignore_ascii_case(candidate))
};
let mut candidates: Vec<String> = Vec::new();
if parts.len() > 1 {
let initials: String = parts.iter().filter_map(|p| p.chars().next()).collect();
if initials.len() >= 2 {
candidates.push(initials.chars().take(2).collect());
}
if initials.len() >= 3 {
candidates.push(initials.chars().take(3).collect());
}
if parts[0].len() >= 2 {
candidates.push(parts[0].chars().take(2).collect());
}
if parts[0].len() >= 3 {
candidates.push(parts[0].chars().take(3).collect());
}
} else {
let chars: Vec<char> = lower.chars().collect();
if let Some(c) = chars[1..].iter().find(|c| !"aeiou".contains(**c)) {
candidates.push(format!("{}{c}", chars[0]));
}
if chars.len() >= 2 {
candidates.push(chars[..2].iter().collect());
}
let consonants: Vec<char> = chars[1..]
.iter()
.filter(|c| !"aeiou".contains(**c) && c.is_ascii_alphabetic())
.copied()
.collect();
if consonants.len() >= 2 {
candidates.push(format!("{}{}{}", chars[0], consonants[0], consonants[1]));
}
if chars.len() >= 3 {
candidates.push(chars[..3].iter().collect());
}
}
for c in &candidates {
if !conflicts(c) {
return c.clone();
}
}
let base = candidates
.first()
.cloned()
.unwrap_or_else(|| "tb".to_string());
for n in 2..100 {
let candidate = format!("{base}{n}");
if !conflicts(&candidate) {
return candidate;
}
}
base
}
pub(super) fn accept_completion(
state: &mut AppState,
cmp: &crate::ui::completion::CompletionState,
) {
use crate::ui::completion::CompletionKind;
let item = match cmp.selected() {
Some(s) => s.clone(),
None => return,
};
let needs_parens = match item.kind {
CompletionKind::Function | CompletionKind::Procedure => true,
CompletionKind::Keyword => {
matches!(
item.label.as_str(),
"IN" | "EXISTS" | "NOT IN" | "NOT EXISTS"
)
}
_ => false,
};
let (insert_text, cursor_inside_parens) = match item.kind {
CompletionKind::Alias | CompletionKind::Schema => (format!("{}.", item.label), false),
_ if needs_parens => (format!("{}()", item.label), true),
CompletionKind::Table | CompletionKind::View if cmp.table_ref_context => {
let alias = generate_table_alias(&item.label, &cmp.existing_aliases);
(format!("{} {alias}", item.label), false)
}
_ => (item.label, false),
};
let tab = match state.tabs.get_mut(state.active_tab_idx) {
Some(t) => t,
None => return,
};
let editor = match tab.active_editor_mut() {
Some(e) => e,
None => return,
};
let row = cmp.origin_row;
if row >= editor.lines.len() {
return;
}
let line = &editor.lines[row];
let start = cmp.origin_col.min(line.len());
let end = editor.cursor_col.min(line.len());
let mut new_line = String::with_capacity(line.len() + insert_text.len());
new_line.push_str(&line[..start]);
new_line.push_str(&insert_text);
if end < line.len() {
new_line.push_str(&line[end..]);
}
editor.lines[row] = new_line;
editor.cursor_col = if cursor_inside_parens {
start + insert_text.len() - 1
} else {
start + insert_text.len()
};
editor.modified = true;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn alias_single_word() {
let ord = generate_table_alias("orders", &[]);
assert!(
!RESERVED_ALIAS_WORDS
.iter()
.any(|r| r.eq_ignore_ascii_case(&ord)),
"alias '{ord}' is a reserved word"
);
assert!(ord.len() <= 3, "orders → {ord}");
let us = generate_table_alias("users", &[]);
assert!(
!RESERVED_ALIAS_WORDS
.iter()
.any(|r| r.eq_ignore_ascii_case(&us)),
"alias '{us}' is a reserved word"
);
let cs = generate_table_alias("customers", &[]);
assert_eq!(cs, "cs"); }
#[test]
fn alias_avoids_reserved_words() {
let alias = generate_table_alias("orders", &[]);
assert_ne!(alias.to_uppercase(), "OR", "got {alias}");
let alias = generate_table_alias("assets", &[]);
assert_ne!(alias.to_uppercase(), "AS", "got {alias}");
let alias = generate_table_alias("invoices", &[]);
assert_ne!(alias.to_uppercase(), "IN", "got {alias}");
let alias = generate_table_alias("online_orders", &[]);
assert_ne!(alias.to_uppercase(), "ON", "got {alias}");
}
#[test]
fn alias_multi_word() {
assert_eq!(generate_table_alias("customer_orders", &[]), "co");
assert_eq!(generate_table_alias("order_line_items", &[]), "ol");
assert_eq!(generate_table_alias("user_role_mapping", &[]), "ur");
}
#[test]
fn alias_conflict_expands_to_3() {
let alias = generate_table_alias("customers", &["cs".to_string()]);
assert!(alias.len() <= 3, "got {alias}");
assert_ne!(alias, "cs");
}
#[test]
fn alias_no_duplicate_across_tables() {
let existing = vec!["or".to_string()]; let alias = generate_table_alias("organisms", &existing);
assert_ne!(alias, "or", "should not conflict");
}
#[test]
fn alias_conflict_case_insensitive() {
let existing = vec!["CO".to_string()];
let alias = generate_table_alias("customer_orders", &existing);
assert_ne!(alias.to_lowercase(), "co");
}
}