Skip to content

Commit 3b50d21

Browse files
committed
fix: store graph strings as text
1 parent 8f2ddb7 commit 3b50d21

18 files changed

Lines changed: 351 additions & 44 deletions

cmd/ccg/main_postgres_test.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,19 @@ func TestRunMigrations_PostgresSmoke(t *testing.T) {
8080
t.Fatalf("expected migration 007 to remove %s at schema version %d", table, migration.RequiredSchemaVersion)
8181
}
8282
}
83+
84+
var varcharCount int64
85+
if err := db.Raw(`
86+
SELECT COUNT(*)
87+
FROM information_schema.columns
88+
WHERE table_schema = current_schema()
89+
AND data_type = 'character varying'
90+
`).Scan(&varcharCount).Error; err != nil {
91+
t.Fatalf("count varchar columns: %v", err)
92+
}
93+
if varcharCount != 0 {
94+
t.Fatalf("PostgreSQL schema retained %d varchar columns, want 0", varcharCount)
95+
}
8396
}
8497

8598
func TestRunMigrations_PostgresVersionThreePolicyTablesUseJSONB(t *testing.T) {
@@ -119,6 +132,40 @@ func TestRunMigrations_PostgresVersionThreePolicyTablesUseJSONB(t *testing.T) {
119132
}
120133
}
121134

135+
func TestRunMigrations_PostgresVersionFourteenConvertsGraphStringsAndCanMigrateDown(t *testing.T) {
136+
db := setupPostgresMigrationDB(t)
137+
138+
migrator, _, err := migration.NewMigrator(db, "postgres", "")
139+
if err != nil {
140+
t.Fatalf("create migrator: %v", err)
141+
}
142+
if err := migrator.Steps(13); err != nil {
143+
t.Fatalf("run migrations through version 13: %v", err)
144+
}
145+
assertPostgresColumnType(t, db, "nodes", "name", "character varying")
146+
147+
if err := migrator.Steps(1); err != nil {
148+
t.Fatalf("migrate to version 14: %v", err)
149+
}
150+
assertPostgresColumnType(t, db, "nodes", "name", "text")
151+
152+
if err := migrator.Steps(-1); err != nil {
153+
t.Fatalf("migrate down to version 13: %v", err)
154+
}
155+
assertPostgresColumnType(t, db, "nodes", "name", "character varying")
156+
}
157+
158+
func assertPostgresColumnType(t *testing.T, db *gorm.DB, table, column, want string) {
159+
t.Helper()
160+
got, err := migration.PostgresColumnDataType(db, table, column)
161+
if err != nil {
162+
t.Fatalf("inspect %s.%s type: %v", table, column, err)
163+
}
164+
if got != want {
165+
t.Fatalf("%s.%s type = %q, want %q", table, column, got, want)
166+
}
167+
}
168+
122169
func TestRunMigrations_PostgresBackfillsVersionOneNulls(t *testing.T) {
123170
db := setupPostgresMigrationDB(t)
124171

internal/adapters/outbound/graphgorm/store_postgres_test.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,32 @@ func TestWithTxDB_PostgresRollsBackGraphAndSearchDocumentTogether(t *testing.T)
5151
assertGraphAndSearchDocumentCounts(t, db, "txdb.PostgresRollback", "postgres rolled back search document", 0)
5252
}
5353

54+
func TestUpsertNodes_PostgresAllowsFileNameLongerThan256Characters(t *testing.T) {
55+
s, db := setupIsolatedPostgresStore(t)
56+
longPath := strings.Repeat("nested-directory/", 20) + "component.tsx"
57+
node := graph.Node{
58+
QualifiedName: longPath,
59+
Kind: graph.NodeKindFile,
60+
Name: longPath,
61+
FilePath: longPath,
62+
StartLine: 1,
63+
EndLine: 1,
64+
Language: "typescript",
65+
}
66+
67+
if err := s.UpsertNodes(context.Background(), []graph.Node{node}); err != nil {
68+
t.Fatalf("upsert long file node: %v", err)
69+
}
70+
71+
var persisted graph.Node
72+
if err := db.Where("qualified_name = ?", longPath).First(&persisted).Error; err != nil {
73+
t.Fatalf("load long file node: %v", err)
74+
}
75+
if persisted.Name != longPath {
76+
t.Fatalf("persisted name length = %d, want %d", len(persisted.Name), len(longPath))
77+
}
78+
}
79+
5480
func TestDeleteGraph_PostgresHandlesMoreThanBindParameterLimit(t *testing.T) {
5581
s, db := setupIsolatedPostgresStore(t)
5682
ctx := context.Background()

internal/db/migration/migration.go

Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import (
2323
)
2424

2525
const (
26-
RequiredSchemaVersion = 13
26+
RequiredSchemaVersion = 14
2727
SchemaVersionKey = "schema"
2828
LegacySchemaVersionTable = "ccg_schema_versions"
2929
)
@@ -388,6 +388,57 @@ func RequiredSchemaTables() []string {
388388
}
389389
}
390390

