use inillucent_value::Collation;
use super::BoundSelect;
use crate::catalog_view::ColumnInfo;
pub(super) fn finish_view_columns(
bound: &mut BoundSelect,
columns: &[Vec<u8>],
view_name: &[u8],
span: crate::lexer::Span,
) -> Result<(), crate::diagnostic::ParseError> {
if !columns.is_empty() && columns.len() != bound.columns.len() {
return Err(super::refused(
format!(
"expected {} columns for '{}' but got {}",
columns.len(),
String::from_utf8_lossy(view_name),
bound.columns.len()
),
span,
));
}
for column in &mut bound.columns {
column.written = None;
}
Ok(())
}
pub fn unique_column_names(names: &[Vec<u8>]) -> Vec<Vec<u8>> {
let mut taken: Vec<Vec<u8>> = Vec::with_capacity(names.len());
let mut out = Vec::with_capacity(names.len());
for (position, name) in names.iter().enumerate() {
let mut chosen = base_name(name, position);
let mut count = 0u32;
while taken.contains(&chosen.to_ascii_lowercase()) {
count = count.saturating_add(1);
chosen = with_suffix(&chosen, count);
}
taken.push(chosen.to_ascii_lowercase());
out.push(chosen);
}
out
}
fn base_name(name: &[u8], position: usize) -> Vec<u8> {
let unnamed = name.is_empty()
|| name.eq_ignore_ascii_case(b"true")
|| name.eq_ignore_ascii_case(b"false");
if unnamed {
return format!("column{}", position.saturating_add(1)).into_bytes();
}
name.to_vec()
}
fn with_suffix(name: &[u8], count: u32) -> Vec<u8> {
let digits = name
.iter()
.rev()
.take_while(|byte| byte.is_ascii_digit())
.count();
let stem_end = name.len().saturating_sub(digits);
let keeps_colon =
digits > 0 && stem_end > 0 && name.get(stem_end.saturating_sub(1)) == Some(&b':');
let stem = match keeps_colon {
true => name.get(..stem_end.saturating_sub(1)).unwrap_or(name),
false => name,
};
let mut out = stem.to_vec();
out.extend_from_slice(format!(":{count}").as_bytes());
out
}
pub fn subquery_columns(select: &BoundSelect, names: &[Vec<u8>]) -> Vec<ColumnInfo> {
let written: Vec<Vec<u8>> = select
.columns
.iter()
.enumerate()
.map(|(index, column)| {
names
.get(index)
.cloned()
.or_else(|| column.written.clone())
.unwrap_or_else(|| column.name.clone())
})
.collect();
let unique = unique_column_names(&written);
select
.columns
.iter()
.enumerate()
.zip(unique)
.map(|((index, column), name)| {
let folded = name.to_ascii_lowercase();
let collation = column.expr.collation().unwrap_or(Collation::Binary);
ColumnInfo {
name,
folded,
declared_type: column.declared_type.clone(),
affinity: select.column_affinity(index),
collation: collation.name().as_bytes().to_ascii_lowercase(),
not_null: false,
not_null_conflict: None,
primary_key_conflict: None,
default_sql: None,
primary_key_position: None,
hidden: false,
generated: false,
stored: false,
generated_sql: None,
}
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn names(list: &[&str]) -> Vec<Vec<u8>> {
list.iter().map(|name| name.as_bytes().to_vec()).collect()
}
#[test]
fn duplicates_are_numbered_without_regard_to_case() {
assert_eq!(
unique_column_names(&names(&["a", "a", "A"])),
names(&["a", "a:1", "A:2"])
);
}
#[test]
fn an_existing_suffix_is_replaced() {
assert_eq!(
unique_column_names(&names(&["x:1", "x", "x"])),
names(&["x:1", "x", "x:2"])
);
}
#[test]
fn unnamed_columns_are_called_column_and_a_position() {
assert_eq!(
unique_column_names(&names(&["", "TRUE", "b"])),
names(&["column1", "column2", "b"])
);
}
}
impl super::Binder<'_> {
pub(super) fn written_column_name(&self, id: crate::ast::ExprId) -> Option<&[u8]> {
let mut current = id;
loop {
match self.ast.expr(current)? {
crate::ast::Expr::Collate { operand, .. } => current = *operand,
crate::ast::Expr::Column { column, .. } => return Some(self.ast.text(*column)),
_ => return None,
}
}
}
}
impl super::Binder<'_> {
pub(super) fn reference_label(
&self,
database: Option<crate::ast::NameId>,
table: Option<crate::ast::NameId>,
column: crate::ast::NameId,
) -> Vec<u8> {
let mut label = Vec::new();
for part in [database, table].into_iter().flatten() {
label.extend_from_slice(self.ast.text(part));
label.push(b'.');
}
label.extend_from_slice(self.ast.text(column));
label
}
}