feat(sqlite): support autoincrement primary key in structure editor

This commit is contained in:
二丫讲梵 authored and GitHub committed 2026-09-13 02:11:17 +08:00
1 parent b8f39f9c24
commit 3767f4a83b
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")]);