From 1f522e0cb268f70357fac6d9c2fe5aa5b23403a5 Mon Sep 17 00:00:00 2001 From: Chen-Wei Sun Date: Mon, 19 Oct 2026 23:40:48 +0800 Subject: [PATCH] fix(sonarqube): make file_metrics_key length migration re-runnable Migration 20230927145127 dropped the primary key of _tool_sonarqube_file_metrics unconditionally. The migrator records a script only after it succeeds, so a run interrupted after the drop failed on every later startup (PostgreSQL SQLSTATE 42704, MySQL error 1091). Drop the primary key only when it exists, and add an e2e test covering the normal run and a re-run without the primary key. Closes #9241 --- .../file_metrics_key_length_migration_test.go | 109 ++++++++++++++++++ ...20230927_modify_file_metrics_key_length.go | 15 ++- 2 files changed, 122 insertions(+), 2 deletions(-) create mode 100644 backend/plugins/sonarqube/e2e/file_metrics_key_length_migration_test.go diff --git a/backend/plugins/sonarqube/e2e/file_metrics_key_length_migration_test.go b/backend/plugins/sonarqube/e2e/file_metrics_key_length_migration_test.go new file mode 100644 index 00000000000..3fa1204386e --- /dev/null +++ b/backend/plugins/sonarqube/e2e/file_metrics_key_length_migration_test.go @@ -0,0 +1,109 @@ +/* +Licensed to the Apache Software Foundation (ASF) under one or more +contributor license agreements. See the NOTICE file distributed with +this work for additional information regarding copyright ownership. +The ASF licenses this file to You under the Apache License, Version 2.0 +(the "License"); you may not use this file except in compliance with +the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package e2e + +import ( + "testing" + + "github.com/apache/devlake/core/models/common" + "github.com/apache/devlake/helpers/e2ehelper" + implcontext "github.com/apache/devlake/impls/context" + "github.com/apache/devlake/plugins/sonarqube/impl" + sonarqubemigrations "github.com/apache/devlake/plugins/sonarqube/models/migrationscripts" + "github.com/stretchr/testify/require" +) + +type sonarqubeFileMetricsBeforeKeyLength struct { + ConnectionId uint64 `gorm:"primaryKey"` + FileMetricsKey string `gorm:"primaryKey;type:varchar(191)"` + ProjectKey string `gorm:"index"` + common.NoPKModel +} + +func (sonarqubeFileMetricsBeforeKeyLength) TableName() string { + return "_tool_sonarqube_file_metrics" +} + +// TestSonarqubeFileMetricsKeyLengthMigrationRerun covers the normal run and a +// re-run after an interrupted attempt that already dropped the primary key. +func TestSonarqubeFileMetricsKeyLengthMigrationRerun(t *testing.T) { + var sonarqube impl.Sonarqube + dataflowTester := e2ehelper.NewDataFlowTester(t, "sonarqube", sonarqube) + basicRes := implcontext.NewDefaultBasicRes(dataflowTester.Cfg, dataflowTester.Log, dataflowTester.Dal) + const name = "modify _tool_sonarqube_file_metrics file_metrics_key from varchar(191) to varchar(500)" + + // normal run: the primary key exists and is re-created + prepareFileMetricsBeforeKeyLength(t, dataflowTester) + runMigration(t, sonarqubemigrations.All(), name, basicRes) + assertFileMetricsMigrated(t, dataflowTester) + + // re-run: an interrupted attempt dropped the primary key before the + // migration history was recorded + prepareFileMetricsBeforeKeyLength(t, dataflowTester) + if dataflowTester.Dal.Dialect() == "mysql" { + require.NoError(t, dataflowTester.Dal.Exec("ALTER TABLE _tool_sonarqube_file_metrics DROP PRIMARY KEY")) + } else { + require.NoError(t, dataflowTester.Dal.Exec("ALTER TABLE _tool_sonarqube_file_metrics DROP CONSTRAINT _tool_sonarqube_file_metrics_pkey")) + } + require.False(t, hasPrimaryKey(t, dataflowTester)) + runMigration(t, sonarqubemigrations.All(), name, basicRes) + assertFileMetricsMigrated(t, dataflowTester) +} + +func prepareFileMetricsBeforeKeyLength(t *testing.T, dataflowTester *e2ehelper.DataFlowTester) { + t.Helper() + require.NoError(t, dataflowTester.Db.Migrator().DropTable(&sonarqubeFileMetricsBeforeKeyLength{})) + require.NoError(t, dataflowTester.Db.AutoMigrate(&sonarqubeFileMetricsBeforeKeyLength{})) + require.NoError(t, dataflowTester.Db.Create(&sonarqubeFileMetricsBeforeKeyLength{ + ConnectionId: 1, + FileMetricsKey: "project:src/main.go", + ProjectKey: "project", + }).Error) +} + +func assertFileMetricsMigrated(t *testing.T, dataflowTester *e2ehelper.DataFlowTester) { + t.Helper() + require.True(t, hasPrimaryKey(t, dataflowTester)) + columnTypes, err := dataflowTester.Db.Migrator().ColumnTypes(&sonarqubeFileMetricsBeforeKeyLength{}) + require.NoError(t, err) + for _, columnType := range columnTypes { + if columnType.Name() == "file_metrics_key" { + length, ok := columnType.Length() + require.True(t, ok) + require.Equal(t, int64(500), length) + } + } + var migrated sonarqubeFileMetricsBeforeKeyLength + require.NoError(t, dataflowTester.Db.First(&migrated, "connection_id = ? AND file_metrics_key = ?", 1, "project:src/main.go").Error) + require.Equal(t, "project", migrated.ProjectKey) +} + +func hasPrimaryKey(t *testing.T, dataflowTester *e2ehelper.DataFlowTester) bool { + t.Helper() + schema := "current_schema()" + if dataflowTester.Dal.Dialect() == "mysql" { + schema = "DATABASE()" + } + var count int64 + require.NoError(t, dataflowTester.Db.Raw( + "SELECT COUNT(*) FROM information_schema.table_constraints WHERE table_schema = "+schema+ + " AND table_name = ? AND constraint_type = 'PRIMARY KEY'", + "_tool_sonarqube_file_metrics", + ).Scan(&count).Error) + return count > 0 +} diff --git a/backend/plugins/sonarqube/models/migrationscripts/20230927_modify_file_metrics_key_length.go b/backend/plugins/sonarqube/models/migrationscripts/20230927_modify_file_metrics_key_length.go index fb85c1a61ed..c2cce1e992e 100644 --- a/backend/plugins/sonarqube/models/migrationscripts/20230927_modify_file_metrics_key_length.go +++ b/backend/plugins/sonarqube/models/migrationscripts/20230927_modify_file_metrics_key_length.go @@ -49,12 +49,23 @@ func (script *modifyFileMetricsKeyLength) Up(basicRes context.BasicRes) errors.E if err1 != nil { return errors.Convert(err1) } + // Drop the primary key only if it still exists, so the script can be re-run + // after an interrupted attempt that already dropped it. if u.Scheme == "mysql" { - if err := db.Exec("ALTER TABLE _tool_sonarqube_file_metrics DROP PRIMARY KEY"); err != nil { + pkCount, err := db.Count( + dal.From("information_schema.table_constraints"), + dal.Where("table_schema = DATABASE() AND table_name = ? AND constraint_type = 'PRIMARY KEY'", "_tool_sonarqube_file_metrics"), + ) + if err != nil { return err } + if pkCount > 0 { + if err := db.Exec("ALTER TABLE _tool_sonarqube_file_metrics DROP PRIMARY KEY"); err != nil { + return err + } + } } else { - if err := db.Exec("ALTER TABLE _tool_sonarqube_file_metrics DROP CONSTRAINT _tool_sonarqube_file_metrics_pkey"); err != nil { + if err := db.Exec("ALTER TABLE _tool_sonarqube_file_metrics DROP CONSTRAINT IF EXISTS _tool_sonarqube_file_metrics_pkey"); err != nil { return err } }