use std::collections::HashMap;
use sqlparser::ast::{
AlterType, AlterTypeAddValue, AlterTypeAddValuePosition, AlterTypeOperation, Expr, ObjectName,
Value,
};
use super::alter::drop_column;
use super::names::{ident_name, normalize_schema, object_leaf_name, object_schema_name};
use super::table::rust_type;
use crate::parser::naming;
use crate::schema::{BaseType, Column, EnumType, EnumVariant, Shape, Table};
pub(super) fn uses_type(table: &Table, rust_path: &str) -> bool {
table
.columns
.iter()
.any(|column| uses_type_column(column, rust_path))
}
fn uses_type_column(column: &Column, rust_path: &str) -> bool {
column.sqlx_type == rust_path || Shape::of(&column.sqlx_type) == Shape::List(rust_path)
}
pub(super) fn resolve_value_lists(
tables: &mut [Table],
enums: &EnumRegistry,
earlier: &[EnumType],
) -> (Vec<EnumType>, HashMap<String, String>) {
let mut generated: Vec<EnumType> = Vec::new();
let mut broken = HashMap::new();
for table in tables.iter_mut() {
let mut table_enums = Vec::new();
let mut failure = None;
for column in &table.columns {
let Some(labels) = &column.value_list else {
continue;
};
let rust_name =
naming::type_name(&format!("{}_{}", table.struct_name, column.rust_name));
let taken = enums.has_rust_name(&rust_name)
|| earlier
.iter()
.chain(&generated)
.chain(&table_enums)
.any(|other: &EnumType| other.rust_name == rust_name);
let variants = if taken {
Err(format!(
"its Rust name `{rust_name}` is already used by another enum type"
))
} else {
enum_variants(labels.clone())
};
match variants {
Ok(variants) => table_enums.push(EnumType {
schema: None,
sql_name: column.sql_name.clone(),
rust_name,
variants,
column: Some(format!("{}.{}", table.display_name(), column.sql_name)),
}),
Err(reason) => {
failure = Some(format!(
"column `{}`: value list cannot be generated: {reason}",
column.sql_name
));
break;
}
}
}
if let Some(reason) = failure {
broken.insert(table.module_name.clone(), reason);
continue;
}
for enum_type in table_enums {
let column = table
.columns
.iter_mut()
.find(|column| Some(&column.sql_name) == Some(&enum_type.sql_name))
.expect("value-list enums come from the table's columns");
column.sqlx_type = enum_type.rust_path();
column.rust_type = rust_type(&column.sqlx_type, column.nullable);
generated.push(enum_type);
}
}
(generated, broken)
}
#[derive(Default)]
pub(super) struct EnumRegistry {
pub(super) entries: Vec<EnumEntry>,
}
pub(super) struct EnumEntry {
pub(super) enum_type: EnumType,
pub(super) broken: Option<String>,
}
impl EnumEntry {
pub(super) fn rust_path(&self) -> std::result::Result<String, String> {
match &self.broken {
None => Ok(self.enum_type.rust_path()),
Some(reason) => Err(format!(
"type `{}` cannot be generated: {reason}",
self.enum_type.display_name()
)),
}
}
fn set_labels(&mut self, labels: Vec<String>) {
match enum_variants(labels) {
Ok(variants) => self.enum_type.variants = variants,
Err(reason) => self.broken = Some(reason),
}
}
fn labels(&self) -> Vec<String> {
self.enum_type
.variants
.iter()
.map(|variant| variant.label.clone())
.collect()
}
}
pub(super) struct RenamedEnum {
from: String,
to: String,
}
pub(super) struct BrokenEnum {
pub(super) rust_path: String,
pub(super) reason: String,
}
impl EnumRegistry {
fn position(&self, name: &ObjectName) -> Option<usize> {
let schema = normalize_schema(object_schema_name(name));
let sql_name = object_leaf_name(name);
self.entries.iter().position(|entry| {
entry.enum_type.sql_name == sql_name && entry.enum_type.schema == schema
})
}
fn has_rust_name(&self, rust_name: &str) -> bool {
self.entries
.iter()
.any(|entry| entry.enum_type.rust_name == rust_name)
}
pub(super) fn lookup(&self, name: &ObjectName) -> Option<&EnumEntry> {
self.position(name).map(|index| &self.entries[index])
}
fn rust_name_for(
&self,
schema: Option<&str>,
sql_name: &str,
except: Option<usize>,
) -> (String, Option<String>) {
let taken = |rust_name: &str| {
self.entries.iter().enumerate().any(|(index, entry)| {
Some(index) != except && entry.enum_type.rust_name == rust_name
})
};
let mut rust_name = naming::type_name(sql_name);
if taken(&rust_name)
&& let Some(schema) = schema
{
rust_name = naming::type_name(&format!("{schema}_{sql_name}"));
}
let broken = if rust_name.is_empty() {
Some(format!(
"`{sql_name}` does not contain any characters usable in a Rust identifier"
))
} else if taken(&rust_name) {
Some(format!(
"its Rust name `{rust_name}` is already used by another enum type"
))
} else {
None
};
(rust_name, broken)
}
pub(super) fn define(&mut self, name: &ObjectName, labels: impl Iterator<Item = String>) {
if let Some(index) = self.position(name) {
self.entries[index].broken = Some("the type is defined more than once".to_owned());
return;
}
let schema = normalize_schema(object_schema_name(name));
let sql_name = object_leaf_name(name);
let (rust_name, broken) = self.rust_name_for(schema.as_deref(), &sql_name, None);
let mut entry = EnumEntry {
enum_type: EnumType {
schema,
sql_name,
rust_name,
variants: Vec::new(),
column: None,
},
broken,
};
entry.set_labels(labels.collect());
self.entries.push(entry);
}
pub(super) fn alter(
&mut self,
alter: &AlterType,
) -> std::result::Result<Option<RenamedEnum>, BrokenEnum> {
let Some(index) = self.position(&alter.name) else {
return Ok(None);
};
if self.entries[index].broken.is_some() {
return Ok(None);
}
let mut labels = self.entries[index].labels();
let mut renamed = None;
let applied = match &alter.operation {
AlterTypeOperation::AddValue(add) => add_enum_value(&mut labels, add),
AlterTypeOperation::RenameValue(rename) => {
let (from, to) = (&rename.from.value, &rename.to.value);
match labels.iter().position(|label| label == from) {
_ if labels.contains(to) => {
Err(format!("RENAME VALUE: label `{to}` already exists"))
}
Some(index) => {
labels[index] = to.clone();
Ok(())
}
None => Err(format!("RENAME VALUE: label `{from}` does not exist")),
}
}
AlterTypeOperation::Rename(rename) => {
let sql_name = ident_name(&rename.new_name);
let schema = self.entries[index].enum_type.schema.clone();
let exists = self.entries.iter().any(|entry| {
entry.enum_type.sql_name == sql_name && entry.enum_type.schema == schema
});
let (rust_name, broken) =
self.rust_name_for(schema.as_deref(), &sql_name, Some(index));
match (exists, broken) {
(true, _) => {
let name = match &schema {
Some(schema) => format!("{schema}.{sql_name}"),
None => sql_name,
};
Err(format!("RENAME TO: type `{name}` already exists"))
}
(false, Some(reason)) => Err(format!("RENAME TO: {reason}")),
(false, None) => {
let enum_type = &mut self.entries[index].enum_type;
let from = enum_type.rust_path();
enum_type.sql_name = sql_name;
enum_type.rust_name = rust_name;
renamed = Some(RenamedEnum {
from,
to: enum_type.rust_path(),
});
Ok(())
}
}
}
};
let entry = &mut self.entries[index];
let reason = match applied {
Ok(()) => {
entry.set_labels(labels);
match entry.broken.clone() {
None => return Ok(renamed),
Some(reason) => reason,
}
}
Err(reason) => reason,
};
entry.broken = Some(reason.clone());
Err(BrokenEnum {
rust_path: entry.enum_type.rust_path(),
reason: format!("ALTER TYPE `{}`: {reason}", entry.enum_type.display_name()),
})
}
pub(super) fn drop(&mut self, name: &ObjectName) -> Option<String> {
let index = self.position(name)?;
Some(self.entries.remove(index).enum_type.rust_path())
}
pub(super) fn into_generatable(self) -> Vec<EnumType> {
self.entries
.into_iter()
.filter(|entry| entry.broken.is_none())
.map(|entry| entry.enum_type)
.collect()
}
}
fn add_enum_value(
labels: &mut Vec<String>,
add: &AlterTypeAddValue,
) -> std::result::Result<(), String> {
let value = &add.value.value;
if labels.contains(value) {
if add.if_not_exists {
return Ok(());
}
return Err(format!("ADD VALUE: label `{value}` already exists"));
}
let index = match &add.position {
None => labels.len(),
Some(
AlterTypeAddValuePosition::Before(neighbor)
| AlterTypeAddValuePosition::After(neighbor),
) => {
let found = labels
.iter()
.position(|label| *label == neighbor.value)
.ok_or_else(|| format!("ADD VALUE: label `{}` does not exist", neighbor.value))?;
match &add.position {
Some(AlterTypeAddValuePosition::After(_)) => found + 1,
_ => found,
}
}
};
labels.insert(index, value.clone());
Ok(())
}
fn enum_variants(labels: Vec<String>) -> std::result::Result<Vec<EnumVariant>, String> {
if labels.is_empty() {
return Err("the enum has no labels".to_owned());
}
let mut variants: Vec<EnumVariant> = Vec::with_capacity(labels.len());
for label in labels {
let rust_name = naming::variant_name(&label).ok_or_else(|| {
format!("label `{label}` has no characters usable in a Rust identifier")
})?;
if let Some(other) = variants
.iter()
.find(|variant| variant.rust_name == rust_name)
{
return Err(format!(
"labels `{}` and `{label}` both map to the variant `{rust_name}`",
other.label
));
}
variants.push(EnumVariant { label, rust_name });
}
Ok(variants)
}
pub(super) fn retype_columns(table: &mut Table, renamed: &RenamedEnum) {
for column in &mut table.columns {
if column.sqlx_type == renamed.from {
column.sqlx_type.clone_from(&renamed.to);
} else if Shape::of(&column.sqlx_type) == Shape::List(&renamed.from) {
column.sqlx_type = format!("Vec<{}>", renamed.to);
} else {
continue;
}
column.rust_type = rust_type(&column.sqlx_type, column.nullable);
}
}
pub(super) fn drop_columns_of_type(table: &mut Table, rust_path: &str) {
let doomed: Vec<String> = table
.columns
.iter()
.filter(|column| uses_type_column(column, rust_path))
.map(|column| column.sql_name.clone())
.collect();
for name in doomed {
let _ = drop_column(table, &name, true);
}
}
pub(super) fn value_list_check(expr: &Expr) -> Option<(String, Vec<String>)> {
match expr {
Expr::Nested(inner) => value_list_check(inner),
Expr::InList {
expr,
list,
negated: false,
} => {
let Expr::Identifier(column) = expr.as_ref() else {
return None;
};
let mut labels: Vec<String> = Vec::with_capacity(list.len());
for item in list {
let Expr::Value(value) = item else {
return None;
};
let Value::SingleQuotedString(label) = &value.value else {
return None;
};
if !labels.contains(label) {
labels.push(label.clone());
}
}
Some((ident_name(column), labels))
}
_ => None,
}
}
pub(super) fn restrict_to_values(column: &mut Column, labels: Vec<String>) {
if column.base_type() != BaseType::Text {
return;
}
column.value_list = Some(match column.value_list.take() {
Some(existing) => existing
.into_iter()
.filter(|label| labels.contains(label))
.collect(),
None => labels,
});
}