391+
// RequiredTextColumns lists graph string columns that must remain unbounded across database drivers.
392+
// @intent prevent source-derived graph values from failing persistence because of arbitrary varchar widths.
393+
func RequiredTextColumns() []SchemaColumn {
394+
return []SchemaColumn{
395+
{Table: "nodes", Column: "namespace"},
396+
{Table: "nodes", Column: "qualified_name"},
397+
{Table: "nodes", Column: "kind"},
398+
{Table: "nodes", Column: "name"},
399+
{Table: "nodes", Column: "file_path"},
400+
{Table: "nodes", Column: "hash"},
401+
{Table: "nodes", Column: "language"},
402+
{Table: "edges", Column: "namespace"},
403+
{Table: "edges", Column: "kind"},
404+
{Table: "edges", Column: "file_path"},
405+
{Table: "edges", Column: "fingerprint"},
406+
{Table: "annotations", Column: "summary"},
407+
{Table: "annotations", Column: "context"},
408+
{Table: "annotations", Column: "raw_text"},
409+
{Table: "doc_tags", Column: "kind"},
410+
{Table: "doc_tags", Column: "type"},
411+
{Table: "doc_tags", Column: "name"},
412+
{Table: "doc_tags", Column: "value"},
413+
{Table: "communities", Column: "namespace"},
414+
{Table: "communities", Column: "key"},
415+
{Table: "communities", Column: "label"},
416+
{Table: "communities", Column: "strategy"},
417+
{Table: "communities", Column: "description"},
418+
{Table: "flows", Column: "namespace"},
419+
{Table: "flows", Column: "name"},
420+
{Table: "flows", Column: "description"},
421+
{Table: "flow_memberships", Column: "namespace"},
422+
{Table: "search_documents", Column: "namespace"},
423+
{Table: "search_documents", Column: "content"},
424+
{Table: "search_documents", Column: "language"},
425+
{Table: "parse_cache_entries", Column: "namespace"},
426+
{Table: "parse_cache_entries", Column: "file_path"},
427+
{Table: "parse_cache_entries", Column: "source_hash"},
428+
{Table: "parse_cache_entries", Column: "parser_version"},
429+
{Table: "parse_cache_entries", Column: "context_hash"},
430+
{Table: "unresolved_edge_candidates", Column: "namespace"},
431+
{Table: "unresolved_edge_candidates", Column: "lookup_key"},
432+
{Table: "unresolved_edge_candidates", Column: "lookup_key_hash"},
433+
{Table: "unresolved_edge_candidates", Column: "fingerprint"},
434+
{Table: "unresolved_edge_candidates", Column: "fingerprint_hash"},
435+
{Table: "unresolved_edge_candidates", Column: "file_path"},
436+
{Table: "unresolved_edge_candidates", Column: "kind"},
437+
{Table: "unresolved_index_states", Column: "namespace"},
438+
{Table: "unresolved_index_states", Column: "version"},
439+
}
440+
}
441+
391442
// ModelNullabilityColumns enumerates columns that must remain NOT NULL for model invariants.
392443
// @intent 주요 모델 필드의 nullable drift를 런타임 검증에서 감지한다.
393444
func ModelNullabilityColumns() []SchemaColumn {
@@ -585,7 +636,7 @@ func validateSQLiteSchemaParity(db *gorm.DB) error {
585636
return nil
586637
}
587638

588-
// validatePostgresSchemaParity checks PostgreSQL-only indexes, triggers, and JSONB column types.
639+
// validatePostgresSchemaParity checks PostgreSQL-only indexes, triggers, and column types.
589640
// @intent PostgreSQL 검색/후처리 스키마가 운영 계약과 일치하는지 확인한다.
590641
func validatePostgresSchemaParity(db *gorm.DB) error {
591642
for _, column := range ModelNullabilityColumns() {
@@ -621,13 +672,10 @@ func validatePostgresSchemaParity(db *gorm.DB) error {
621672
return fmt.Errorf("required index %q is missing", indexName)
622673
}
623674
}
624-
for _, column := range []SchemaColumn{
625-
{Table: "annotations", Column: "summary"},
626-
{Table: "annotations", Column: "context"},
627-
} {
675+
for _, column := range RequiredTextColumns() {
628676
dataType, err := postgresColumnDataType(db, column.Table, column.Column)
629677
if err != nil {
630-
return trace.Wrap(err, "inspect postgres annotation column type")
678+
return trace.Wrap(err, "inspect postgres text column type")
631679
}
632680
if dataType != "text" {
633681
return fmt.Errorf("required column %q.%q has type %q, want text", column.Table, column.Column, dataType)

internal/db/migration/migration_test.go

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,16 @@ package migration
22

33
import (
44
"path/filepath"
5+
"strings"
56
"testing"
67

78
"gorm.io/driver/sqlite"
89
"gorm.io/gorm"
910
)
1011

11-
func TestRequiredSchemaVersion_IncludesUnboundedAnnotationsAndResolverFileLookupIndex(t *testing.T) {
12-
if RequiredSchemaVersion != 13 {
13-
t.Fatalf("RequiredSchemaVersion = %d, want 13", RequiredSchemaVersion)
12+
func TestRequiredSchemaVersion_IncludesUnboundedGraphStrings(t *testing.T) {
13+
if RequiredSchemaVersion != 14 {
14+
t.Fatalf("RequiredSchemaVersion = %d, want 14", RequiredSchemaVersion)
1415
}
1516
}
1617

@@ -141,3 +142,39 @@ func TestSQLiteMigrationThirteen_AddsResolverFileLookupIndexAndCanMigrateDown(t
141142
t.Fatalf("version 12 retained index %q", indexName)
142143
}
143144
}
145+
146+
func TestSQLiteMigrationFourteen_PreservesTextColumnsAndCanMigrateDown(t *testing.T) {
147+
dsn := filepath.Join(t.TempDir(), "migration.db")
148+
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
149+
if err != nil {
150+
t.Fatalf("open SQLite: %v", err)
151+
}
152+
migrator, _, err := NewMigrator(db, "sqlite", "")
153+
if err != nil {
154+
t.Fatalf("NewMigrator: %v", err)
155+
}
156+
if err := migrator.Steps(14); err != nil {
157+
t.Fatalf("migrate to version 14: %v", err)
158+
}
159+
160+
columns, err := db.Migrator().ColumnTypes("nodes")
161+
if err != nil {
162+
t.Fatalf("inspect nodes.name: %v", err)
163+
}
164+
foundName := false
165+
for _, column := range columns {
166+
if column.Name() != "name" {
167+
continue
168+
}
169+
foundName = true
170+
if !strings.EqualFold(column.DatabaseTypeName(), "text") {
171+
t.Fatalf("nodes.name type = %q, want TEXT", column.DatabaseTypeName())
172+
}
173+
}
174+
if !foundName {
175+
t.Fatal("nodes.name column is missing")
176+
}
177+
if err := migrator.Steps(-1); err != nil {
178+
t.Fatalf("migrate down to version 13: %v", err)
179+
}
180+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
ALTER TABLE unresolved_index_states
2+
ALTER COLUMN namespace TYPE varchar(256),
3+
ALTER COLUMN version TYPE varchar(64);
4+
5+
ALTER TABLE unresolved_edge_candidates
6+
ALTER COLUMN namespace TYPE varchar(256),
7+
ALTER COLUMN lookup_key_hash TYPE varchar(64),
8+
ALTER COLUMN fingerprint_hash TYPE varchar(64),
9+
ALTER COLUMN file_path TYPE varchar(1024),
10+
ALTER COLUMN kind TYPE varchar(32);
11+
12+
ALTER TABLE parse_cache_entries
13+
ALTER COLUMN namespace TYPE varchar(256),
14+
ALTER COLUMN file_path TYPE varchar(768),
15+
ALTER COLUMN source_hash TYPE varchar(64),
16+
ALTER COLUMN parser_version TYPE varchar(160),
17+
ALTER COLUMN context_hash TYPE varchar(64);
18+
19+
ALTER TABLE search_documents
20+
ALTER COLUMN namespace TYPE varchar(256),
21+
ALTER COLUMN language TYPE varchar(32);
22+
23+
ALTER TABLE flow_memberships
24+
ALTER COLUMN namespace TYPE varchar(256);
25+
26+
ALTER TABLE flows
27+
ALTER COLUMN namespace TYPE varchar(256),
28+
ALTER COLUMN name TYPE varchar(256);
29+
30+
ALTER TABLE communities
31+
ALTER COLUMN namespace TYPE varchar(256),
32+
ALTER COLUMN key TYPE varchar(512),
33+
ALTER COLUMN label TYPE varchar(256),
34+
ALTER COLUMN strategy TYPE varchar(32);
35+
36+
ALTER TABLE doc_tags
37+
ALTER COLUMN kind TYPE varchar(32),
38+
ALTER COLUMN name TYPE varchar(128);
39+
40+
ALTER TABLE edges
41+
ALTER COLUMN namespace TYPE varchar(256),
42+
ALTER COLUMN kind TYPE varchar(32),
43+
ALTER COLUMN file_path TYPE varchar(1024);
44+
45+
ALTER TABLE nodes
46+
ALTER COLUMN namespace TYPE varchar(256),
47+
ALTER COLUMN qualified_name TYPE varchar(512),
48+
ALTER COLUMN kind TYPE varchar(32),
49+
ALTER COLUMN name TYPE varchar(256),
50+
ALTER COLUMN file_path TYPE varchar(768),
51+
ALTER COLUMN hash TYPE varchar(64),
52+
ALTER COLUMN language TYPE varchar(32);
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
ALTER TABLE nodes
2+
ALTER COLUMN namespace TYPE text,
3+
ALTER COLUMN qualified_name TYPE text,
4+
ALTER COLUMN kind TYPE text,
5+
ALTER COLUMN name TYPE text,
6+
ALTER COLUMN file_path TYPE text,
7+
ALTER COLUMN hash TYPE text,
8+
ALTER COLUMN language TYPE text;
9+
10+
ALTER TABLE edges
11+
ALTER COLUMN namespace TYPE text,
12+
ALTER COLUMN kind TYPE text,
13+
ALTER COLUMN file_path TYPE text;
14+
15+
ALTER TABLE doc_tags
16+
ALTER COLUMN kind TYPE text,
17+
ALTER COLUMN name TYPE text;
18+
19+
ALTER TABLE communities
20+
ALTER COLUMN namespace TYPE text,
21+
ALTER COLUMN key TYPE text,
22+
ALTER COLUMN label TYPE text,
23+
ALTER COLUMN strategy TYPE text;
24+
25+
ALTER TABLE flows
26+
ALTER COLUMN namespace TYPE text,
27+
ALTER COLUMN name TYPE text;
28+
29+
ALTER TABLE flow_memberships
30+
ALTER COLUMN namespace TYPE text;
31+
32+
ALTER TABLE search_documents
33+
ALTER COLUMN namespace TYPE text,
34+
ALTER COLUMN language TYPE text;
35+
36+
ALTER TABLE parse_cache_entries
37+
ALTER COLUMN namespace TYPE text,
38+
ALTER COLUMN file_path TYPE text,
39+
ALTER COLUMN source_hash TYPE text,
40+
ALTER COLUMN parser_version TYPE text,
41+
ALTER COLUMN context_hash TYPE text;
42+
43+
ALTER TABLE unresolved_edge_candidates
44+
ALTER COLUMN namespace TYPE text,
45+
ALTER COLUMN lookup_key_hash TYPE text,
46+
ALTER COLUMN fingerprint_hash TYPE text,
47+
ALTER COLUMN file_path TYPE text,
48+
ALTER COLUMN kind TYPE text;
49+
50+
ALTER TABLE unresolved_index_states
51+
ALTER COLUMN namespace TYPE text,
52+
ALTER COLUMN version TYPE text;
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
-- Version alignment only; SQLite requires no schema reversal.
2+
SELECT 1;
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
-- SQLite graph string columns have always used unbounded text.
2+
SELECT 1;

internal/domain/graph/annotation.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,9 @@ type Annotation struct {
4444
type DocTag struct {
4545
ID uint `gorm:"primaryKey"`
4646
AnnotationID uint `gorm:"not null;index"`
47-
Kind TagKind `gorm:"size:32;not null;index"`
47+
Kind TagKind `gorm:"type:text;not null;index"`
4848
Type string `gorm:"type:text"`
49-
Name string `gorm:"size:128"`
49+
Name string `gorm:"type:text"`
5050
Value string `gorm:"type:text;not null"`
5151
Ordinal int `gorm:"not null"`
5252
CreatedAt time.Time

internal/domain/graph/community.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ import "time"
66
// @intent 연관된 노드 집합을 전략별 커뮤니티 단위로 표현한다.
77
type Community struct {
88
ID uint `gorm:"primaryKey"`
9-
Namespace string `gorm:"size:256;not null;default:'default';uniqueIndex:idx_community_ns_key"`
10-
Key string `gorm:"size:512;not null;uniqueIndex:idx_community_ns_key"`
11-
Label string `gorm:"size:256;not null"`
12-
Strategy string `gorm:"size:32;not null;index"`
9+
Namespace string `gorm:"type:text;not null;default:'default';uniqueIndex:idx_community_ns_key"`
10+
Key string `gorm:"type:text;not null;uniqueIndex:idx_community_ns_key"`
11+
Label string `gorm:"type:text;not null"`
12+
Strategy string `gorm:"type:text;not null;index"`
1313
Description string `gorm:"type:text"`
1414
CreatedAt time.Time
1515
UpdatedAt time.Time

0 commit comments

Comments
 (0)