Skip to content

Commit bdbe59b

Browse files
authored
Merge pull request colbymchenry#62 from colbymchenry/fix/fk-constraint-empty-names
fix: Prevent FK constraint failure from nodes with empty names
2 parents 3ffcac7 + 92631d5 commit bdbe59b

2 files changed

Lines changed: 66 additions & 34 deletions

File tree

src/extraction/index.ts

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -650,24 +650,40 @@ export class ExtractionOrchestrator {
650650
this.queries.deleteFile(filePath);
651651
}
652652

653+
// Filter out nodes with missing required fields before insertion.
654+
// This prevents FK violations when edges reference nodes that would
655+
// be silently skipped by insertNode() (see issue #42).
656+
const validNodes = result.nodes.filter((n) => n.id && n.kind && n.name && n.filePath && n.language);
657+
653658
// Insert nodes
654-
if (result.nodes.length > 0) {
655-
this.queries.insertNodes(result.nodes);
659+
if (validNodes.length > 0) {
660+
this.queries.insertNodes(validNodes);
656661
}
657662

658-
// Insert edges
663+
// Filter edges to only reference nodes that were actually inserted
659664
if (result.edges.length > 0) {
660-
this.queries.insertEdges(result.edges);
665+
const insertedIds = new Set(validNodes.map((n) => n.id));
666+
const validEdges = result.edges.filter(
667+
(e) => insertedIds.has(e.source) && insertedIds.has(e.target)
668+
);
669+
if (validEdges.length > 0) {
670+
this.queries.insertEdges(validEdges);
671+
}
661672
}
662673

663674
// Insert unresolved references in batch with denormalized filePath/language
664675
if (result.unresolvedReferences.length > 0) {
665-
const refsWithContext = result.unresolvedReferences.map((ref) => ({
666-
...ref,
667-
filePath: ref.filePath ?? filePath,
668-
language: ref.language ?? language,
669-
}));
670-
this.queries.insertUnresolvedRefsBatch(refsWithContext);
676+
const insertedIds = new Set(validNodes.map((n) => n.id));
677+
const refsWithContext = result.unresolvedReferences
678+
.filter((ref) => insertedIds.has(ref.fromNodeId))
679+
.map((ref) => ({
680+
...ref,
681+
filePath: ref.filePath ?? filePath,
682+
language: ref.language ?? language,
683+
}));
684+
if (refsWithContext.length > 0) {
685+
this.queries.insertUnresolvedRefsBatch(refsWithContext);
686+
}
671687
}
672688

673689
// Insert file record

src/extraction/tree-sitter.ts

Lines changed: 40 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1086,7 +1086,13 @@ export class TreeSitterExtractor {
10861086
name: string,
10871087
node: SyntaxNode,
10881088
extra?: Partial<Node>
1089-
): Node {
1089+
): Node | null {
1090+
// Skip nodes with empty/missing names — they are not meaningful symbols
1091+
// and would cause FK violations when edges reference them (see issue #42)
1092+
if (!name) {
1093+
return null;
1094+
}
1095+
10901096
const id = generateNodeId(this.filePath, kind, name, node.startPosition.row + 1);
10911097

10921098
const newNode: Node = {
@@ -1209,6 +1215,7 @@ export class TreeSitterExtractor {
12091215
isAsync,
12101216
isStatic,
12111217
});
1218+
if (!funcNode) return;
12121219

12131220
// Push to stack and visit body
12141221
this.nodeStack.push(funcNode.id);
@@ -1238,6 +1245,7 @@ export class TreeSitterExtractor {
12381245
visibility,
12391246
isExported,
12401247
});
1248+
if (!classNode) return;
12411249

12421250
// Extract extends/implements
12431251
this.extractInheritance(node, classNode.id);
@@ -1291,6 +1299,7 @@ export class TreeSitterExtractor {
12911299
isAsync,
12921300
isStatic,
12931301
});
1302+
if (!methodNode) return;
12941303

12951304
// Push to stack and visit body
12961305
this.nodeStack.push(methodNode.id);
@@ -1340,6 +1349,7 @@ export class TreeSitterExtractor {
13401349
visibility,
13411350
isExported,
13421351
});
1352+
if (!structNode) return;
13431353

13441354
// Push to stack for field extraction
13451355
this.nodeStack.push(structNode.id);
@@ -2214,43 +2224,49 @@ export class TreeSitterExtractor {
22142224

22152225
if (declClass) {
22162226
const classNode = this.createNode('class', name, node);
2217-
// Extract inheritance from typeref children of declClass
2218-
this.extractPascalInheritance(declClass, classNode.id);
2219-
// Visit class body
2220-
this.nodeStack.push(classNode.id);
2221-
for (let i = 0; i < declClass.namedChildCount; i++) {
2222-
const child = declClass.namedChild(i);
2223-
if (child) this.visitNode(child);
2227+
if (classNode) {
2228+
// Extract inheritance from typeref children of declClass
2229+
this.extractPascalInheritance(declClass, classNode.id);
2230+
// Visit class body
2231+
this.nodeStack.push(classNode.id);
2232+
for (let i = 0; i < declClass.namedChildCount; i++) {
2233+
const child = declClass.namedChild(i);
2234+
if (child) this.visitNode(child);
2235+
}
2236+
this.nodeStack.pop();
22242237
}
2225-
this.nodeStack.pop();
22262238
} else if (declIntf) {
22272239
const ifaceNode = this.createNode('interface', name, node);
2228-
// Visit interface members
2229-
this.nodeStack.push(ifaceNode.id);
2230-
for (let i = 0; i < declIntf.namedChildCount; i++) {
2231-
const child = declIntf.namedChild(i);
2232-
if (child) this.visitNode(child);
2240+
if (ifaceNode) {
2241+
// Visit interface members
2242+
this.nodeStack.push(ifaceNode.id);
2243+
for (let i = 0; i < declIntf.namedChildCount; i++) {
2244+
const child = declIntf.namedChild(i);
2245+
if (child) this.visitNode(child);
2246+
}
2247+
this.nodeStack.pop();
22332248
}
2234-
this.nodeStack.pop();
22352249
} else if (typeChild) {
22362250
// Check if it contains a declEnum
22372251
const declEnum = typeChild.namedChildren.find(
22382252
(c: SyntaxNode) => c.type === 'declEnum'
22392253
);
22402254
if (declEnum) {
22412255
const enumNode = this.createNode('enum', name, node);
2242-
// Extract enum members
2243-
this.nodeStack.push(enumNode.id);
2244-
for (let i = 0; i < declEnum.namedChildCount; i++) {
2245-
const child = declEnum.namedChild(i);
2246-
if (child?.type === 'declEnumValue') {
2247-
const memberName = getChildByField(child, 'name');
2248-
if (memberName) {
2249-
this.createNode('enum_member', getNodeText(memberName, this.source), child);
2256+
if (enumNode) {
2257+
// Extract enum members
2258+
this.nodeStack.push(enumNode.id);
2259+
for (let i = 0; i < declEnum.namedChildCount; i++) {
2260+
const child = declEnum.namedChild(i);
2261+
if (child?.type === 'declEnumValue') {
2262+
const memberName = getChildByField(child, 'name');
2263+
if (memberName) {
2264+
this.createNode('enum_member', getNodeText(memberName, this.source), child);
2265+
}
22502266
}
22512267
}
2268+
this.nodeStack.pop();
22522269
}
2253-
this.nodeStack.pop();
22542270
} else {
22552271
// Simple type alias: type TFoo = string / type TFoo = Integer
22562272
this.createNode('type_alias', name, node);

0 commit comments

Comments
 (0)