feat(sqlite): support autoincrement primary key in structure editor
This commit is contained in:
9 files changed
+285
-13
No files matched your search
@@ -984,10 +984,23 @@ pub(super) fn build_sqlite_existing_column_sql(
|
||||
return Vec::new();
|
||||
};
|
||||
let mut statements = Vec::new();
|
||||
// Tri-state: an unset auto_increment flag inherits the original value; only an
|
||||
// explicit toggle (SQLite cannot add or drop AUTOINCREMENT without a rebuild)
|
||||
// counts as an unsupported change.
|
||||
let original_auto_increment = original.extra.as_deref().is_some_and(|extra| {
|
||||
let lower = extra.to_ascii_lowercase();
|
||||
lower.contains("auto_increment")
|
||||
|| lower.contains("autoincrement")
|
||||
|| lower.contains("identity")
|
||||
|| lower.contains("serial")
|
||||
});
|
||||
let auto_increment_changed =
|
||||
column.extra.as_ref().and_then(|e| e.auto_increment).is_some_and(|value| value != original_auto_increment);
|
||||
let unsupported_change = column.data_type.trim() != original.data_type.trim()
|
||||
|| column.is_nullable != original.is_nullable
|
||||
|| normalize_default(Some(&column.default_value)) != original_default(column)
|
||||
|| clean(&column.comment) != original_comment(column);
|
||||
|| clean(&column.comment) != original_comment(column)
|
||||
|| auto_increment_changed;
|
||||
if column.name != original.name {
|
||||
statements.push(format!(
|
||||
"ALTER TABLE {table} RENAME COLUMN {} TO {};",
|
||||
|
||||
@@ -113,6 +113,13 @@ pub(super) fn original_is_mysql_generated_column(column: &EditableStructureColum
|
||||
pub(super) fn column_extra_clause(dialect: StructureDialect, column: &EditableStructureColumn) -> Option<String> {
|
||||
let extra = column.extra.as_ref()?;
|
||||
match dialect {
|
||||
StructureDialect::Sqlite => {
|
||||
if extra.auto_increment.unwrap_or(false) && column.is_primary_key {
|
||||
Some("AUTOINCREMENT".to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
StructureDialect::Mysql => {
|
||||
let mut clauses = Vec::new();
|
||||
if extra.auto_increment.unwrap_or(false) {
|
||||
|
||||
@@ -16,6 +16,13 @@ use super::util::{
|
||||
use super::validation::{validate_columns, validate_concurrent_index_scope, validate_dameng_identity};
|
||||
use crate::models::connection::DatabaseType;
|
||||
|
||||
fn is_sqlite_integer_family_type(data_type: &str) -> bool {
|
||||
let normalized = data_type.trim().to_ascii_lowercase();
|
||||
["int", "integer", "tinyint", "smallint", "mediumint", "bigint"]
|
||||
.iter()
|
||||
.any(|candidate| normalized == *candidate || normalized.starts_with(&format!("{candidate}(")))
|
||||
}
|
||||
|
||||
pub fn build_create_table_sql(mut options: TableStructureSqlOptions) -> TableStructureSqlResult {
|
||||
let capabilities;
|
||||
let dialect;
|
||||
@@ -60,11 +67,40 @@ pub fn build_create_table_sql(mut options: TableStructureSqlOptions) -> TableStr
|
||||
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 data_type = column_data_type(dialect, column);
|
||||
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() {
|
||||
@@ -74,7 +110,12 @@ pub fn build_create_table_sql(mut options: TableStructureSqlOptions) -> TableStr
|
||||
parts.push(format!("COLLATE {}", quote_ident(dialect, &column.collation)));
|
||||
}
|
||||
}
|
||||
if !column.is_nullable
|
||||
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)
|
||||
{
|
||||
@@ -106,7 +147,12 @@ pub fn build_create_table_sql(mut options: TableStructureSqlOptions) -> TableStr
|
||||
|
||||
let pk_columns: Vec<_> = active_columns
|
||||
.iter()
|
||||
.filter(|column| column.is_primary_key && dialect != StructureDialect::ManticoreSearch)
|
||||
.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
|
||||
|
||||
@@ -78,6 +78,48 @@ fn mysql_table_engine_change_generates_alter_table() {
|
||||
assert_eq!(result.statements, vec!["ALTER TABLE `remote_orders` ENGINE = FEDERATED;"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sqlite_autoincrement_normalizes_integer_aliases_to_exact_integer() {
|
||||
let mut id = column("id");
|
||||
id.data_type = "bigint".to_string();
|
||||
id.is_nullable = false;
|
||||
id.is_primary_key = true;
|
||||
id.extra = Some(ColumnExtra { auto_increment: Some(true), ..Default::default() });
|
||||
|
||||
let options = structure_change_options(DatabaseType::Sqlite, None, "items", vec![id, column("value")]);
|
||||
let result = build_create_table_sql(options);
|
||||
assert!(result.warnings.is_empty(), "{:?}", result.warnings);
|
||||
assert_eq!(
|
||||
result.statements[0],
|
||||
"CREATE TABLE \"items\" (\n \"id\" INTEGER PRIMARY KEY AUTOINCREMENT,\n \"value\" varchar(255)\n);"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sqlite_autoincrement_rejects_composite_primary_keys_and_non_integer_types() {
|
||||
let mut a = column("a");
|
||||
a.data_type = "int".to_string();
|
||||
a.is_primary_key = true;
|
||||
a.extra = Some(ColumnExtra { auto_increment: Some(true), ..Default::default() });
|
||||
let mut b = column("b");
|
||||
b.data_type = "int".to_string();
|
||||
b.is_primary_key = true;
|
||||
|
||||
let options = structure_change_options(DatabaseType::Sqlite, None, "items", vec![a, b]);
|
||||
let result = build_create_table_sql(options);
|
||||
assert!(result.statements.is_empty());
|
||||
assert!(result.warnings.iter().any(|w| w.contains("composite primary keys")), "{:?}", result.warnings);
|
||||
|
||||
let mut text_id = column("id");
|
||||
text_id.data_type = "text".to_string();
|
||||
text_id.is_primary_key = true;
|
||||
text_id.extra = Some(ColumnExtra { auto_increment: Some(true), ..Default::default() });
|
||||
let options = structure_change_options(DatabaseType::Sqlite, None, "items2", vec![text_id]);
|
||||
let result = build_create_table_sql(options);
|
||||
assert!(result.statements.is_empty());
|
||||
assert!(result.warnings.iter().any(|w| w.contains("must use an integer type")), "{:?}", result.warnings);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mysql_create_table_includes_engine_before_comment() {
|
||||
let mut options = structure_change_options(DatabaseType::Mysql, Some("dbx_test"), "archive", vec![column("id")]);
|
||||
|
||||
Reference in new issue
Block a user