Skip to content

Commit 87b644a

Browse files
committed
Merge remote-tracking branch 'upstream/main' into versionUpgrade
# Conflicts: # backend/core/models/migrationscripts/20260629_change_issue_component_to_text.go # backend/core/models/migrationscripts/20260701_change_cq_issue_code_blocks_component_to_text.go # backend/plugins/jira/models/migrationscripts/20260707_change_fix_versions_to_text.go # backend/plugins/sonarqube/e2e/issue_code_block_long_component_test.go # backend/plugins/sonarqube/models/migrationscripts/20260701_change_issue_code_block_component_type.go # backend/plugins/sonarqube/tasks/issue_code_blocks_convertor_test.go
2 parents 01457a3 + 27c771a commit 87b644a

13 files changed

Lines changed: 237 additions & 217 deletions

File tree

backend/core/models/migrationscripts/20260629_change_issue_component_to_text.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,13 @@ var _ plugin.MigrationScript = (*changeIssueComponentToText)(nil)
2727

2828
type changeIssueComponentToText struct{}
2929

30-
func (script *changeIssueComponentToText) Up(basicRes context.BasicRes) errors.Error {
31-
// the previous script (20240813) targeted the non-existent "components" column,
32-
// the actual column is the singular "component" which stayed varchar(255).
30+
func (*changeIssueComponentToText) Up(basicRes context.BasicRes) errors.Error {
31+
// The 20240813 migration targeted the non-existent plural "components" column.
3332
return basicRes.GetDal().ModifyColumnType("issues", "component", "text")
3433
}
3534

3635
func (*changeIssueComponentToText) Version() uint64 {
37-
return 20250629120000
36+
return 20260629120000
3837
}
3938

4039
func (*changeIssueComponentToText) Name() string {
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
Licensed to the Apache Software Foundation (ASF) under one or more
3+
contributor license agreements. See the NOTICE file distributed with
4+
this work for additional information regarding copyright ownership.
5+
The ASF licenses this file to You under the Apache License, Version 2.0
6+
(the "License"); you may not use this file except in compliance with
7+
the License. You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
16+
*/
17+
18+
package migrationscripts
19+
20+
import (
21+
"testing"
22+
23+
"github.com/apache/incubator-devlake/core/context"
24+
"github.com/apache/incubator-devlake/core/dal"
25+
"github.com/apache/incubator-devlake/core/errors"
26+
)
27+
28+
type modifyColumnTypeCall struct {
29+
tableName string
30+
columnName string
31+
columnType string
32+
}
33+
34+
type recordingDal struct {
35+
dal.Dal
36+
call *modifyColumnTypeCall
37+
}
38+
39+
func (d *recordingDal) ModifyColumnType(tableName string, columnName string, columnType string) errors.Error {
40+
d.call = &modifyColumnTypeCall{tableName, columnName, columnType}
41+
return nil
42+
}
43+
44+
type basicResWithDal struct {
45+
context.BasicRes
46+
database dal.Dal
47+
}
48+
49+
func (r *basicResWithDal) GetDal() dal.Dal {
50+
return r.database
51+
}
52+
53+
func TestChangeIssueComponentToText(t *testing.T) {
54+
database := new(recordingDal)
55+
script := new(changeIssueComponentToText)
56+
57+
if err := script.Up(&basicResWithDal{database: database}); err != nil {
58+
t.Fatalf("migration failed: %v", err)
59+
}
60+
61+
want := &modifyColumnTypeCall{"issues", "component", "text"}
62+
if database.call == nil || *database.call != *want {
63+
t.Fatalf("ModifyColumnType call = %#v, want %#v", database.call, want)
64+
}
65+
if script.Version() != 20260629120000 {
66+
t.Fatalf("Version() = %d, want 20260629120000", script.Version())
67+
}
68+
if script.Name() != "change issues.component type to text" {
69+
t.Fatalf("Name() = %q, want %q", script.Name(), "change issues.component type to text")
70+
}
71+
72+
found := false
73+
for _, registeredScript := range All() {
74+
if registeredScript.Version() == script.Version() {
75+
found = true
76+
break
77+
}
78+
}
79+
if !found {
80+
t.Fatalf("migration version %d is not registered", script.Version())
81+
}
82+
}

backend/core/models/migrationscripts/20260701_change_cq_issue_code_blocks_component_to_text.go

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,8 @@ type changeCqIssueCodeBlocksComponentToText struct{}
2929

3030
func (script *changeCqIssueCodeBlocksComponentToText) Up(basicRes context.BasicRes) errors.Error {
3131
db := basicRes.GetDal()
32-
// Drop the index on component first, since MySQL does not allow TEXT columns in indexes without a key length
33-
err := db.Exec("DROP INDEX idx_cq_issue_code_blocks_component ON cq_issue_code_blocks")
34-
if err != nil {
35-
// Ignore error if index does not exist (e.g. PostgreSQL or already dropped)
36-
basicRes.GetLogger().Warn(err, "failed to drop index idx_cq_issue_code_blocks_component (may not exist)")
32+
if err := db.DropIndexes("cq_issue_code_blocks", "idx_cq_issue_code_blocks_component"); err != nil {
33+
return err
3734
}
3835
return db.ModifyColumnType("cq_issue_code_blocks", "component", "text")
3936
}
@@ -45,4 +42,3 @@ func (*changeCqIssueCodeBlocksComponentToText) Version() uint64 {
4542
func (*changeCqIssueCodeBlocksComponentToText) Name() string {
4643
return "change cq_issue_code_blocks.component type to text"
4744
}
48-

backend/plugins/jira/models/migrationscripts/20260707_change_fix_versions_to_text.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,3 @@ func (*changeFixVersionsToText20260707) Version() uint64 {
4444
func (*changeFixVersionsToText20260707) Name() string {
4545
return "change fix_versions type to text in _tool_jira_issues"
4646
}
47-

backend/plugins/sonarqube/e2e/issue_code_block_long_component_test.go

Lines changed: 126 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -18,139 +18,182 @@ limitations under the License.
1818
package e2e
1919

2020
import (
21+
"strings"
2122
"testing"
2223
"time"
2324

2425
"github.com/apache/incubator-devlake/core/models/common"
26+
"github.com/apache/incubator-devlake/core/models/domainlayer"
2527
"github.com/apache/incubator-devlake/core/models/domainlayer/codequality"
28+
coremigrations "github.com/apache/incubator-devlake/core/models/migrationscripts"
29+
"github.com/apache/incubator-devlake/core/plugin"
2630
"github.com/apache/incubator-devlake/helpers/e2ehelper"
31+
implcontext "github.com/apache/incubator-devlake/impls/context"
2732
"github.com/apache/incubator-devlake/plugins/sonarqube/impl"
2833
"github.com/apache/incubator-devlake/plugins/sonarqube/models"
34+
sonarqubemigrations "github.com/apache/incubator-devlake/plugins/sonarqube/models/migrationscripts"
2935
"github.com/apache/incubator-devlake/plugins/sonarqube/tasks"
30-
"github.com/stretchr/testify/assert"
36+
"github.com/stretchr/testify/require"
3137
)
3238

33-
// TestSonarqubeIssueCodeBlockLongComponent verifies that the convertIssueCodeBlocks
34-
// subtask can handle component paths longer than 256 and 500 characters without
35-
// producing "Data too long for column 'component'" errors.
36-
//
37-
// This test reproduces the original bug:
38-
//
39-
// Error 1406 (22001): Data too long for column 'component' at row 91
40-
//
41-
// The fix changed both tool layer and domain layer `component` columns to TEXT type.
39+
type sonarqubeIssueCodeBlockBeforeText struct {
40+
ConnectionId uint64 `gorm:"primaryKey"`
41+
Id string `gorm:"primaryKey"`
42+
IssueKey string `gorm:"index"`
43+
Component string `gorm:"index;type:varchar(500)"`
44+
StartLine int
45+
EndLine int
46+
StartOffset int
47+
EndOffset int
48+
Msg string
49+
common.NoPKModel
50+
}
51+
52+
func (sonarqubeIssueCodeBlockBeforeText) TableName() string {
53+
return "_tool_sonarqube_issue_code_blocks"
54+
}
55+
56+
type cqIssueCodeBlockBeforeText struct {
57+
domainlayer.DomainEntity
58+
IssueKey string `json:"key" gorm:"index"`
59+
Component string `gorm:"index"`
60+
StartLine int
61+
EndLine int
62+
StartOffset int
63+
EndOffset int
64+
Msg string
65+
}
66+
67+
func (cqIssueCodeBlockBeforeText) TableName() string {
68+
return "cq_issue_code_blocks"
69+
}
70+
4271
func TestSonarqubeIssueCodeBlockLongComponent(t *testing.T) {
4372
var sonarqube impl.Sonarqube
4473
dataflowTester := e2ehelper.NewDataFlowTester(t, "sonarqube", sonarqube)
45-
46-
// Migrate the tables to get the current schema (with TEXT type for component)
4774
dataflowTester.FlushTabler(&models.SonarqubeIssue{})
48-
dataflowTester.FlushTabler(&models.SonarqubeIssueCodeBlock{})
49-
dataflowTester.FlushTabler(&codequality.CqIssueCodeBlock{})
50-
51-
// Create a component path that exceeds 256 chars (old GORM default varchar limit)
52-
longComponent256 := "my-org_my-project-key:src/main/java/com/myorganization/myapplication/services/implementations/deeply/nested/package/structure/subpackage/another/level/of/nesting/MyVeryLongServiceImplementationClassNameThatExceedsThe256CharacterLimit.java"
53-
assert.Greater(t, len(longComponent256), 256)
54-
55-
// Create a component path that exceeds 500 chars (previous varchar(500) limit)
56-
longComponent500 := "my-organization_my-extremely-long-project-name-with-feature-branch:" +
57-
"src/main/java/com/enterprise/application/modules/customer/services/implementations/" +
58-
"internal/validation/rules/complex/nested/deeply/structured/package/hierarchy/" +
59-
"subsystem/core/domain/entities/aggregates/valueobjects/specifications/" +
60-
"MyExtremelyLongAndDescriptiveClassNameForAnEnterpriseApplicationServiceImplementation" +
61-
"ThatFollowsAllNamingConventionsAndExceedsReasonableLimits.java"
62-
assert.Greater(t, len(longComponent500), 500)
63-
64-
// Insert test issue (needed for the JOIN in the converter)
75+
require.NoError(t, dataflowTester.Db.Migrator().DropTable(
76+
&sonarqubeIssueCodeBlockBeforeText{},
77+
&cqIssueCodeBlockBeforeText{},
78+
))
79+
require.NoError(t, dataflowTester.Db.AutoMigrate(
80+
&sonarqubeIssueCodeBlockBeforeText{},
81+
&cqIssueCodeBlockBeforeText{},
82+
))
83+
84+
existingComponent := "existing:component"
85+
require.NoError(t, dataflowTester.Db.Create(&sonarqubeIssueCodeBlockBeforeText{
86+
ConnectionId: 1,
87+
Id: "existing-tool-block",
88+
IssueKey: "existing-issue",
89+
Component: existingComponent,
90+
}).Error)
91+
require.NoError(t, dataflowTester.Db.Create(&cqIssueCodeBlockBeforeText{
92+
DomainEntity: domainlayer.DomainEntity{Id: "existing-domain-block"},
93+
IssueKey: "existing-domain-issue",
94+
Component: existingComponent,
95+
}).Error)
96+
97+
basicRes := implcontext.NewDefaultBasicRes(dataflowTester.Cfg, dataflowTester.Log, dataflowTester.Dal)
98+
runMigration(t, coremigrations.All(), "change cq_issue_code_blocks.component type to text", basicRes)
99+
runMigration(t, sonarqubemigrations.All(), "change _tool_sonarqube_issue_code_blocks.component type to text", basicRes)
100+
assertTextColumnWithoutIndex(t, dataflowTester, "cq_issue_code_blocks")
101+
assertTextColumnWithoutIndex(t, dataflowTester, "_tool_sonarqube_issue_code_blocks")
102+
103+
var migratedToolBlock sonarqubeIssueCodeBlockBeforeText
104+
require.NoError(t, dataflowTester.Db.First(&migratedToolBlock, "id = ?", "existing-tool-block").Error)
105+
require.Equal(t, existingComponent, migratedToolBlock.Component)
106+
var migratedDomainBlock cqIssueCodeBlockBeforeText
107+
require.NoError(t, dataflowTester.Db.First(&migratedDomainBlock, "id = ?", "existing-domain-block").Error)
108+
require.Equal(t, existingComponent, migratedDomainBlock.Component)
109+
require.NoError(t, dataflowTester.Db.Delete(&migratedToolBlock).Error)
110+
require.NoError(t, dataflowTester.Db.Delete(&migratedDomainBlock).Error)
111+
112+
longComponent256 := "project:" + strings.Repeat("a", 256)
113+
longComponent500 := "project:" + strings.Repeat("b", 500)
114+
require.Greater(t, len(longComponent256), 256)
115+
require.Greater(t, len(longComponent500), 500)
116+
65117
issueKey := "TEST-LONG-COMPONENT-ISSUE"
66118
projectKey := "test-long-component-project"
67-
testIssue := &models.SonarqubeIssue{
119+
result := dataflowTester.Db.Create(&models.SonarqubeIssue{
68120
ConnectionId: 1,
69121
IssueKey: issueKey,
70122
ProjectKey: projectKey,
71123
Component: longComponent500,
72124
Rule: "java:S3776",
73125
Severity: "CRITICAL",
74-
}
75-
result := dataflowTester.Db.Create(testIssue)
76-
assert.NoError(t, result.Error, "inserting issue with long component should succeed")
126+
})
127+
require.NoError(t, result.Error)
77128

78-
// Insert code blocks with long component paths
79129
codeBlocks := []*models.SonarqubeIssueCodeBlock{
80130
{
81131
ConnectionId: 1,
82132
Id: "test-long-component-block-256",
83133
IssueKey: issueKey,
84134
Component: longComponent256,
85-
StartLine: 10,
86-
EndLine: 20,
87-
StartOffset: 0,
88-
EndOffset: 50,
89-
Msg: "Test message for component > 256 chars",
135+
Msg: "component longer than 256 characters",
90136
},
91137
{
92138
ConnectionId: 1,
93139
Id: "test-long-component-block-500",
94140
IssueKey: issueKey,
95141
Component: longComponent500,
96-
StartLine: 42,
97-
EndLine: 42,
98-
StartOffset: 5,
99-
EndOffset: 80,
100-
Msg: "Test message for component > 500 chars",
142+
Msg: "component longer than 500 characters",
101143
},
102144
}
103145
for _, block := range codeBlocks {
104-
result := dataflowTester.Db.Create(block)
105-
assert.NoError(t, result.Error, "inserting code block with long component should succeed (tool layer)")
146+
require.NoError(t, dataflowTester.Db.Create(block).Error)
106147
}
107148

108-
// Run the converter subtask - this is where the original error occurred
109-
taskData := &tasks.SonarqubeTaskData{
149+
dataflowTester.Subtask(tasks.ConvertIssueCodeBlocksMeta, &tasks.SonarqubeTaskData{
110150
Options: &tasks.SonarqubeOptions{
111151
ConnectionId: 1,
112152
ProjectKey: projectKey,
113153
},
114154
TaskStartTime: time.Now(),
115-
}
116-
117-
// This would fail with the old schema:
118-
// Error 1406 (22001): Data too long for column 'component' at row N
119-
dataflowTester.Subtask(tasks.ConvertIssueCodeBlocksMeta, taskData)
155+
})
120156

121-
// Verify the converted domain layer records have the full component path
122157
var domainBlocks []codequality.CqIssueCodeBlock
123-
result = dataflowTester.Db.Find(&domainBlocks)
124-
assert.NoError(t, result.Error)
125-
assert.GreaterOrEqual(t, len(domainBlocks), 2, "should have at least 2 converted code blocks")
126-
127-
// Verify the long component values were preserved without truncation
128-
foundLong256 := false
129-
foundLong500 := false
130-
for _, block := range domainBlocks {
131-
if block.Component == longComponent256 {
132-
foundLong256 = true
133-
assert.Equal(t, "Test message for component > 256 chars", block.Msg)
134-
}
135-
if block.Component == longComponent500 {
136-
foundLong500 = true
137-
assert.Equal(t, "Test message for component > 500 chars", block.Msg)
138-
}
139-
}
140-
assert.True(t, foundLong256, "domain layer should contain the 256+ char component without truncation")
141-
assert.True(t, foundLong500, "domain layer should contain the 500+ char component without truncation")
158+
require.NoError(t, dataflowTester.Db.Find(&domainBlocks).Error)
159+
require.Len(t, domainBlocks, 2)
160+
require.ElementsMatch(t,
161+
[]string{longComponent256, longComponent500},
162+
[]string{domainBlocks[0].Component, domainBlocks[1].Component},
163+
)
142164

143-
// Also verify the tool layer stored the values correctly
144165
var toolBlocks []models.SonarqubeIssueCodeBlock
145-
result = dataflowTester.Db.Where("connection_id = ? AND issue_key = ?", 1, issueKey).Find(&toolBlocks)
146-
assert.NoError(t, result.Error)
147-
assert.Equal(t, 2, len(toolBlocks))
148-
for _, block := range toolBlocks {
149-
assert.True(t, len(block.Component) > 256,
150-
"tool layer component should not be truncated")
151-
}
166+
require.NoError(t, dataflowTester.Db.Where(
167+
"connection_id = ? AND issue_key = ?", 1, issueKey,
168+
).Find(&toolBlocks).Error)
169+
require.Len(t, toolBlocks, 2)
170+
require.ElementsMatch(t,
171+
[]string{longComponent256, longComponent500},
172+
[]string{toolBlocks[0].Component, toolBlocks[1].Component},
173+
)
174+
}
152175

153-
// Cleanup - ignore NoPKModel fields in verification
154-
_ = common.NoPKModel{}
176+
func runMigration(t *testing.T, scripts []plugin.MigrationScript, name string, basicRes *implcontext.DefaultBasicRes) {
177+
t.Helper()
178+
for _, script := range scripts {
179+
if script.Name() == name {
180+
require.NoError(t, script.Up(basicRes))
181+
return
182+
}
183+
}
184+
require.Fail(t, "migration is not registered", name)
155185
}
156186

187+
func assertTextColumnWithoutIndex(t *testing.T, dataflowTester *e2ehelper.DataFlowTester, table string) {
188+
t.Helper()
189+
columnTypes, err := dataflowTester.Db.Migrator().ColumnTypes(table)
190+
require.NoError(t, err)
191+
for _, columnType := range columnTypes {
192+
if columnType.Name() == "component" {
193+
require.Contains(t, strings.ToLower(columnType.DatabaseTypeName()), "text")
194+
require.False(t, dataflowTester.Db.Migrator().HasIndex(table, "idx_"+table+"_component"))
195+
return
196+
}
197+
}
198+
require.Fail(t, "component column not found", table)
199+
}

0 commit comments

Comments
 (0)