feat(core): split foundational capabilities into dedicated crates
This commit is contained in:
402 files changed
+4386
-3367
No files matched your search
@@ -0,0 +1,277 @@
|
||||
use super::column_format::{
|
||||
column_data_type, column_extra_clause, has_dameng_identity, is_dameng_identity_compatible_type,
|
||||
is_mysql_character_data_type, is_mysql_timestamp_type, mysql_on_update_current_timestamp_clause,
|
||||
strip_inherited_mysql_column_charsets,
|
||||
};
|
||||
use super::comments::{build_sqlserver_column_comment_sql_for_profile, build_sqlserver_table_comment_sql_for_profile};
|
||||
use super::dialect::{capabilities_for, database_label, StructureDialect};
|
||||
use super::foreign_keys::build_foreign_key_sql_for_new_table;
|
||||
use super::indexes::build_create_index_statements;
|
||||
use super::mysql_engine::{append_mysql_table_option, validate_mysql_engine};
|
||||
use super::triggers::build_trigger_sql_for_new_table;
|
||||
use super::types::{TableStructureSqlOptions, TableStructureSqlResult};
|
||||
use super::util::{
|
||||
clean, format_default_for_sql, normalize_default, qualified_new_table, quote_ident, quote_new_ident, quote_string,
|
||||
};
|
||||
use super::validation::{validate_columns, validate_concurrent_index_scope, validate_dameng_identity};
|
||||
use crate::models::connection::DatabaseType;
|
||||
|
||||
fn is_sqlite_integer_family_type(data_type: &str) -> bool {
|
||||
let normalized = data_type.trim().to_ascii_lowercase();
|
||||
["int", "integer", "tinyint", "smallint", "mediumint", "bigint"]
|
||||
.iter()
|
||||
.any(|candidate| normalized == *candidate || normalized.starts_with(&format!("{candidate}(")))
|
||||
}
|
||||
|
||||
pub fn build_create_table_sql(mut options: TableStructureSqlOptions) -> TableStructureSqlResult {
|
||||
let capabilities;
|
||||
let dialect;
|
||||
if options.is_gaussdb_m_mode {
|
||||
capabilities = super::dialect::gaussdb_m_capabilities();
|
||||
dialect = StructureDialect::GaussdbM;
|
||||
} else {
|
||||
capabilities = capabilities_for(options.database_type, options.driver_profile.as_deref());
|
||||
dialect = capabilities.dialect;
|
||||
}
|
||||
options.table_name = clean(&options.table_name);
|
||||
strip_inherited_mysql_column_charsets(&mut options);
|
||||
let mut warnings = Vec::new();
|
||||
warnings.extend(validate_mysql_engine(&options));
|
||||
// Fail closed: a concurrent-index request on a partitioned parent (or on an
|
||||
// existing index in a hand-built draft) is refused up front instead of
|
||||
// degrading into blocking index DDL.
|
||||
warnings.extend(validate_concurrent_index_scope(&options));
|
||||
if options.table_name.is_empty() {
|
||||
warnings.push("Table name is required.".to_string());
|
||||
}
|
||||
let active_columns: Vec<_> = options.columns.iter().filter(|column| !column.marked_for_drop).collect();
|
||||
if active_columns.is_empty() {
|
||||
warnings.push("At least one column is required.".to_string());
|
||||
}
|
||||
validate_columns(&active_columns, &mut warnings);
|
||||
validate_dameng_identity(&options, &active_columns, &mut warnings);
|
||||
if !warnings.is_empty() {
|
||||
return TableStructureSqlResult { statements: Vec::new(), warnings };
|
||||
}
|
||||
let table = qualified_new_table(options.database_type, dialect, options.schema.as_deref(), &options.table_name);
|
||||
if dialect == StructureDialect::Dameng {
|
||||
for column in &active_columns {
|
||||
if has_dameng_identity(column) && !is_dameng_identity_compatible_type(&column.data_type) {
|
||||
warnings.push(format!(
|
||||
"Dameng identity column \"{}\" must use tinyint, smallint, int, integer, bigint, number, numeric, or decimal/dec with scale 0.",
|
||||
column.name
|
||||
));
|
||||
}
|
||||
}
|
||||
if !warnings.is_empty() {
|
||||
return TableStructureSqlResult { statements: Vec::new(), warnings };
|
||||
}
|
||||
}
|
||||
if dialect == StructureDialect::Sqlite {
|
||||
let primary_key_count = active_columns.iter().filter(|column| column.is_primary_key).count();
|
||||
for column in &active_columns {
|
||||
if !column.is_primary_key || !column.extra.as_ref().is_some_and(|e| e.auto_increment.unwrap_or(false)) {
|
||||
continue;
|
||||
}
|
||||
if primary_key_count != 1 {
|
||||
warnings.push(
|
||||
"SQLite AUTOINCREMENT requires a single INTEGER PRIMARY KEY column; disable auto-increment on composite primary keys.".to_string(),
|
||||
);
|
||||
} else if !is_sqlite_integer_family_type(&column.data_type) {
|
||||
warnings.push(format!(
|
||||
"SQLite auto-increment column \"{}\" must use an integer type (normalized to INTEGER).",
|
||||
column.name
|
||||
));
|
||||
}
|
||||
}
|
||||
if !warnings.is_empty() {
|
||||
return TableStructureSqlResult { statements: Vec::new(), warnings };
|
||||
}
|
||||
}
|
||||
let mut statements = Vec::new();
|
||||
let mut column_definitions = Vec::new();
|
||||
|
||||
for column in &active_columns {
|
||||
let mut data_type = column_data_type(dialect, column);
|
||||
// SQLite accepts AUTOINCREMENT only on an exact INTEGER PRIMARY KEY,
|
||||
// so integer-family aliases are normalized when auto-increment is on.
|
||||
if dialect == StructureDialect::Sqlite
|
||||
&& column.is_primary_key
|
||||
&& column.extra.as_ref().is_some_and(|e| e.auto_increment.unwrap_or(false))
|
||||
{
|
||||
data_type = "INTEGER".to_string();
|
||||
}
|
||||
let mut parts = vec![quote_new_ident(options.database_type, dialect, &column.name), data_type];
|
||||
if options.database_type == Some(DatabaseType::Mysql) && is_mysql_character_data_type(&column.data_type) {
|
||||
if !column.character_set.trim().is_empty() {
|
||||
parts.push(format!("CHARACTER SET {}", quote_ident(dialect, &column.character_set)));
|
||||
}
|
||||
if !column.collation.trim().is_empty() {
|
||||
parts.push(format!("COLLATE {}", quote_ident(dialect, &column.collation)));
|
||||
}
|
||||
}
|
||||
if dialect == StructureDialect::Sqlite
|
||||
&& column.is_primary_key
|
||||
&& column.extra.as_ref().is_some_and(|e| e.auto_increment.unwrap_or(false))
|
||||
{
|
||||
parts.push("PRIMARY KEY".to_string());
|
||||
} else if !column.is_nullable
|
||||
&& !column.is_primary_key
|
||||
&& !matches!(dialect, StructureDialect::ClickHouse | StructureDialect::ManticoreSearch)
|
||||
{
|
||||
parts.push("NOT NULL".to_string());
|
||||
} else if column.is_nullable
|
||||
&& !column.is_primary_key
|
||||
&& dialect == StructureDialect::Mysql
|
||||
&& is_mysql_timestamp_type(&column.data_type)
|
||||
{
|
||||
parts.push("NULL".to_string());
|
||||
}
|
||||
if let Some(extra_clause) = column_extra_clause(dialect, column) {
|
||||
parts.push(extra_clause);
|
||||
}
|
||||
let default_value = normalize_default(Some(&column.default_value));
|
||||
if !default_value.is_empty() && dialect != StructureDialect::ManticoreSearch {
|
||||
parts.push(format!("DEFAULT {}", format_default_for_sql(dialect, &column.data_type, &default_value)));
|
||||
}
|
||||
if let Some(on_update) = column.extra.as_ref().and_then(|e| e.on_update_current_timestamp).filter(|v| *v) {
|
||||
if on_update && dialect == StructureDialect::Mysql {
|
||||
parts.push(mysql_on_update_current_timestamp_clause(&column.data_type));
|
||||
}
|
||||
}
|
||||
if dialect == StructureDialect::Mysql && capabilities.comment && !clean(&column.comment).is_empty() {
|
||||
parts.push(format!("COMMENT {}", quote_string(&clean(&column.comment))));
|
||||
}
|
||||
column_definitions.push(parts.join(" "));
|
||||
}
|
||||
|
||||
let pk_columns: Vec<_> = active_columns
|
||||
.iter()
|
||||
.filter(|column| {
|
||||
column.is_primary_key
|
||||
&& dialect != StructureDialect::ManticoreSearch
|
||||
&& !(dialect == StructureDialect::Sqlite
|
||||
&& column.extra.as_ref().is_some_and(|e| e.auto_increment.unwrap_or(false)))
|
||||
})
|
||||
.collect();
|
||||
if !pk_columns.is_empty() {
|
||||
let pk_list = pk_columns
|
||||
.iter()
|
||||
.map(|column| quote_new_ident(options.database_type, dialect, &column.name))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
column_definitions.push(format!("PRIMARY KEY ({pk_list})"));
|
||||
}
|
||||
|
||||
statements.push(format!("CREATE TABLE {table} (\n {}\n);", column_definitions.join(",\n ")));
|
||||
|
||||
if let Some(engine) = options.mysql_engine.as_deref().map(str::trim).filter(|engine| !engine.is_empty()) {
|
||||
if let Some(statement) = statements.last_mut() {
|
||||
append_mysql_table_option(statement, &format!("ENGINE = {engine}"));
|
||||
}
|
||||
}
|
||||
|
||||
if capabilities.comment {
|
||||
let table_comment = clean(options.table_comment.as_deref().unwrap_or(""));
|
||||
if !table_comment.is_empty() {
|
||||
if matches!(dialect, StructureDialect::Mysql | StructureDialect::GaussdbM) {
|
||||
if let Some(last) = statements.last_mut() {
|
||||
append_mysql_table_option(last, &format!("COMMENT = {}", quote_string(&table_comment)));
|
||||
}
|
||||
} else if matches!(
|
||||
dialect,
|
||||
StructureDialect::Postgres
|
||||
| StructureDialect::Oracle
|
||||
| StructureDialect::Dameng
|
||||
| StructureDialect::Oscar
|
||||
| StructureDialect::H2
|
||||
) {
|
||||
statements.push(format!("COMMENT ON TABLE {table} IS {};", quote_string(&table_comment)));
|
||||
} else if dialect == StructureDialect::ClickHouse {
|
||||
statements.push(format!("ALTER TABLE {table} MODIFY COMMENT {};", quote_string(&table_comment)));
|
||||
} else if dialect == StructureDialect::SqlServer {
|
||||
statements.extend(build_sqlserver_table_comment_sql_for_profile(
|
||||
&table,
|
||||
options.schema.as_deref(),
|
||||
&options.table_name,
|
||||
&table_comment,
|
||||
options.driver_profile.as_deref(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if capabilities.comment
|
||||
&& matches!(
|
||||
dialect,
|
||||
StructureDialect::Postgres
|
||||
| StructureDialect::Oracle
|
||||
| StructureDialect::Dameng
|
||||
| StructureDialect::Oscar
|
||||
| StructureDialect::H2
|
||||
)
|
||||
{
|
||||
for column in &active_columns {
|
||||
if !clean(&column.comment).is_empty() {
|
||||
statements.push(format!(
|
||||
"COMMENT ON COLUMN {table}.{} IS {};",
|
||||
quote_new_ident(options.database_type, dialect, &column.name),
|
||||
quote_string(&clean(&column.comment))
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
if capabilities.comment && dialect == StructureDialect::ClickHouse {
|
||||
for column in &active_columns {
|
||||
if !clean(&column.comment).is_empty() {
|
||||
statements.push(format!(
|
||||
"ALTER TABLE {table} COMMENT COLUMN {} {};",
|
||||
quote_new_ident(options.database_type, dialect, &column.name),
|
||||
quote_string(&clean(&column.comment))
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
if capabilities.comment && dialect == StructureDialect::SqlServer {
|
||||
for column in &active_columns {
|
||||
if !clean(&column.comment).is_empty() {
|
||||
statements.extend(build_sqlserver_column_comment_sql_for_profile(
|
||||
&table,
|
||||
options.schema.as_deref(),
|
||||
&options.table_name,
|
||||
&column.name,
|
||||
&column.comment,
|
||||
options.driver_profile.as_deref(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for index in options.indexes.iter().filter(|index| !index.marked_for_drop && !index.is_primary) {
|
||||
if !capabilities.create_index {
|
||||
warnings.push(format!(
|
||||
"Creating indexes is not supported for {} from this editor.",
|
||||
database_label(options.database_type)
|
||||
));
|
||||
continue;
|
||||
}
|
||||
statements.extend(build_create_index_statements(
|
||||
options.database_type,
|
||||
dialect,
|
||||
&table,
|
||||
index,
|
||||
&mut warnings,
|
||||
options.schema.as_deref(),
|
||||
&options.table_name,
|
||||
false,
|
||||
capabilities.index_concurrent,
|
||||
true,
|
||||
options.driver_profile.as_deref(),
|
||||
));
|
||||
}
|
||||
|
||||
statements.extend(build_foreign_key_sql_for_new_table(&options, &mut warnings));
|
||||
statements.extend(build_trigger_sql_for_new_table(&options, &mut warnings));
|
||||
|
||||
TableStructureSqlResult { statements, warnings }
|
||||
}
|
||||
Reference in new issue
Block a user