Skip to content

Commit 492e9ab

Browse files
tae2089claude
andcommitted
refactor: remove pgvector/pgstore and MySQL driver support
- Delete internal/store/pgstore package (pgvector/AGE sync) - Delete internal/store/search/mysql.go and mysql_test.go - Remove PGStore field from cli.Deps and service.GraphService - Remove SyncGraph option from BuildOptions - Remove --graph flag from build command - Remove gorm.io/driver/mysql and lib/pq dependencies Supported drivers: sqlite, postgres only. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 9c88768 commit 492e9ab

40 files changed

Lines changed: 2155 additions & 1300 deletions

File tree

.ccg.yaml

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
output:
2+
dir: ./docs
3+
per_file: true
4+
index: true
5+
6+
languages:
7+
- name: go
8+
extensions: [".go"]
9+
queries:
10+
functions: |
11+
(function_declaration name: (identifier) @name) @function
12+
(method_declaration name: (field_identifier) @name) @function
13+
(type_spec name: (type_identifier) @name) @function
14+
comments: "(comment) @comment"
15+
16+
- name: java
17+
extensions: [".java"]
18+
queries:
19+
functions: |
20+
(method_declaration name: (identifier) @name) @function
21+
(constructor_declaration name: (identifier) @name) @function
22+
(variable_declarator name: (identifier) @name value: (lambda_expression)) @function
23+
(class_declaration name: (identifier) @name) @function
24+
(interface_declaration name: (identifier) @name) @function
25+
comments: "(block_comment) @comment"
26+
27+
- name: typescript
28+
extensions: [".ts", ".tsx"]
29+
queries:
30+
functions: |
31+
(function_declaration name: (identifier) @name) @function
32+
(method_definition name: (property_identifier) @name) @function
33+
(variable_declarator name: (identifier) @name value: (arrow_function)) @function
34+
(variable_declarator name: (identifier) @name value: (function_expression)) @function
35+
(class_declaration name: (type_identifier) @name) @function
36+
(interface_declaration name: (type_identifier) @name) @function
37+
(type_alias_declaration name: (type_identifier) @name) @function
38+
comments: "(comment) @comment"
39+
40+
- name: python
41+
extensions: [".py"]
42+
queries:
43+
functions: |
44+
(function_definition name: (identifier) @name) @function
45+
(assignment left: (identifier) @name right: (lambda)) @function
46+
(class_definition name: (identifier) @name) @function
47+
comments: "(comment) @comment"
48+
49+
- name: kotlin
50+
extensions: [".kt", ".kts"]
51+
queries:
52+
functions: |
53+
(function_declaration (simple_identifier) @name) @function
54+
(class_declaration (type_identifier) @name) @function
55+
(object_declaration (type_identifier) @name) @function
56+
(interface_declaration (simple_identifier) @name) @function
57+
comments: "(multiline_comment) @comment\n(line_comment) @comment"
58+
59+
# exclude: regex 패턴 목록. 파일 경로가 매칭되면 파싱에서 제외된다.
60+
exclude:
61+
- ".*_test\\.go$"
62+
- "vendor/.*"
63+
- "node_modules/.*"
64+
- ".*\\.pb\\.go$"
65+
- "bin/.*"
66+
- "docs/.*"
67+
- "testdata/.*"
68+
- ".*\\.gen\\.go$"
69+
- "scripts/.*"
70+
- "root.go"
71+
- "main.go"
72+
- "cmd/.*"
73+
74+
comment_tags:
75+
standard:
76+
- param
77+
- return
78+
- see
79+
extended:
80+
- intent
81+
- domainRule
82+
- sideEffect
83+
- mutates
84+
- requires
85+
- ensures

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,5 @@
1313

1414
# Plugin cache
1515
.omc/
16+
17+
docs/

ccg_stripped

44.5 MB
Binary file not shown.

