972 lines
42 KiB
Rust
972 lines
42 KiB
Rust
//! 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<u32>,
|
|
/// 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};");
|
|
}
|
|
}
|