This is an automated email from the ASF dual-hosted git repository.
klesh pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/devlake.git
The following commit(s) were added to refs/heads/main by this push:
new 0cb4ddb86 fix(sonarqube): make file_metrics_key length migration
re-runnable (#9242)
0cb4ddb86 is described below
commit 0cb4ddb86a6c9fea7c66e14907a0005c0642488d
Author: AwEi <[email protected]>
AuthorDate: Thu Oct 22 10:31:08 2026 +0800
fix(sonarqube): make file_metrics_key length migration re-runnable (#9242)
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
---
.../e2e/file_metrics_key_length_migration_test.go | 109 +++++++++++++++++++++
.../20230927_modify_file_metrics_key_length.go | 15 ++-
2 files changed, 122 insertions(+), 2 deletions(-)
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 000000000..3fa120438
--- /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 fb85c1a61..c2cce1e99 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
}
}