diff --git a/go/vehicle-gateway/internal/identity/resolver.go b/go/vehicle-gateway/internal/identity/resolver.go index c30b4f2c..9bb3cc1c 100644 --- a/go/vehicle-gateway/internal/identity/resolver.go +++ b/go/vehicle-gateway/internal/identity/resolver.go @@ -88,6 +88,13 @@ func (r *MySQLResolver) EnsureSchema(ctx context.Context) error { if _, err := r.db.ExecContext(ctx, identityBindingTableSQL(r.table)); err != nil { return err } + for _, statement := range identityBindingAlterSQL(r.table) { + if _, err := r.db.ExecContext(ctx, statement); err != nil { + if !isDuplicateColumnError(err) { + return err + } + } + } _, err := r.db.ExecContext(ctx, jt808RegistrationTableSQL) return err } @@ -101,6 +108,7 @@ func identityBindingTableSQL(table string) string { plate VARCHAR(32) NULL, phone VARCHAR(32) NULL, device_id VARCHAR(64) NULL, + oem VARCHAR(64) NULL, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_identity_plate (plate), UNIQUE KEY uk_identity_phone (phone), @@ -108,6 +116,20 @@ func identityBindingTableSQL(table string) string { )` } +func identityBindingAlterSQL(table string) []string { + if table == "" || !safeIdentifier(table) { + table = "vehicle_identity_binding" + } + return []string{ + `ALTER TABLE ` + table + ` ADD COLUMN oem VARCHAR(64) NULL AFTER device_id`, + } +} + +func isDuplicateColumnError(err error) bool { + text := strings.ToLower(strings.TrimSpace(fmt.Sprint(err))) + return strings.Contains(text, "duplicate column") || strings.Contains(text, "error 1060") +} + const jt808RegistrationTableSQL = `CREATE TABLE IF NOT EXISTS jt808_registration ( phone VARCHAR(32) PRIMARY KEY, device_id VARCHAR(64) NULL, diff --git a/go/vehicle-gateway/internal/identity/resolver_test.go b/go/vehicle-gateway/internal/identity/resolver_test.go index 1972d42c..f83f3ce4 100644 --- a/go/vehicle-gateway/internal/identity/resolver_test.go +++ b/go/vehicle-gateway/internal/identity/resolver_test.go @@ -3,6 +3,7 @@ package identity import ( "context" "database/sql" + "errors" "strings" "testing" "time" @@ -293,6 +294,27 @@ func TestMySQLResolverEnsuresMinimalIdentitySchema(t *testing.T) { defer db.Close() mock.ExpectExec("CREATE TABLE IF NOT EXISTS vehicle_identity_binding"). WillReturnResult(sqlmock.NewResult(0, 0)) + mock.ExpectExec("ALTER TABLE vehicle_identity_binding ADD COLUMN oem"). + WillReturnResult(sqlmock.NewResult(0, 0)) + mock.ExpectExec("CREATE TABLE IF NOT EXISTS jt808_registration"). + WillReturnResult(sqlmock.NewResult(0, 0)) + + resolver := NewMySQLResolver(db, "vehicle_identity_binding") + if err := resolver.EnsureSchema(context.Background()); err != nil { + t.Fatalf("EnsureSchema() error = %v", err) + } + if err := mock.ExpectationsWereMet(); err != nil { + t.Fatalf("sql expectations: %v", err) + } +} + +func TestMySQLResolverIgnoresExistingOEMColumn(t *testing.T) { + db, mock := newMockDB(t) + defer db.Close() + mock.ExpectExec("CREATE TABLE IF NOT EXISTS vehicle_identity_binding"). + WillReturnResult(sqlmock.NewResult(0, 0)) + mock.ExpectExec("ALTER TABLE vehicle_identity_binding ADD COLUMN oem"). + WillReturnError(errors.New("Error 1060 (42S21): Duplicate column name 'oem'")) mock.ExpectExec("CREATE TABLE IF NOT EXISTS jt808_registration"). WillReturnResult(sqlmock.NewResult(0, 0))