Files
dbx/crates/dbx-sql/src/table_structure_sql/validation.rs
T

169 lines
7.2 KiB
Rust

use super::column_format::has_dameng_identity;
use super::dialect::{capabilities_for, StructureDialect};
use super::indexes::has_existing_index_change;
use super::types::{EditableStructureColumn, TableStructureSqlOptions};
use super::util::clean;
/// Hard errors for `CREATE INDEX CONCURRENTLY` requests the editor cannot
/// honor safely. Unlike the advisory warnings in [`validate_draft`], these
/// reject the whole save plan: the caller asked for a non-blocking build and
/// the only way to honor that is the requested concurrent DDL, so falling back
/// to blocking index DDL must never happen silently.
///
/// Only real PostgreSQL advertises `index_concurrent`, so stale/forged
/// `concurrently` flags on other engines are ignored here (they cannot express
/// a concurrent request in the first place).
pub(super) fn validate_concurrent_index_scope(options: &TableStructureSqlOptions) -> Vec<String> {
let mut errors = Vec::new();
let capabilities = capabilities_for(options.database_type, options.driver_profile.as_deref());
if !capabilities.index_concurrent || capabilities.dialect != StructureDialect::Postgres {
return errors;
}
for index in options.indexes.iter().filter(|index| !index.marked_for_drop) {
if !index.concurrently {
continue;
}
if index.original.is_some() {
errors.push(format!(
"CREATE INDEX CONCURRENTLY is only supported for newly created indexes. Editing an existing index \"{}\" with Concurrent enabled is not supported.",
index.name
));
} else if options.partitioned {
errors.push(
"CREATE INDEX CONCURRENTLY is not supported on PostgreSQL partitioned parent tables. Create indexes concurrently on individual partitions and attach them separately."
.to_string(),
);
}
}
errors
}
pub fn validate_draft(options: &TableStructureSqlOptions) -> Vec<String> {
let mut warnings = Vec::new();
let active_columns: Vec<_> = options.columns.iter().filter(|column| !column.marked_for_drop).collect();
validate_columns(&active_columns, &mut warnings);
validate_dameng_identity(options, &active_columns, &mut warnings);
validate_gin_opclass_warnings(options, &mut warnings);
for index in options
.indexes
.iter()
.filter(|index| !index.marked_for_drop && (index.original.is_none() || has_existing_index_change(index)))
{
if clean(&index.name).is_empty() {
warnings.push("Index name cannot be empty.".to_string());
}
if index.columns.iter().map(|column| clean(column)).filter(|column| !column.is_empty()).count() == 0 {
warnings.push(format!(
"Index \"{}\" needs at least one column.",
if index.name.is_empty() { "(new)" } else { &index.name }
));
}
}
warnings
}
pub(super) fn validate_dameng_identity(
options: &TableStructureSqlOptions,
columns: &[&EditableStructureColumn],
warnings: &mut Vec<String>,
) {
if capabilities_for(options.database_type, options.driver_profile.as_deref()).dialect != StructureDialect::Dameng {
return;
}
let identity_columns: Vec<_> = columns.iter().filter(|column| has_dameng_identity(column)).collect();
if identity_columns.len() > 1 {
warnings.push("Dameng tables can have only one identity column.".to_string());
}
for column in identity_columns {
if column.extra.as_ref().and_then(|extra| extra.identity.as_ref()).and_then(|identity| identity.increment)
== Some(0)
{
warnings.push(format!("Dameng identity column \"{}\" increment cannot be 0.", column.name));
}
}
}
pub(super) fn validate_columns(columns: &[&EditableStructureColumn], warnings: &mut Vec<String>) {
let mut names = std::collections::HashSet::new();
for column in columns {
if clean(&column.name).is_empty() {
warnings.push("Column name cannot be empty.".to_string());
}
if clean(&column.data_type).is_empty() {
warnings.push(format!(
"Column \"{}\" type cannot be empty.",
if column.name.is_empty() { "(new)" } else { &column.name }
));
}
let key = clean(&column.name).to_lowercase();
if !key.is_empty() && !names.insert(key) {
warnings.push(format!("Column \"{}\" is duplicated.", column.name));
}
}
}
/// PostgreSQL GIN indexes on varchar/text/character columns without an explicit
/// operator class are a common trap: the default `array_ops` class is rarely what
/// the user wants, and `gin_trgm_ops` (from `pg_trgm` extension) or similar
/// classes are almost always needed. This is a warning, not an error, because
/// the default class may be deliberate in some cases.
fn validate_gin_opclass_warnings(options: &TableStructureSqlOptions, warnings: &mut Vec<String>) {
let capabilities = capabilities_for(options.database_type, options.driver_profile.as_deref());
if capabilities.dialect != StructureDialect::Postgres {
return;
}
// Build a lookup: column name (lowercased) → data type for active columns.
let col_types: std::collections::HashMap<String, &str> = options
.columns
.iter()
.filter(|col| !col.marked_for_drop)
.map(|col| (clean(&col.name).to_lowercase(), col.data_type.as_str()))
.collect();
let text_like = |dt: &str| -> bool {
let lower = dt.to_lowercase();
lower == "text"
|| lower.starts_with("character varying")
|| lower.starts_with("varchar")
|| lower.starts_with("character")
|| lower == "char"
};
for index in options.indexes.iter().filter(|idx| {
!idx.marked_for_drop
&& clean(&idx.index_type).eq_ignore_ascii_case("GIN")
&& (idx.original.is_none() || has_existing_index_change(idx))
}) {
for (i, col) in index.columns.iter().enumerate() {
let col_name = clean(col);
if col_name.is_empty() {
continue;
}
// Expression columns are not matched against table column types.
let is_expr =
index.original.as_ref().and_then(|orig| orig.key_is_expression.get(i).copied()).unwrap_or(false);
if is_expr {
continue;
}
// Check if this column has a text-like type.
let Some(dt) = col_types.get(&col_name.to_lowercase()) else {
continue;
};
if !text_like(dt) {
continue;
}
// Check if opclass is explicitly provided.
let has_opclass =
index.column_opclasses.get(i).and_then(|o| o.as_deref()).map(|v| !clean(v).is_empty()).unwrap_or(false);
if !has_opclass {
warnings.push(format!(
"GIN index \"{}\" on column \"{}\" (type {}) has no operator class specified. Consider adding an operator class, e.g. `gin_trgm_ops` (requires pg_trgm extension) for text search.",
index.name, col_name, dt
));
}
}
}
}