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, ) -> 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::>() .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 } }