From 1b6c01857151460f427c3c2a04dc8a2342b16c06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=80=97=E5=AD=90?= Date: Fri, 31 Jul 2026 17:34:26 +0800 Subject: [PATCH] fix: don't reduce a composite primary key to its autoIncrement column MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DataTypeOf emitted "integer PRIMARY KEY AUTOINCREMENT" for any auto-increment int field. With a composite primary key GORM then skips the table-level PRIMARY KEY clause (the field type already contains PRIMARY KEY), so the other key columns silently lost their primary-key status. AUTOINCREMENT only applies to a single-column INTEGER PRIMARY KEY anyway, so it is dropped when the field is part of a multi-column key and the table-level PRIMARY KEY (a, b) is emitted instead. The single-key behavior — including autoIncrement without an explicit primaryKey tag — is unchanged. --- migrator_test.go | 36 ++++++++++++++++++++++++++++++++++++ sqlite.go | 11 ++++++++++- 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/migrator_test.go b/migrator_test.go index 301d3bc..046bb0f 100644 --- a/migrator_test.go +++ b/migrator_test.go @@ -178,3 +178,39 @@ func openRecreateTestDB(t *testing.T, name string) *gorm.DB { }) return db } + +type compositeKeyModel struct { + A int `gorm:"primaryKey;autoIncrement"` + B string `gorm:"primaryKey"` +} + +func (compositeKeyModel) TableName() string { return "composite_key_models" } + +// A composite primary key that includes an auto-increment field must keep all +// key columns. AUTOINCREMENT only applies to a single-column INTEGER PRIMARY +// KEY, and emitting it made GORM skip the table-level PRIMARY KEY clause, +// silently reducing the key to one column. +func TestCompositePrimaryKeyAutoIncrement(t *testing.T) { + db, err := gorm.Open(Open("file:composite_pk?mode=memory&cache=shared"), &gorm.Config{}) + if err != nil { + t.Fatalf("gorm.Open: %v", err) + } + t.Cleanup(func() { + if sqlDB, err := db.DB(); err == nil { + _ = sqlDB.Close() + } + }) + + if err := db.AutoMigrate(&compositeKeyModel{}); err != nil { + t.Fatalf("AutoMigrate: %v", err) + } + var pkCount int + if err := db.Raw("SELECT count(*) FROM pragma_table_info('composite_key_models') WHERE pk > 0").Scan(&pkCount).Error; err != nil { + t.Fatal(err) + } + if pkCount != 2 { + var ddl string + _ = db.Raw("SELECT sql FROM sqlite_master WHERE type='table' AND name='composite_key_models'").Scan(&ddl).Error + t.Errorf("primary key column count = %d, want 2 (DDL: %s)", pkCount, ddl) + } +} diff --git a/sqlite.go b/sqlite.go index b3c8e31..87907f4 100644 --- a/sqlite.go +++ b/sqlite.go @@ -222,7 +222,7 @@ func (dialector Dialector) dataTypeOf(field *schema.Field) string { case schema.Bool: return "numeric" case schema.Int, schema.Uint: - if field.AutoIncrement { + if field.AutoIncrement && !isCompositePrimaryKey(field) { // doesn't check `PrimaryKey`, to keep backward compatibility // https://www.sqlite.org/autoinc.html return "integer PRIMARY KEY AUTOINCREMENT" @@ -317,3 +317,12 @@ func isIdentityKeyword(value string) bool { } return identity } + +// isCompositePrimaryKey reports whether field is part of a multi-column +// primary key. AUTOINCREMENT only works on a single-column INTEGER PRIMARY +// KEY; emitting it for a composite key would silently reduce the primary key +// to that one column (GORM skips the table-level PRIMARY KEY clause when a +// field type already contains PRIMARY KEY). +func isCompositePrimaryKey(field *schema.Field) bool { + return field.PrimaryKey && field.Schema != nil && len(field.Schema.PrimaryFields) > 1 +}