//! Target-database DDL profile: how to *write* SQL for a concrete [`DatabaseType`]. //! //! Profiles are data + small enums. Call sites must not branch on individual databases; //! they only consult profile fields (quote style, auto-increment form, type map, …). use crate::models::connection::DatabaseType; /// Identifier quoting style for generated DDL. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum QuoteStyle { /// MySQL-family: `name` Backtick, /// PostgreSQL / Access / most ANSI: "name" DoubleQuote, /// SQL Server: [name] Brackets, /// Oracle-style unquoted uppercase UnquotedUpper, } /// How auto-increment / identity is expressed on the target database. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum AutoIncSyntax { /// No special auto-increment DDL. None, /// Append a fixed suffix to the column definition (e.g. ` AUTO_INCREMENT`, ` IDENTITY(1,1)`). Suffix(&'static str), /// Replace the mapped type with this type name for auto PK columns (e.g. Access `COUNTER`). ReplaceTypeWith(&'static str), /// PostgreSQL-style sequence + DEFAULT nextval (handled by generator after CREATE). PostgresSequence, } /// Static type rewrite rule: source base type (uppercase, no params) → target template. /// Target may use `{}` for a single length/precision placeholder (first param only). #[derive(Debug, Clone, Copy)] pub struct TypeMapEntry { pub source_base: &'static str, pub target_template: &'static str, } /// How CREATE INDEX places the index method / type. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum IndexTypePlacement { /// No index-type clause. None, /// PostgreSQL: `CREATE INDEX … ON t USING btree (…)` UsingSuffix, /// SQL Server: `CREATE CLUSTERED INDEX …` TypePrefix, /// MySQL: `CREATE INDEX … USING BTREE ON t (…)` UsingBeforeOn, } /// CREATE TRIGGER body shape (templates use `{name}` `{timing}` `{event}` `{table}` `{body}`). #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum TriggerTemplate { /// `CREATE TRIGGER {name} {timing} {event} ON {table} FOR EACH ROW BEGIN {body} END;` MysqlStyle, /// `CREATE TRIGGER {name} {timing} {event} ON {table} FOR EACH ROW EXECUTE FUNCTION {body};` PostgresStyle, /// `CREATE TRIGGER {name} ON {table} {timing} {event} AS BEGIN {body} END;` SqlServerStyle, /// Conservative MySQL-like default for unknown engines. GenericRowBody, } /// RENAME COLUMN strategy inside ALTER TABLE. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum RenameColumnSyntax { /// `CHANGE COLUMN old new_def` (MySQL) MysqlChangeColumn, /// `RENAME COLUMN old TO new` RenameColumn, /// `ALTER COLUMN old RENAME TO new` (H2) AlterColumnRenameTo, /// `EXEC sp_rename 't.old', 'new', 'COLUMN'` SqlServerSpRename, } /// Column DDL that cannot use the generic `ADD COLUMN` / `ALTER COLUMN` forms. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ColumnDdlSyntax { Generic, /// SQL Server uses `ADD` without `COLUMN`, a full definition for /// `ALTER COLUMN`, and separate default-constraint objects. SqlServer, } /// Standalone table and column comment representation. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum CommentDdlSyntax { Generic, /// SQL Server stores comments in the `MS_Description` extended property. SqlServerExtendedProperty, } /// DDL generation profile for one [`DatabaseType`]. #[derive(Debug, Clone, Copy)] pub struct DdlDialectProfile { pub database_type: DatabaseType, pub quote: QuoteStyle, pub auto_inc: AutoIncSyntax, /// When false, display widths like `INT(11)` are stripped if no type_map hit. pub supports_display_width: bool, /// Whether this target's VARCHAR/CHARACTER VARYING/NVARCHAR syntax rejects a /// missing length (e.g. `VARCHAR` with no length is a MySQL syntax error). /// Distinct from `supports_display_width`: that flag is about whether the /// target *prints* MySQL-style `INT(11)` widths, not whether VARCHAR needs one. pub requires_explicit_varchar_length: bool, /// Cap for length params when applying `{}` templates (e.g. Access TEXT max 255). pub max_varchar_len: Option, /// MySQL-style inline `COMMENT '...'` on columns. pub inline_column_comment: bool, /// SQLite-style: emit FOREIGN KEY clauses inside CREATE TABLE. pub foreign_keys_inline_in_create: bool, /// `DROP INDEX name ON table` (MySQL) vs `DROP INDEX IF EXISTS name`. pub drop_index_uses_on_table: bool, /// `DROP FOREIGN KEY` (MySQL) vs `DROP CONSTRAINT`. pub drop_fk_as_foreign_key: bool, /// `DROP TABLE t CASCADE` is accepted. Oracle's `CASCADE CONSTRAINTS` is a /// different clause and does not count. pub drop_table_supports_cascade: bool, /// `DROP TABLE IF EXISTS t` is valid grammar. False for Oracle (pre-23c), /// Access, Firebird, Db2, Informix, SAP HANA and Teradata. pub drop_table_supports_if_exists: bool, /// Index method placement. pub index_type_placement: IndexTypePlacement, /// `INCLUDE (cols)` on indexes. pub index_supports_include: bool, /// Partial index `WHERE …`. pub index_supports_filter: bool, /// Index `COMMENT '…'` (MySQL). pub index_supports_comment: bool, /// Table comment: `ALTER TABLE t COMMENT = '…'` vs `COMMENT ON TABLE`. pub table_comment_via_alter: bool, /// Standalone column comment SQL is unsupported (MySQL needs MODIFY COLUMN). pub column_comment_via_modify_only: bool, pub comment_ddl: CommentDdlSyntax, pub trigger_template: TriggerTemplate, pub rename_column: RenameColumnSyntax, pub column_ddl: ColumnDdlSyntax, /// MySQL `MODIFY COLUMN` vs ANSI `ALTER COLUMN … TYPE/SET`. pub alter_uses_modify_column: bool, /// Batch multiple alter clauses in one `ALTER TABLE` statement. pub alter_batches_clauses: bool, /// Prefer emitting source SHOW CREATE / native DDL when dialects match (MySQL-family). pub prefers_native_source_ddl: bool, /// GRANT/REVOKE identifier style: backtick + user quotes vs ANSI double-quote. pub grant_uses_mysql_user_syntax: bool, /// Emit a comment that FK changes may need table rebuild (SQLite-family). pub warn_fk_needs_table_rebuild: bool, pub create_table_if_not_exists: bool, pub create_index_if_not_exists: bool, pub create_function_or_replace: bool, pub supports_function_ddl: bool, pub supports_sequence_ddl: bool, pub supports_rule_ddl: bool, pub supports_owner_ddl: bool, /// `{create_kw} {name} {definition};` — `create_kw` is CREATE [OR REPLACE] FUNCTION. pub function_create_template: Option<&'static str>, /// `DROP FUNCTION IF EXISTS {name}{cascade};` pub function_drop_template: Option<&'static str>, /// `CREATE SEQUENCE {name} AS {data_type} START WITH {start_value} … {cycle};` pub sequence_create_template: Option<&'static str>, /// `ALTER SEQUENCE {name} AS {data_type} START WITH {start_value} … {cycle};` pub sequence_alter_template: Option<&'static str>, /// `DROP SEQUENCE {name}{cascade};` pub sequence_drop_template: Option<&'static str>, /// `DROP RULE IF EXISTS {rule_name} ON {table_name};` pub rule_drop_template: Option<&'static str>, /// `ALTER {object_type} {name} OWNER TO {owner};` pub owner_alter_template: Option<&'static str>, /// `DROP TABLE {table}{cascade};` — the caller quotes `{table}` itself. Engines /// that drop a differently-named object override the whole shape. pub drop_table_template: &'static str, /// Optional session lock-timeout preamble for generated scripts. pub lock_timeout_sql: Option<&'static str>, /// Data-driven base-type rewrites for this target (empty → rely on matrix / normalize only). pub type_map: &'static [TypeMapEntry], } impl DdlDialectProfile { pub fn lookup_type(&self, source_base_upper: &str) -> Option<&'static str> { self.type_map.iter().find(|e| e.source_base.eq_ignore_ascii_case(source_base_upper)).map(|e| e.target_template) } pub fn quote_ident(&self, name: &str) -> String { match self.quote { QuoteStyle::Backtick => format!("`{}`", name.replace('`', "``")), QuoteStyle::DoubleQuote => format!("\"{}\"", name.replace('"', "\"\"")), QuoteStyle::Brackets => format!("[{}]", name.replace(']', "]]")), // Oracle: unquoted identifiers fold to uppercase. Mixed-case names and // special characters must be double-quoted with original spelling preserved. QuoteStyle::UnquotedUpper => { let has_lower = name.chars().any(|c| c.is_ascii_lowercase()); let has_upper = name.chars().any(|c| c.is_ascii_uppercase()); let has_special = name.chars().any(|c| !c.is_ascii_alphanumeric() && c != '_' && c != '$' && c != '#'); let bad_start = name.is_empty() || !name.chars().next().is_some_and(|c| c.is_ascii_alphabetic() || c == '_'); // All-lower "emp" stays unquoted → EMP (Oracle fold). Mixed "empMixed" → "empMixed". let needs_quotes = (has_lower && has_upper) || has_special || bad_start; if needs_quotes { format!("\"{}\"", name.replace('"', "\"\"")) } else { name.to_uppercase() } } } } pub fn alter_modify_keyword(&self) -> &'static str { if self.database_type == DatabaseType::Dameng { "MODIFY" } else { "MODIFY COLUMN" } } /// Replace `{key}` placeholders. Unknown keys are left unchanged. pub fn render_template(template: &str, vars: &[(&str, &str)]) -> String { let mut out = template.to_string(); for (key, value) in vars { out = out.replace(&format!("{{{key}}}"), value); } out } } const FN_CREATE: &str = "{create_kw} {name} {definition};"; const FN_DROP: &str = "DROP FUNCTION IF EXISTS {name}{cascade};"; const SEQ_CREATE: &str = "CREATE SEQUENCE {name} AS {data_type} START WITH {start_value} INCREMENT BY {increment} MINVALUE {min_value} MAXVALUE {max_value} {cycle};"; const SEQ_ALTER: &str = "ALTER SEQUENCE {name} AS {data_type} START WITH {start_value} INCREMENT BY {increment} MINVALUE {min_value} MAXVALUE {max_value} {cycle};"; const SEQ_DROP: &str = "DROP SEQUENCE {name}{cascade};"; const SQLSERVER_FN_DROP: &str = "DROP FUNCTION {name};"; const SQLSERVER_SEQ_ALTER: &str = "ALTER SEQUENCE {name} RESTART WITH {start_value} INCREMENT BY {increment} MINVALUE {min_value} MAXVALUE {max_value} {cycle};"; const RULE_DROP: &str = "DROP RULE IF EXISTS {rule_name} ON {table_name}{cascade};"; const OWNER_ALTER: &str = "ALTER {object_type} {name} OWNER TO {owner};"; /// Shared ANSI `DROP TABLE` shape. `{if_exists}` is deliberately absent: whether a /// dialect accepts `IF EXISTS` is a capability flag, not part of the rendered shape. const DROP_TABLE: &str = "DROP TABLE {table}{cascade};"; // --------------------------------------------------------------------------- // Type maps (data only — no per-database logic at call sites) // --------------------------------------------------------------------------- const ACCESS_TYPE_MAP: &[TypeMapEntry] = &[ TypeMapEntry { source_base: "BOOL", target_template: "YESNO" }, TypeMapEntry { source_base: "BOOLEAN", target_template: "YESNO" }, TypeMapEntry { source_base: "BIT", target_template: "YESNO" }, TypeMapEntry { source_base: "YESNO", target_template: "YESNO" }, TypeMapEntry { source_base: "TINYINT", target_template: "BYTE" }, TypeMapEntry { source_base: "BYTE", target_template: "BYTE" }, TypeMapEntry { source_base: "SMALLINT", target_template: "SMALLINT" }, TypeMapEntry { source_base: "SHORT", target_template: "SMALLINT" }, TypeMapEntry { source_base: "MEDIUMINT", target_template: "INTEGER" }, TypeMapEntry { source_base: "INT", target_template: "INTEGER" }, TypeMapEntry { source_base: "INTEGER", target_template: "INTEGER" }, TypeMapEntry { source_base: "INT4", target_template: "INTEGER" }, TypeMapEntry { source_base: "LONG", target_template: "INTEGER" }, TypeMapEntry { source_base: "BIGINT", target_template: "DECIMAL(20,0)" }, TypeMapEntry { source_base: "INT8", target_template: "DECIMAL(20,0)" }, TypeMapEntry { source_base: "FLOAT", target_template: "SINGLE" }, TypeMapEntry { source_base: "REAL", target_template: "SINGLE" }, TypeMapEntry { source_base: "SINGLE", target_template: "SINGLE" }, TypeMapEntry { source_base: "DOUBLE", target_template: "DOUBLE" }, TypeMapEntry { source_base: "DOUBLE PRECISION", target_template: "DOUBLE" }, TypeMapEntry { source_base: "DECIMAL", target_template: "DECIMAL({})" }, TypeMapEntry { source_base: "NUMERIC", target_template: "DECIMAL({})" }, TypeMapEntry { source_base: "NUMBER", target_template: "DECIMAL({})" }, TypeMapEntry { source_base: "CURRENCY", target_template: "CURRENCY" }, TypeMapEntry { source_base: "VARCHAR", target_template: "TEXT({})" }, TypeMapEntry { source_base: "CHARACTER VARYING", target_template: "TEXT({})" }, TypeMapEntry { source_base: "CHAR", target_template: "TEXT({})" }, TypeMapEntry { source_base: "CHARACTER", target_template: "TEXT({})" }, TypeMapEntry { source_base: "NVARCHAR", target_template: "TEXT({})" }, TypeMapEntry { source_base: "NVARCHAR2", target_template: "TEXT({})" }, TypeMapEntry { source_base: "VARCHAR2", target_template: "TEXT({})" }, TypeMapEntry { source_base: "TEXT", target_template: "LONGTEXT" }, TypeMapEntry { source_base: "TINYTEXT", target_template: "LONGTEXT" }, TypeMapEntry { source_base: "MEDIUMTEXT", target_template: "LONGTEXT" }, TypeMapEntry { source_base: "LONGTEXT", target_template: "LONGTEXT" }, TypeMapEntry { source_base: "CLOB", target_template: "LONGTEXT" }, TypeMapEntry { source_base: "MEMO", target_template: "LONGTEXT" }, TypeMapEntry { source_base: "DATE", target_template: "DATETIME" }, TypeMapEntry { source_base: "DATETIME", target_template: "DATETIME" }, TypeMapEntry { source_base: "TIMESTAMP", target_template: "DATETIME" }, TypeMapEntry { source_base: "TIME", target_template: "DATETIME" }, TypeMapEntry { source_base: "YEAR", target_template: "SMALLINT" }, TypeMapEntry { source_base: "BLOB", target_template: "OLEOBJECT" }, TypeMapEntry { source_base: "TINYBLOB", target_template: "OLEOBJECT" }, TypeMapEntry { source_base: "MEDIUMBLOB", target_template: "OLEOBJECT" }, TypeMapEntry { source_base: "LONGBLOB", target_template: "OLEOBJECT" }, TypeMapEntry { source_base: "BINARY", target_template: "OLEOBJECT" }, TypeMapEntry { source_base: "VARBINARY", target_template: "OLEOBJECT" }, TypeMapEntry { source_base: "IMAGE", target_template: "OLEOBJECT" }, TypeMapEntry { source_base: "JSON", target_template: "LONGTEXT" }, TypeMapEntry { source_base: "JSONB", target_template: "LONGTEXT" }, TypeMapEntry { source_base: "UUID", target_template: "GUID" }, TypeMapEntry { source_base: "UNIQUEIDENTIFIER", target_template: "GUID" }, TypeMapEntry { source_base: "GUID", target_template: "GUID" }, ]; const SQLITE_TYPE_MAP: &[TypeMapEntry] = &[ TypeMapEntry { source_base: "TINYINT", target_template: "INTEGER" }, TypeMapEntry { source_base: "SMALLINT", target_template: "INTEGER" }, TypeMapEntry { source_base: "MEDIUMINT", target_template: "INTEGER" }, TypeMapEntry { source_base: "INT", target_template: "INTEGER" }, TypeMapEntry { source_base: "INTEGER", target_template: "INTEGER" }, TypeMapEntry { source_base: "BIGINT", target_template: "INTEGER" }, TypeMapEntry { source_base: "FLOAT", target_template: "REAL" }, TypeMapEntry { source_base: "DOUBLE", target_template: "REAL" }, TypeMapEntry { source_base: "DOUBLE PRECISION", target_template: "REAL" }, TypeMapEntry { source_base: "DECIMAL", target_template: "NUMERIC" }, TypeMapEntry { source_base: "NUMERIC", target_template: "NUMERIC" }, TypeMapEntry { source_base: "VARCHAR", target_template: "TEXT" }, TypeMapEntry { source_base: "CHAR", target_template: "TEXT" }, TypeMapEntry { source_base: "TEXT", target_template: "TEXT" }, TypeMapEntry { source_base: "TINYTEXT", target_template: "TEXT" }, TypeMapEntry { source_base: "MEDIUMTEXT", target_template: "TEXT" }, TypeMapEntry { source_base: "LONGTEXT", target_template: "TEXT" }, TypeMapEntry { source_base: "DATETIME", target_template: "TEXT" }, TypeMapEntry { source_base: "TIMESTAMP", target_template: "TEXT" }, TypeMapEntry { source_base: "DATE", target_template: "TEXT" }, TypeMapEntry { source_base: "BLOB", target_template: "BLOB" }, TypeMapEntry { source_base: "JSON", target_template: "TEXT" }, ]; // --------------------------------------------------------------------------- // Profile families (shared shapes; registration is the only DatabaseType match) // --------------------------------------------------------------------------- fn mysql_family(db: DatabaseType) -> DdlDialectProfile { DdlDialectProfile { database_type: db, quote: QuoteStyle::Backtick, auto_inc: AutoIncSyntax::Suffix(" AUTO_INCREMENT"), supports_display_width: true, requires_explicit_varchar_length: true, max_varchar_len: Some(65_535), inline_column_comment: true, foreign_keys_inline_in_create: false, drop_index_uses_on_table: true, drop_fk_as_foreign_key: true, drop_table_supports_cascade: false, drop_table_supports_if_exists: true, index_type_placement: IndexTypePlacement::UsingBeforeOn, index_supports_include: false, index_supports_filter: false, index_supports_comment: true, table_comment_via_alter: true, column_comment_via_modify_only: true, comment_ddl: CommentDdlSyntax::Generic, trigger_template: TriggerTemplate::MysqlStyle, rename_column: RenameColumnSyntax::MysqlChangeColumn, column_ddl: ColumnDdlSyntax::Generic, prefers_native_source_ddl: true, grant_uses_mysql_user_syntax: true, warn_fk_needs_table_rebuild: false, alter_uses_modify_column: true, alter_batches_clauses: true, create_table_if_not_exists: true, create_index_if_not_exists: false, create_function_or_replace: false, supports_function_ddl: false, supports_sequence_ddl: false, supports_rule_ddl: false, supports_owner_ddl: false, function_create_template: None, function_drop_template: None, sequence_create_template: None, sequence_alter_template: None, sequence_drop_template: None, rule_drop_template: None, owner_alter_template: None, drop_table_template: DROP_TABLE, lock_timeout_sql: Some("SET SESSION lock_wait_timeout = 3;"), type_map: &[], } } fn postgres_family(db: DatabaseType) -> DdlDialectProfile { DdlDialectProfile { database_type: db, quote: QuoteStyle::DoubleQuote, auto_inc: AutoIncSyntax::PostgresSequence, supports_display_width: false, requires_explicit_varchar_length: false, max_varchar_len: None, inline_column_comment: false, foreign_keys_inline_in_create: false, drop_index_uses_on_table: false, drop_fk_as_foreign_key: false, drop_table_supports_cascade: true, drop_table_supports_if_exists: true, index_type_placement: IndexTypePlacement::UsingSuffix, index_supports_include: true, index_supports_filter: true, index_supports_comment: false, table_comment_via_alter: false, column_comment_via_modify_only: false, comment_ddl: CommentDdlSyntax::Generic, trigger_template: TriggerTemplate::PostgresStyle, rename_column: RenameColumnSyntax::RenameColumn, column_ddl: ColumnDdlSyntax::Generic, prefers_native_source_ddl: false, grant_uses_mysql_user_syntax: false, warn_fk_needs_table_rebuild: false, alter_uses_modify_column: false, alter_batches_clauses: false, create_table_if_not_exists: false, create_index_if_not_exists: true, create_function_or_replace: true, supports_function_ddl: true, supports_sequence_ddl: true, supports_rule_ddl: true, supports_owner_ddl: true, function_create_template: Some(FN_CREATE), function_drop_template: Some(FN_DROP), sequence_create_template: Some(SEQ_CREATE), sequence_alter_template: Some(SEQ_ALTER), sequence_drop_template: Some(SEQ_DROP), rule_drop_template: Some(RULE_DROP), owner_alter_template: Some(OWNER_ALTER), drop_table_template: DROP_TABLE, lock_timeout_sql: Some("SET lock_timeout = '3s';"), type_map: &[], } } fn oracle_family(db: DatabaseType) -> DdlDialectProfile { DdlDialectProfile { database_type: db, quote: QuoteStyle::UnquotedUpper, auto_inc: AutoIncSyntax::Suffix(" GENERATED BY DEFAULT AS IDENTITY"), supports_display_width: false, // Oracle's VARCHAR2 also rejects a missing length in real DDL, but this // is left false (unverified, no Oracle instance in scope for this // change) rather than asserting untested cross-dialect behavior. requires_explicit_varchar_length: false, max_varchar_len: Some(4000), inline_column_comment: false, foreign_keys_inline_in_create: false, drop_index_uses_on_table: false, drop_fk_as_foreign_key: false, // Oracle spells it `CASCADE CONSTRAINTS`, and `IF EXISTS` only landed in 23c. drop_table_supports_cascade: false, drop_table_supports_if_exists: false, index_type_placement: IndexTypePlacement::None, index_supports_include: false, index_supports_filter: false, index_supports_comment: false, table_comment_via_alter: false, column_comment_via_modify_only: false, comment_ddl: CommentDdlSyntax::Generic, trigger_template: TriggerTemplate::GenericRowBody, rename_column: RenameColumnSyntax::RenameColumn, column_ddl: ColumnDdlSyntax::Generic, prefers_native_source_ddl: false, grant_uses_mysql_user_syntax: false, warn_fk_needs_table_rebuild: false, alter_uses_modify_column: false, alter_batches_clauses: false, create_table_if_not_exists: false, create_index_if_not_exists: false, create_function_or_replace: false, supports_function_ddl: true, supports_sequence_ddl: true, supports_rule_ddl: false, supports_owner_ddl: false, // Same ANSI shapes as Postgres for the subset currently generated by schema_diff. function_create_template: Some(FN_CREATE), function_drop_template: Some(FN_DROP), sequence_create_template: Some(SEQ_CREATE), sequence_alter_template: Some(SEQ_ALTER), sequence_drop_template: Some(SEQ_DROP), rule_drop_template: None, owner_alter_template: None, drop_table_template: DROP_TABLE, lock_timeout_sql: Some("ALTER SESSION SET DDL_LOCK_TIMEOUT = 3;"), type_map: &[], } } fn sqlserver_family(db: DatabaseType) -> DdlDialectProfile { DdlDialectProfile { database_type: db, quote: QuoteStyle::Brackets, auto_inc: AutoIncSyntax::Suffix(" IDENTITY(1,1)"), supports_display_width: false, requires_explicit_varchar_length: false, max_varchar_len: None, inline_column_comment: false, foreign_keys_inline_in_create: false, drop_index_uses_on_table: false, drop_fk_as_foreign_key: false, drop_table_supports_cascade: false, // `DROP TABLE IF EXISTS` is valid on SQL Server 2016+. drop_table_supports_if_exists: true, index_type_placement: IndexTypePlacement::TypePrefix, index_supports_include: true, index_supports_filter: true, index_supports_comment: false, table_comment_via_alter: false, column_comment_via_modify_only: false, comment_ddl: CommentDdlSyntax::SqlServerExtendedProperty, trigger_template: TriggerTemplate::SqlServerStyle, rename_column: RenameColumnSyntax::SqlServerSpRename, column_ddl: ColumnDdlSyntax::SqlServer, prefers_native_source_ddl: false, grant_uses_mysql_user_syntax: false, warn_fk_needs_table_rebuild: false, alter_uses_modify_column: false, alter_batches_clauses: false, create_table_if_not_exists: false, create_index_if_not_exists: false, create_function_or_replace: false, supports_function_ddl: true, supports_sequence_ddl: true, supports_rule_ddl: false, supports_owner_ddl: false, function_create_template: Some(FN_CREATE), function_drop_template: Some(SQLSERVER_FN_DROP), sequence_create_template: Some(SEQ_CREATE), sequence_alter_template: Some(SQLSERVER_SEQ_ALTER), sequence_drop_template: Some(SEQ_DROP), rule_drop_template: None, owner_alter_template: None, drop_table_template: DROP_TABLE, lock_timeout_sql: Some("SET LOCK_TIMEOUT 3000;"), type_map: &[], } } fn sqlite_family(db: DatabaseType) -> DdlDialectProfile { DdlDialectProfile { database_type: db, quote: QuoteStyle::DoubleQuote, auto_inc: AutoIncSyntax::Suffix(" AUTOINCREMENT"), supports_display_width: false, requires_explicit_varchar_length: false, max_varchar_len: None, inline_column_comment: false, foreign_keys_inline_in_create: true, drop_index_uses_on_table: false, drop_fk_as_foreign_key: false, drop_table_supports_cascade: false, drop_table_supports_if_exists: true, index_type_placement: IndexTypePlacement::None, index_supports_include: false, index_supports_filter: true, index_supports_comment: false, table_comment_via_alter: false, column_comment_via_modify_only: false, comment_ddl: CommentDdlSyntax::Generic, trigger_template: TriggerTemplate::GenericRowBody, rename_column: RenameColumnSyntax::RenameColumn, column_ddl: ColumnDdlSyntax::Generic, prefers_native_source_ddl: false, grant_uses_mysql_user_syntax: false, warn_fk_needs_table_rebuild: true, alter_uses_modify_column: false, alter_batches_clauses: false, create_table_if_not_exists: true, create_index_if_not_exists: true, create_function_or_replace: false, supports_function_ddl: false, supports_sequence_ddl: false, supports_rule_ddl: false, supports_owner_ddl: false, function_create_template: None, function_drop_template: None, sequence_create_template: None, sequence_alter_template: None, sequence_drop_template: None, rule_drop_template: None, owner_alter_template: None, drop_table_template: DROP_TABLE, lock_timeout_sql: None, type_map: SQLITE_TYPE_MAP, } } fn access_profile() -> DdlDialectProfile { DdlDialectProfile { database_type: DatabaseType::Access, quote: QuoteStyle::DoubleQuote, auto_inc: AutoIncSyntax::ReplaceTypeWith("COUNTER"), supports_display_width: false, requires_explicit_varchar_length: false, max_varchar_len: Some(255), inline_column_comment: false, foreign_keys_inline_in_create: false, drop_index_uses_on_table: false, drop_fk_as_foreign_key: false, // Jet SQL has neither CASCADE nor IF EXISTS on DROP TABLE. drop_table_supports_cascade: false, drop_table_supports_if_exists: false, index_type_placement: IndexTypePlacement::None, index_supports_include: false, index_supports_filter: false, index_supports_comment: false, table_comment_via_alter: false, column_comment_via_modify_only: false, comment_ddl: CommentDdlSyntax::Generic, trigger_template: TriggerTemplate::GenericRowBody, rename_column: RenameColumnSyntax::RenameColumn, column_ddl: ColumnDdlSyntax::Generic, prefers_native_source_ddl: false, grant_uses_mysql_user_syntax: false, warn_fk_needs_table_rebuild: false, alter_uses_modify_column: false, alter_batches_clauses: false, create_table_if_not_exists: false, create_index_if_not_exists: false, create_function_or_replace: false, supports_function_ddl: false, supports_sequence_ddl: false, supports_rule_ddl: false, supports_owner_ddl: false, function_create_template: None, function_drop_template: None, sequence_create_template: None, sequence_alter_template: None, sequence_drop_template: None, rule_drop_template: None, owner_alter_template: None, drop_table_template: DROP_TABLE, lock_timeout_sql: None, type_map: ACCESS_TYPE_MAP, } } fn h2_profile() -> DdlDialectProfile { DdlDialectProfile { database_type: DatabaseType::H2, quote: QuoteStyle::DoubleQuote, auto_inc: AutoIncSyntax::None, supports_display_width: false, requires_explicit_varchar_length: false, max_varchar_len: None, inline_column_comment: false, foreign_keys_inline_in_create: false, drop_index_uses_on_table: false, drop_fk_as_foreign_key: false, // H2 accepts CASCADE, but it stays off to keep the cascade set aligned with // the frontend preview list in `dbAdminSql.ts`. drop_table_supports_cascade: false, drop_table_supports_if_exists: true, index_type_placement: IndexTypePlacement::None, index_supports_include: false, index_supports_filter: false, index_supports_comment: false, table_comment_via_alter: false, column_comment_via_modify_only: false, comment_ddl: CommentDdlSyntax::Generic, trigger_template: TriggerTemplate::GenericRowBody, rename_column: RenameColumnSyntax::AlterColumnRenameTo, column_ddl: ColumnDdlSyntax::Generic, prefers_native_source_ddl: false, grant_uses_mysql_user_syntax: false, warn_fk_needs_table_rebuild: false, alter_uses_modify_column: false, alter_batches_clauses: false, create_table_if_not_exists: false, create_index_if_not_exists: false, create_function_or_replace: false, supports_function_ddl: false, supports_sequence_ddl: false, supports_rule_ddl: false, supports_owner_ddl: false, function_create_template: None, function_drop_template: None, sequence_create_template: None, sequence_alter_template: None, sequence_drop_template: None, rule_drop_template: None, owner_alter_template: None, drop_table_template: DROP_TABLE, lock_timeout_sql: None, type_map: &[], } } fn conservative_ansi(db: DatabaseType) -> DdlDialectProfile { DdlDialectProfile { database_type: db, quote: QuoteStyle::DoubleQuote, auto_inc: AutoIncSyntax::None, supports_display_width: false, // This fallback also covers DB2 and Informix, which reject a bare // VARCHAR with no length — but it's shared with dialects that don't // (Snowflake, BigQuery, DuckDB, Trino, ...), so it can't be flipped // true here without a dedicated DB2/Informix profile and DB // verification. Tracked as a known gap, not fixed by this flag. requires_explicit_varchar_length: false, max_varchar_len: None, inline_column_comment: false, foreign_keys_inline_in_create: false, drop_index_uses_on_table: false, drop_fk_as_foreign_key: false, drop_table_supports_cascade: false, // Most engines in this bucket accept `IF EXISTS`; `profile_for` opts out the // ones that do not. drop_table_supports_if_exists: true, index_type_placement: IndexTypePlacement::None, index_supports_include: false, index_supports_filter: false, index_supports_comment: false, table_comment_via_alter: false, column_comment_via_modify_only: false, comment_ddl: CommentDdlSyntax::Generic, trigger_template: TriggerTemplate::GenericRowBody, rename_column: RenameColumnSyntax::RenameColumn, column_ddl: ColumnDdlSyntax::Generic, prefers_native_source_ddl: false, grant_uses_mysql_user_syntax: false, warn_fk_needs_table_rebuild: false, alter_uses_modify_column: false, alter_batches_clauses: false, create_table_if_not_exists: false, create_index_if_not_exists: false, create_function_or_replace: false, supports_function_ddl: false, supports_sequence_ddl: false, supports_rule_ddl: false, supports_owner_ddl: false, function_create_template: None, function_drop_template: None, sequence_create_template: None, sequence_alter_template: None, sequence_drop_template: None, rule_drop_template: None, owner_alter_template: None, drop_table_template: DROP_TABLE, lock_timeout_sql: None, type_map: &[], } } fn clickhouse_profile() -> DdlDialectProfile { let mut p = conservative_ansi(DatabaseType::ClickHouse); // ClickHouse supports CREATE TABLE IF NOT EXISTS like MySQL/SQLite. p.create_table_if_not_exists = true; p.lock_timeout_sql = None; p } fn influxdb_profile() -> DdlDialectProfile { let mut p = conservative_ansi(DatabaseType::InfluxDb); // InfluxDB drops a measurement, not a table, and has no IF EXISTS form. p.drop_table_template = "DROP MEASUREMENT {table};"; p.drop_table_supports_if_exists = false; p } /// Resolve DDL profile for a concrete target database type. /// /// This is the **only** place that maps [`DatabaseType`] → profile data. /// Generators must not re-match on individual databases afterward. pub fn profile_for(db_type: DatabaseType) -> DdlDialectProfile { use DatabaseType::*; match db_type { // MySQL family Mysql | Doris | StarRocks | Goldendb | Sundb | Databend | Gbase | ManticoreSearch => mysql_family(db_type), Dameng => { let mut profile = oracle_family(db_type); profile.alter_uses_modify_column = true; profile } // PostgreSQL family Postgres | Redshift | Gaussdb | Kingbase | Highgo | Vastbase | OpenGauss | Kwdb | Uxdb => { postgres_family(db_type) } // PostgreSQL-shaped DDL, but not the PostgreSQL `DROP TABLE` grammar: // Firebird has neither CASCADE nor IF EXISTS; Vertica and Exasol are kept off // CASCADE so the set stays aligned with the frontend preview list. Firebird | Vertica | Exasol => { let mut profile = postgres_family(db_type); profile.drop_table_supports_cascade = false; profile.drop_table_supports_if_exists = db_type != Firebird; profile } // Oracle family Oracle | OceanbaseOracle | Yashandb | Xugu | Iris => oracle_family(db_type), // SQL Server (not Access) SqlServer => sqlserver_family(db_type), // Access: own profile (must not inherit SqlServer IDENTITY) Access => access_profile(), // SQLite family Sqlite | Rqlite | Turso | CloudflareD1 => sqlite_family(db_type), H2 => h2_profile(), ClickHouse => clickhouse_profile(), // Engines whose DROP TABLE grammar has no `IF EXISTS`. Db2 | Informix | SapHana | Teradata => { let mut profile = conservative_ansi(db_type); profile.drop_table_supports_if_exists = false; profile } DuckDb | Questdb | Snowflake | Trino | PrestoSql | Hive | Kyuubi | Impala | Argo | Spark | Bigquery | Spanner | Kylin | Ignite | Ignite3 | Oscar | Tdengine | Iotdb | Databricks | Jdbc => { conservative_ansi(db_type) } InfluxDb => influxdb_profile(), Plugin => conservative_ansi(db_type), // Non-tabular / not applicable for relational CREATE TABLE Redis | MongoDb | DynamoDb | Elasticsearch | Easysearch | Meilisearch | Qdrant | Milvus | Weaviate | ChromaDb | Neo4j | Cassandra | Etcd | ZooKeeper | Nacos | Consul | InfluxDb3 | VictoriaMetrics | MessageQueue | Mqtt | Hbase => conservative_ansi(db_type), } } #[cfg(test)] mod tests { use super::*; #[test] fn access_profile_is_not_sqlserver_identity() { let access = profile_for(DatabaseType::Access); let mssql = profile_for(DatabaseType::SqlServer); assert!(matches!(access.auto_inc, AutoIncSyntax::ReplaceTypeWith("COUNTER"))); assert!(matches!(mssql.auto_inc, AutoIncSyntax::Suffix(s) if s.contains("IDENTITY"))); assert!(access.lookup_type("VARCHAR").is_some()); assert!(mssql.type_map.is_empty()); // Access must not inherit SQL Server lock-timeout behavior. Neither // database supports CREATE INDEX IF NOT EXISTS syntax. assert_eq!(access.lock_timeout_sql, None); assert!(mssql.lock_timeout_sql.is_some()); assert!(!access.create_index_if_not_exists); assert!(!mssql.create_index_if_not_exists); } #[test] fn oracle_profile_uses_valid_identity_and_ddl_lock_timeout() { let oracle = profile_for(DatabaseType::Oracle); assert!(matches!( oracle.auto_inc, AutoIncSyntax::Suffix(s) if s.contains("GENERATED BY DEFAULT AS IDENTITY") )); assert_eq!(oracle.lock_timeout_sql, Some("ALTER SESSION SET DDL_LOCK_TIMEOUT = 3;")); } #[test] fn quote_styles() { assert_eq!(profile_for(DatabaseType::Mysql).quote_ident("a`b"), "`a``b`"); assert_eq!(profile_for(DatabaseType::Access).quote_ident(r#"a"b"#), "\"a\"\"b\""); assert_eq!(profile_for(DatabaseType::SqlServer).quote_ident("a]b"), "[a]]b]"); assert_eq!(profile_for(DatabaseType::Oracle).quote_ident("emp"), "EMP"); assert_eq!(profile_for(DatabaseType::Oracle).quote_ident("EMP"), "EMP"); // Mixed-case must preserve spelling via quotes (not EMPMIXED). assert_eq!(profile_for(DatabaseType::Oracle).quote_ident("empMixed"), "\"empMixed\""); assert_eq!(profile_for(DatabaseType::Oracle).quote_ident("Emp"), "\"Emp\""); } #[test] fn postgres_object_templates_are_populated() { let p = profile_for(DatabaseType::Postgres); assert!(p.function_create_template.is_some()); assert!(p.function_drop_template.is_some()); assert!(p.sequence_create_template.is_some()); assert!(p.sequence_alter_template.is_some()); assert!(p.sequence_drop_template.is_some()); assert!(p.rule_drop_template.is_some()); assert!(p.owner_alter_template.is_some()); } #[test] fn mysql_object_templates_are_absent() { let p = profile_for(DatabaseType::Mysql); assert!(p.function_create_template.is_none()); assert!(p.sequence_create_template.is_none()); assert!(p.rule_drop_template.is_none()); assert!(p.owner_alter_template.is_none()); } #[test] fn render_template_replaces_placeholders() { let rendered = DdlDialectProfile::render_template( "DROP FUNCTION IF EXISTS {name}{cascade};", &[("name", "\"public\".\"f\""), ("cascade", " CASCADE")], ); assert_eq!(rendered, "DROP FUNCTION IF EXISTS \"public\".\"f\" CASCADE;"); } #[test] fn drop_table_if_exists_support_matches_dialect_grammar() { for db in [ DatabaseType::Mysql, DatabaseType::Postgres, DatabaseType::SqlServer, DatabaseType::Sqlite, DatabaseType::DuckDb, DatabaseType::ClickHouse, DatabaseType::H2, DatabaseType::Snowflake, ] { assert!(profile_for(db).drop_table_supports_if_exists, "{db:?} does support DROP TABLE IF EXISTS"); } // Oracle only gained `IF EXISTS` in 23c; the rest have no such grammar at all. for db in [ DatabaseType::Oracle, DatabaseType::Dameng, DatabaseType::OceanbaseOracle, DatabaseType::Access, DatabaseType::Firebird, DatabaseType::Db2, DatabaseType::Informix, DatabaseType::SapHana, DatabaseType::Teradata, DatabaseType::InfluxDb, ] { assert!(!profile_for(db).drop_table_supports_if_exists, "{db:?} does not support DROP TABLE IF EXISTS"); } } #[test] fn drop_table_cascade_support_is_limited_to_the_postgres_grammar() { for db in [ DatabaseType::Postgres, DatabaseType::Redshift, DatabaseType::Gaussdb, DatabaseType::Kwdb, DatabaseType::Kingbase, DatabaseType::Highgo, DatabaseType::Uxdb, DatabaseType::Vastbase, DatabaseType::OpenGauss, ] { assert!(profile_for(db).drop_table_supports_cascade, "{db:?} should keep DROP TABLE … CASCADE"); } // Oracle's `CASCADE CONSTRAINTS` is a different clause; Firebird, Vertica and Exasol // share the PostgreSQL profile but stay out of the cascade set so the backend matches // the frontend preview list in `dbAdminSql.ts`. for db in [ DatabaseType::Oracle, DatabaseType::Firebird, DatabaseType::Vertica, DatabaseType::Exasol, DatabaseType::Mysql, DatabaseType::Sqlite, DatabaseType::DuckDb, DatabaseType::SqlServer, DatabaseType::H2, ] { assert!(!profile_for(db).drop_table_supports_cascade, "{db:?} must not emit DROP TABLE … CASCADE"); } } #[test] fn drop_table_template_carries_the_whole_statement_shape() { assert_eq!(profile_for(DatabaseType::Postgres).drop_table_template, "DROP TABLE {table}{cascade};"); assert_eq!(profile_for(DatabaseType::InfluxDb).drop_table_template, "DROP MEASUREMENT {table};"); } }