Merge pull request #62 from colbymchenry/fix/fk-constraint-empty-names

fix: Prevent FK constraint failure from nodes with empty names
This commit is contained in:
Colby Mchenry
2026-03-18 14:36:16 -05:00
committed by GitHub
2 changed files with 66 additions and 34 deletions
+21 -5
View File
@@ -650,25 +650,41 @@ export class ExtractionOrchestrator {
this.queries.deleteFile(filePath); this.queries.deleteFile(filePath);
} }
// Filter out nodes with missing required fields before insertion.
// This prevents FK violations when edges reference nodes that would
// be silently skipped by insertNode() (see issue #42).
const validNodes = result.nodes.filter((n) => n.id && n.kind && n.name && n.filePath && n.language);
// Insert nodes // Insert nodes
if (result.nodes.length > 0) { if (validNodes.length > 0) {
this.queries.insertNodes(result.nodes); this.queries.insertNodes(validNodes);
} }
// Insert edges // Filter edges to only reference nodes that were actually inserted
if (result.edges.length > 0) { if (result.edges.length > 0) {
this.queries.insertEdges(result.edges); const insertedIds = new Set(validNodes.map((n) => n.id));
const validEdges = result.edges.filter(
(e) => insertedIds.has(e.source) && insertedIds.has(e.target)
);
if (validEdges.length > 0) {
this.queries.insertEdges(validEdges);
}
} }
// Insert unresolved references in batch with denormalized filePath/language // Insert unresolved references in batch with denormalized filePath/language
if (result.unresolvedReferences.length > 0) { if (result.unresolvedReferences.length > 0) {
const refsWithContext = result.unresolvedReferences.map((ref) => ({ const insertedIds = new Set(validNodes.map((n) => n.id));
const refsWithContext = result.unresolvedReferences
.filter((ref) => insertedIds.has(ref.fromNodeId))
.map((ref) => ({
...ref, ...ref,
filePath: ref.filePath ?? filePath, filePath: ref.filePath ?? filePath,
language: ref.language ?? language, language: ref.language ?? language,
})); }));
if (refsWithContext.length > 0) {
this.queries.insertUnresolvedRefsBatch(refsWithContext); this.queries.insertUnresolvedRefsBatch(refsWithContext);
} }
}
// Insert file record // Insert file record
const fileRecord: FileRecord = { const fileRecord: FileRecord = {
+17 -1
View File
@@ -1086,7 +1086,13 @@ export class TreeSitterExtractor {
name: string, name: string,
node: SyntaxNode, node: SyntaxNode,
extra?: Partial<Node> extra?: Partial<Node>
): Node { ): Node | null {
// Skip nodes with empty/missing names — they are not meaningful symbols
// and would cause FK violations when edges reference them (see issue #42)
if (!name) {
return null;
}
const id = generateNodeId(this.filePath, kind, name, node.startPosition.row + 1); const id = generateNodeId(this.filePath, kind, name, node.startPosition.row + 1);
const newNode: Node = { const newNode: Node = {
@@ -1209,6 +1215,7 @@ export class TreeSitterExtractor {
isAsync, isAsync,
isStatic, isStatic,
}); });
if (!funcNode) return;
// Push to stack and visit body // Push to stack and visit body
this.nodeStack.push(funcNode.id); this.nodeStack.push(funcNode.id);
@@ -1238,6 +1245,7 @@ export class TreeSitterExtractor {
visibility, visibility,
isExported, isExported,
}); });
if (!classNode) return;
// Extract extends/implements // Extract extends/implements
this.extractInheritance(node, classNode.id); this.extractInheritance(node, classNode.id);
@@ -1291,6 +1299,7 @@ export class TreeSitterExtractor {
isAsync, isAsync,
isStatic, isStatic,
}); });
if (!methodNode) return;
// Push to stack and visit body // Push to stack and visit body
this.nodeStack.push(methodNode.id); this.nodeStack.push(methodNode.id);
@@ -1340,6 +1349,7 @@ export class TreeSitterExtractor {
visibility, visibility,
isExported, isExported,
}); });
if (!structNode) return;
// Push to stack for field extraction // Push to stack for field extraction
this.nodeStack.push(structNode.id); this.nodeStack.push(structNode.id);
@@ -2214,6 +2224,7 @@ export class TreeSitterExtractor {
if (declClass) { if (declClass) {
const classNode = this.createNode('class', name, node); const classNode = this.createNode('class', name, node);
if (classNode) {
// Extract inheritance from typeref children of declClass // Extract inheritance from typeref children of declClass
this.extractPascalInheritance(declClass, classNode.id); this.extractPascalInheritance(declClass, classNode.id);
// Visit class body // Visit class body
@@ -2223,8 +2234,10 @@ export class TreeSitterExtractor {
if (child) this.visitNode(child); if (child) this.visitNode(child);
} }
this.nodeStack.pop(); this.nodeStack.pop();
}
} else if (declIntf) { } else if (declIntf) {
const ifaceNode = this.createNode('interface', name, node); const ifaceNode = this.createNode('interface', name, node);
if (ifaceNode) {
// Visit interface members // Visit interface members
this.nodeStack.push(ifaceNode.id); this.nodeStack.push(ifaceNode.id);
for (let i = 0; i < declIntf.namedChildCount; i++) { for (let i = 0; i < declIntf.namedChildCount; i++) {
@@ -2232,6 +2245,7 @@ export class TreeSitterExtractor {
if (child) this.visitNode(child); if (child) this.visitNode(child);
} }
this.nodeStack.pop(); this.nodeStack.pop();
}
} else if (typeChild) { } else if (typeChild) {
// Check if it contains a declEnum // Check if it contains a declEnum
const declEnum = typeChild.namedChildren.find( const declEnum = typeChild.namedChildren.find(
@@ -2239,6 +2253,7 @@ export class TreeSitterExtractor {
); );
if (declEnum) { if (declEnum) {
const enumNode = this.createNode('enum', name, node); const enumNode = this.createNode('enum', name, node);
if (enumNode) {
// Extract enum members // Extract enum members
this.nodeStack.push(enumNode.id); this.nodeStack.push(enumNode.id);
for (let i = 0; i < declEnum.namedChildCount; i++) { for (let i = 0; i < declEnum.namedChildCount; i++) {
@@ -2251,6 +2266,7 @@ export class TreeSitterExtractor {
} }
} }
this.nodeStack.pop(); this.nodeStack.pop();
}
} else { } else {
// Simple type alias: type TFoo = string / type TFoo = Integer // Simple type alias: type TFoo = string / type TFoo = Integer
this.createNode('type_alias', name, node); this.createNode('type_alias', name, node);