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

310 lines
14 KiB
Rust

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(options: TableStructureSqlOptions) -> TableStructureSqlResult {
build_create_table_sql_with_partition_clause(options, None)
}
/// Shared implementation for plain and partitioned `CREATE TABLE`.
///
/// `partition_clause` is a ready-made `PARTITION BY ...` body (no leading
/// keyword), appended to the `CREATE TABLE` statement. Keeping it as a separate
/// entry point means `build_create_table_sql`'s signature — used from ~160 call
/// sites and integration tests — stays unchanged.
pub(super) fn build_create_table_sql_with_partition_clause(
mut options: TableStructureSqlOptions,
partition_clause: Option<String>,
) -> 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)));
}
}
// Oracle's column grammar is `col type [DEFAULT expr] [inline constraint ...]`, so the
// DEFAULT clause has to precede NOT NULL. Emitting `NOT NULL DEFAULT ...` leaves the
// parser looking for the closing parenthesis of the column list and fails with
// ORA-00907 (t8y2/dbx#9477). Scoped to Oracle: every other engine keeps the historical
// NOT NULL-then-DEFAULT order.
let default_value = normalize_default(Some(&column.default_value));
let default_clause = (!default_value.is_empty() && dialect != StructureDialect::ManticoreSearch)
.then(|| format!("DEFAULT {}", format_default_for_sql(dialect, &column.data_type, &default_value)));
if dialect == StructureDialect::Oracle {
if let Some(clause) = default_clause.clone() {
parts.push(clause);
}
}
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);
}
if dialect != StructureDialect::Oracle {
if let Some(clause) = default_clause {
parts.push(clause);
}
}
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})"));
}
let create_table = format!("CREATE TABLE {table} (\n {}\n)", column_definitions.join(",\n "));
let create_table = match partition_clause {
Some(clause) => format!("{create_table} {clause}"),
None => create_table,
};
statements.push(format!("{create_table};"));
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 }
}