cmd/ccg/main.go

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import (
88
"github.com/mark3labs/mcp-go/server"
99
"github.com/spf13/viper"
1010

11-
"gorm.io/driver/mysql"
1211
"gorm.io/driver/postgres"
1312
"gorm.io/driver/sqlite"
1413
"gorm.io/gorm"
@@ -29,7 +28,6 @@ import (
2928
"github.com/imtaebin/code-context-graph/internal/model"
3029
"github.com/imtaebin/code-context-graph/internal/parse/treesitter"
3130
"github.com/imtaebin/code-context-graph/internal/store/gormstore"
32-
"github.com/imtaebin/code-context-graph/internal/store/pgstore"
3331
"github.com/imtaebin/code-context-graph/internal/store/search"
3432
)
3533

@@ -60,15 +58,6 @@ func main() {
6058
return fmt.Errorf("migrate search backend: %w", err)
6159
}
6260

63-
var pg *pgstore.Store
64-
if driver == "postgres" {
65-
var pgErr error
66-
pg, pgErr = pgstore.New(dsn)
67-
if pgErr != nil {
68-
return fmt.Errorf("init pgstore: %w", pgErr)
69-
}
70-
}
71-
7261
walkers := buildWalkers(deps.Logger)
7362
syncer := incremental.New(st, walkers[".go"])
7463

@@ -77,7 +66,6 @@ func main() {
7766
deps.SearchBackend = sb
7867
deps.Walkers = walkers
7968
deps.Syncer = syncer
80-
deps.PGStore = pg
8169

8270
return nil
8371
}
@@ -177,8 +165,6 @@ func openDB(driver, dsn string) (*gorm.DB, error) {
177165
return gorm.Open(sqlite.Open(dsn), cfg)
178166
case "postgres":
179167
return gorm.Open(postgres.Open(dsn), cfg)
180-
case "mysql":
181-
return gorm.Open(mysql.Open(dsn), cfg)
182168
default:
183169
return nil, fmt.Errorf("unsupported database driver: %s", driver)
184170
}
@@ -188,8 +174,6 @@ func newSearchBackend(driver string) search.Backend {
188174
switch driver {
189175
case "postgres":
190176
return search.NewPostgresBackend()
191-
case "mysql":
192-
return search.NewMySQLBackend()
193177
default:
194178
return search.NewSQLiteBackend()
195179
}

conductor/plan.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Code Context Graph - Code Quality & Architecture Refactoring Plan
2+
3+
## Background & Motivation
4+
The `codebase_investigator` identified several critical areas for improvement in the current codebase:
5+
1. **Performance Bottlenecks:** `N+1` queries exist in the GORM store (`UpsertNodes`) and BFS logic (`ImpactRadius`), which will degrade performance on larger code graphs.
6+
2. **Architectural Coupling:** `internal/cli/build.go` handles file walking, parsing orchestration, and database syncing, violating the Single Responsibility Principle (SRP).
7+
3. **Parser Maintainability (God Object):** `internal/parse/treesitter/walker.go` contains hard-coded language-specific logic across its methods, making it difficult to maintain or add new languages.
8+
4. **Robustness:** Database operations during the file build process lack proper transaction boundaries, risking orphaned data on errors.
9+
10+
## Scope & Impact
11+
This refactoring effort aims to systematically eliminate these technical debts. It touches core components (`gormstore`, `impact`, `build`, `treesitter`) without altering the expected external behavior of the CLI.
12+
13+
## Proposed Solution: Phased Refactoring Approach
14+
15+
We will tackle the refactoring in 3 distinct phases to ensure safe integration and review:
16+
17+
### Phase 1: Database Performance & Robustness (Data Layer)
18+
- **Batch Upserts (via GORM):** Replace individual node `SELECT` and `UPDATE/CREATE` calls in `gormstore.UpsertNodes` with GORM's native batching and conflict resolution:
19+
```go
20+
db.Clauses(clause.OnConflict{
21+
Columns: []clause.Column{{Name: "qualified_name"}},
22+
UpdateAll: true,
23+
}).CreateInBatches(nodes, 100)
24+
```
25+
- **Transaction Blocks:** Enforce `db.Transaction(func(tx *gorm.DB) error { ... })` across all file-level mutations to ensure atomicity. If a file fails to index completely, it will automatically roll back.
26+
- **BFS Optimization:** Refactor `ImpactRadius` in `impact.go` to query outgoing and incoming edges for the entire `frontier` in a single `IN (?)` query per depth iteration.
27+
28+
### Phase 2: CLI Decoupling & Service Layer (Orchestration)
29+
- **Create `GraphService`:** Extract the orchestration logic from `cli/build.go` into a new `internal/service/indexer.go`. This service will handle file walking, parsing delegation, and DB upserts within transaction blocks.
30+
- **Isolate CLI:** Simplify `build.go` to only handle argument parsing, flag binding, and invoking the new `GraphService`.
31+
32+
### Phase 3: Walker Refactoring (Parser Layer via Tree-sitter Queries)
33+
- **Migrate to Query-based Pattern Matching:** Instead of maintaining a God Object (`Walker`) with hard-coded AST traversal and language-specific `if/else` logic, we will utilize **Tree-sitter's Query System (S-expressions)**.
34+
- **Language Definitions (.scm):** Create `queries/<language>/tags.scm` files defining language-specific patterns for functions, classes, calls, and imports (e.g., `(function_declaration name: (identifier) @name)`).
35+
- **Generic Query Executor:** Refactor `walker.go` to compile these queries via `sitter.NewQuery` and execute them using `sitter.NewQueryCursor`. This makes the Go code 100% language-agnostic and data-driven.
36+
37+
## Alternatives Considered
38+
- **Strategy Pattern for Parser:** Creating `LanguageHandler` interfaces for 15+ languages in Go. Rejected because Context7 documentation shows that Tree-sitter's native Query API is the industry standard for cross-language AST pattern matching, requiring far less Go boilerplate.
39+
- **Big Bang Refactoring:** Doing all of this in a single pass. Rejected because it introduces too much risk and makes bug isolation extremely difficult.
40+
41+
## Verification & Testing
42+
- Ensure the existing test suite (especially `gormstore_test.go`, `impact_test.go`, `walker_test.go`, and `build_test.go`) continues to pass.
43+
- Write new benchmark tests for `UpsertNodes` and `ImpactRadius` to empirically prove the elimination of N+1 overhead.
44+
45+
## Migration & Rollback
46+
- No database schema migrations are necessary for this refactor. The graph and vectors will be recreated using the new optimized logic.
47+
- Rollback can be performed via standard `git revert` since this does not fundamentally alter the storage schema or external API signatures.

go.mod

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,17 @@ module github.com/imtaebin/code-context-graph
33
go 1.25.5
44

55
require (
6-
github.com/lib/pq v1.12.3
76
github.com/mark3labs/mcp-go v0.47.1
87
github.com/smacker/go-tree-sitter v0.0.0-20240827094217-dd81d9e9be82
98
github.com/spf13/cobra v1.10.2
109
github.com/spf13/viper v1.21.0
11-
gorm.io/driver/mysql v1.6.0
1210
gorm.io/driver/postgres v1.6.0
1311
gorm.io/driver/sqlite v1.6.0
1412
gorm.io/gorm v1.31.1
1513
)
1614

1715
require (
18-
filippo.io/edwards25519 v1.1.0 // indirect
1916
github.com/fsnotify/fsnotify v1.9.0 // indirect
20-
github.com/go-sql-driver/mysql v1.8.1 // indirect
2117
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
2218
github.com/google/jsonschema-go v0.4.2 // indirect
2319
github.com/google/uuid v1.6.0 // indirect

go.sum

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
2-
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
31
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
42
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
53
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -8,8 +6,6 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk
86
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
97
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
108
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
11-
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
12-
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
139
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
1410
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
1511
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
@@ -36,8 +32,6 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
3632
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
3733
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
3834
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
39-
github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ=
40-
github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
4135
github.com/mark3labs/mcp-go v0.47.1 h1:A9sJJ20mscl/ssLYHjodfaoBmq6uuhMG7pAPNYaQymQ=
4236
github.com/mark3labs/mcp-go v0.47.1/go.mod h1:JKTC7R2LLVagkEWK7Kwu7DbmA6iIvnNAod6yrHiQMag=
4337
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
@@ -91,8 +85,6 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV
9185
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
9286
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
9387
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
94-
gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg=
95-
gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo=
9688
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
9789
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
9890
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=

internal/analysis/impact/impact.go

Lines changed: 32 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,11 @@ import (
99

1010
type EdgeReader interface {
1111
GetEdgesFrom(ctx context.Context, nodeID uint) ([]model.Edge, error)
12+
GetEdgesFromNodes(ctx context.Context, nodeIDs []uint) ([]model.Edge, error)
1213
GetEdgesTo(ctx context.Context, nodeID uint) ([]model.Edge, error)
14+
GetEdgesToNodes(ctx context.Context, nodeIDs []uint) ([]model.Edge, error)
1315
GetNodeByID(ctx context.Context, id uint) (*model.Node, error)
16+
GetNodesByIDs(ctx context.Context, ids []uint) ([]model.Node, error)
1417
}
1518

1619
type Analyzer struct {
@@ -36,40 +39,41 @@ func (a *Analyzer) ImpactRadius(ctx context.Context, nodeID uint, depth int) ([]
3639

3740
for d := 0; d < depth && len(frontier) > 0; d++ {
3841
var next []uint
39-
for _, nid := range frontier {
40-
edges, err := a.store.GetEdgesFrom(ctx, nid)
41-
if err != nil {
42-
return nil, err
43-
}
44-
for _, e := range edges {
45-
if !visited[e.ToNodeID] {
46-
visited[e.ToNodeID] = true
47-
next = append(next, e.ToNodeID)
48-
}
49-
}
50-
incoming, err := a.store.GetEdgesTo(ctx, nid)
51-
if err != nil {
52-
return nil, err
53-
}
54-
for _, e := range incoming {
55-
if !visited[e.FromNodeID] {
56-
visited[e.FromNodeID] = true
57-
next = append(next, e.FromNodeID)
58-
}
42+
43+
edgesFrom, err := a.store.GetEdgesFromNodes(ctx, frontier)
44+
if err != nil {
45+
return nil, err
46+
}
47+
for _, e := range edgesFrom {
48+
if !visited[e.ToNodeID] {
49+
visited[e.ToNodeID] = true
50+
next = append(next, e.ToNodeID)
5951
}
6052
}
61-
frontier = next
62-
}
6353

64-
result := make([]model.Node, 0, len(visited))
65-
for id := range visited {
66-
node, err := a.store.GetNodeByID(ctx, id)
54+
edgesTo, err := a.store.GetEdgesToNodes(ctx, frontier)
6755
if err != nil {
6856
return nil, err
6957
}
70-
if node != nil {
71-
result = append(result, *node)
58+
for _, e := range edgesTo {
59+
if !visited[e.FromNodeID] {
60+
visited[e.FromNodeID] = true
61+
next = append(next, e.FromNodeID)
62+
}
7263
}
64+
65+
frontier = next
7366
}
74-
return result, nil
67+
68+
var allVisited []uint
69+
for id := range visited {
70+
allVisited = append(allVisited, id)
71+
}
72+
73+
nodes, err := a.store.GetNodesByIDs(ctx, allVisited)
74+
if err != nil {
75+
return nil, err
76+
}
77+
78+
return nodes, nil
7579
}

internal/analysis/impact/impact_test.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,40 @@ func (m *mockStore) GetNodeByID(_ context.Context, id uint) (*model.Node, error)
3737
return n, nil
3838
}
3939

40+
func (m *mockStore) GetEdgesFromNodes(_ context.Context, nodeIDs []uint) ([]model.Edge, error) {
41+
var result []model.Edge
42+
for _, id := range nodeIDs {
43+
result = append(result, m.edges[id]...)
44+
}
45+
return result, nil
46+
}
47+
48+
func (m *mockStore) GetEdgesToNodes(_ context.Context, nodeIDs []uint) ([]model.Edge, error) {
49+
var result []model.Edge
50+
idMap := make(map[uint]bool)
51+
for _, id := range nodeIDs {
52+
idMap[id] = true
53+
}
54+
for _, edgeList := range m.edges {
55+
for _, e := range edgeList {
56+
if idMap[e.ToNodeID] {
57+
result = append(result, e)
58+
}
59+
}
60+
}
61+
return result, nil
62+
}
63+
64+
func (m *mockStore) GetNodesByIDs(_ context.Context, ids []uint) ([]model.Node, error) {
65+
var result []model.Node
66+
for _, id := range ids {
67+
if n, ok := m.nodes[id]; ok {
68+
result = append(result, *n)
69+
}
70+
}
71+
return result, nil
72+
}
73+
4074
func newNode(id uint, name string) *model.Node {
4175
return &model.Node{ID: id, QualifiedName: name, Kind: model.NodeKindFunction, Name: name, FilePath: "a.go", StartLine: 1, EndLine: 2, Language: "go"}
4276
}

internal/cli/.ccg.yaml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
rules:
2+
- pattern: "pkg/bare.go"
3+
category: missing
4+
action: warn
5+
auto: true
6+
created: "2026-04-14"
7+
- pattern: "pkg/bare.go::Bare"
8+
category: unannotated
9+
action: warn
10+
auto: true
11+
created: "2026-04-14"

0 commit comments

Comments
 (0)