Skip to content

Commit 21988d5

Browse files
tae2089claude
andcommitted
Batch community and membership inserts during rebuild
Community rebuild issued one INSERT per community and one per membership (N+1: a namespace with thousands of nodes meant thousands of round trips), and loaded full node rows only to read id/file_path. Use CreateInBatches for both community and membership rows and Select only the columns grouping needs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent f29ac5b commit 21988d5

2 files changed

Lines changed: 66 additions & 14 deletions

File tree

internal/analysis/community/service.go

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ import (
1111
"gorm.io/gorm"
1212
)
1313

14+
// communityInsertBatchSize bounds the row count per bulk insert during community rebuild so a
15+
// large namespace does not issue one INSERT per community or per membership.
16+
const communityInsertBatchSize = 500
17+
1418
// Config controls directory-based community grouping.
1519
// @intent define how file paths are collapsed into module community keys
1620
type Config struct {
@@ -105,10 +109,11 @@ func deleteCommunities(tx *gorm.DB, ns string) error {
105109

106110
// groupNodesByDirectory loads namespace nodes and buckets them by directory key.
107111
// @intent produce the directory-keyed node groups that drive community creation
112+
// @domainRule only id and file_path are needed for grouping and membership, so avoid loading full node rows.
108113
func groupNodesByDirectory(tx *gorm.DB, ctx context.Context, depth int) (map[string][]model.Node, error) {
109114
var nodes []model.Node
110115
ns := ctxns.FromContext(ctx)
111-
if err := tx.Where("namespace = ?", ns).Find(&nodes).Error; err != nil {
116+
if err := tx.Model(&model.Node{}).Select("id", "file_path").Where("namespace = ?", ns).Find(&nodes).Error; err != nil {
112117
return nil, err
113118
}
114119

@@ -126,33 +131,42 @@ func groupNodesByDirectory(tx *gorm.DB, ctx context.Context, depth int) (map[str
126131
// @sideEffect inserts into communities and community_memberships tables
127132
// @return community lookup by key plus per-node directory key map
128133
func createCommunitiesAndMemberships(tx *gorm.DB, groups map[string][]model.Node, ns string) (map[string]*model.Community, map[uint]string, error) {
129-
communityMap := map[string]*model.Community{}
134+
communities := make([]model.Community, 0, len(groups))
130135
for key := range groups {
131-
c := model.Community{
136+
communities = append(communities, model.Community{
132137
Namespace: ns,
133138
Key: key,
134139
Label: key,
135140
Strategy: "directory",
136-
}
137-
if err := tx.Create(&c).Error; err != nil {
141+
})
142+
}
143+
if len(communities) > 0 {
144+
if err := tx.CreateInBatches(communities, communityInsertBatchSize).Error; err != nil {
138145
return nil, nil, err
139146
}
140-
communityMap[key] = &c
147+
}
148+
communityMap := make(map[string]*model.Community, len(communities))
149+
for i := range communities {
150+
communityMap[communities[i].Key] = &communities[i]
141151
}
142152

143153
nodeComm := map[uint]string{}
144-
for key, ns := range groups {
145-
for _, n := range ns {
146-
m := model.CommunityMembership{
147-
CommunityID: communityMap[key].ID,
154+
memberships := make([]model.CommunityMembership, 0, len(groups))
155+
for key, nodes := range groups {
156+
communityID := communityMap[key].ID
157+
for _, n := range nodes {
158+
memberships = append(memberships, model.CommunityMembership{
159+
CommunityID: communityID,
148160
NodeID: n.ID,
149-
}
150-
if err := tx.Create(&m).Error; err != nil {
151-
return nil, nil, err
152-
}
161+
})
153162
nodeComm[n.ID] = key
154163
}
155164
}
165+
if len(memberships) > 0 {
166+
if err := tx.CreateInBatches(memberships, communityInsertBatchSize).Error; err != nil {
167+
return nil, nil, err
168+
}
169+
}
156170

157171
return communityMap, nodeComm, nil
158172
}

internal/analysis/community/service_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ package community
33
import (
44
"context"
55
"fmt"
6+
"strings"
67
"testing"
8+
"time"
79

810
"github.com/tae2089/code-context-graph/internal/ctxns"
911
"github.com/tae2089/code-context-graph/internal/model"
@@ -63,6 +65,42 @@ func seedEdge(t *testing.T, db *gorm.DB, from, to uint) {
6365
}
6466
}
6567

68+
type insertCountLogger struct {
69+
gormlogger.Interface
70+
membershipInserts int
71+
}
72+
73+
func (l *insertCountLogger) LogMode(gormlogger.LogLevel) gormlogger.Interface { return l }
74+
func (l *insertCountLogger) Trace(_ context.Context, _ time.Time, fc func() (string, int64), _ error) {
75+
sql, _ := fc()
76+
if strings.Contains(sql, "INSERT INTO") && strings.Contains(sql, "community_memberships") {
77+
l.membershipInserts++
78+
}
79+
}
80+
81+
func TestRebuild_BatchesMembershipInserts(t *testing.T) {
82+
counter := &insertCountLogger{Interface: gormlogger.Discard}
83+
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{Logger: counter})
84+
if err != nil {
85+
t.Fatalf("open db: %v", err)
86+
}
87+
if err := gormstore.New(db).AutoMigrate(); err != nil {
88+
t.Fatalf("migrate: %v", err)
89+
}
90+
// 40 nodes in one directory => 40 memberships that must not be 40 separate INSERTs.
91+
for i := uint(1); i <= 40; i++ {
92+
seedNode(t, db, i, fmt.Sprintf("N%d", i), fmt.Sprintf("pkg/f%d.go", i))
93+
}
94+
95+
counter.membershipInserts = 0
96+
if _, err := New(db).Rebuild(context.Background(), Config{Depth: 1}); err != nil {
97+
t.Fatal(err)
98+
}
99+
if counter.membershipInserts != 1 {
100+
t.Fatalf("expected memberships to insert in a single batch, got %d INSERT statements", counter.membershipInserts)
101+
}
102+
}
103+
66104
func TestRebuild_GroupsByDirectory(t *testing.T) {
67105
db := setupDB(t)
68106
seedNode(t, db, 1, "X", "a/x.go")

0 commit comments

Comments
 (0)