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:
+26
-10
@@ -650,24 +650,40 @@ 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));
|
||||||
...ref,
|
const refsWithContext = result.unresolvedReferences
|
||||||
filePath: ref.filePath ?? filePath,
|
.filter((ref) => insertedIds.has(ref.fromNodeId))
|
||||||
language: ref.language ?? language,
|
.map((ref) => ({
|
||||||
}));
|
...ref,
|
||||||
this.queries.insertUnresolvedRefsBatch(refsWithContext);
|
filePath: ref.filePath ?? filePath,
|
||||||
|
language: ref.language ?? language,
|
||||||
|
}));
|
||||||
|
if (refsWithContext.length > 0) {
|
||||||
|
this.queries.insertUnresolvedRefsBatch(refsWithContext);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert file record
|
// Insert file record
|
||||||
|
|||||||
@@ -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,24 +2224,28 @@ export class TreeSitterExtractor {
|
|||||||
|
|
||||||
if (declClass) {
|
if (declClass) {
|
||||||
const classNode = this.createNode('class', name, node);
|
const classNode = this.createNode('class', name, node);
|
||||||
// Extract inheritance from typeref children of declClass
|
if (classNode) {
|
||||||
this.extractPascalInheritance(declClass, classNode.id);
|
// Extract inheritance from typeref children of declClass
|
||||||
// Visit class body
|
this.extractPascalInheritance(declClass, classNode.id);
|
||||||
this.nodeStack.push(classNode.id);
|
// Visit class body
|
||||||
for (let i = 0; i < declClass.namedChildCount; i++) {
|
this.nodeStack.push(classNode.id);
|
||||||
const child = declClass.namedChild(i);
|
for (let i = 0; i < declClass.namedChildCount; i++) {
|
||||||
if (child) this.visitNode(child);
|
const child = declClass.namedChild(i);
|
||||||
|
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);
|
||||||
// Visit interface members
|
if (ifaceNode) {
|
||||||
this.nodeStack.push(ifaceNode.id);
|
// Visit interface members
|
||||||
for (let i = 0; i < declIntf.namedChildCount; i++) {
|
this.nodeStack.push(ifaceNode.id);
|
||||||
const child = declIntf.namedChild(i);
|
for (let i = 0; i < declIntf.namedChildCount; i++) {
|
||||||
if (child) this.visitNode(child);
|
const child = declIntf.namedChild(i);
|
||||||
|
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,18 +2253,20 @@ export class TreeSitterExtractor {
|
|||||||
);
|
);
|
||||||
if (declEnum) {
|
if (declEnum) {
|
||||||
const enumNode = this.createNode('enum', name, node);
|
const enumNode = this.createNode('enum', name, node);
|
||||||
// Extract enum members
|
if (enumNode) {
|
||||||
this.nodeStack.push(enumNode.id);
|
// Extract enum members
|
||||||
for (let i = 0; i < declEnum.namedChildCount; i++) {
|
this.nodeStack.push(enumNode.id);
|
||||||
const child = declEnum.namedChild(i);
|
for (let i = 0; i < declEnum.namedChildCount; i++) {
|
||||||
if (child?.type === 'declEnumValue') {
|
const child = declEnum.namedChild(i);
|
||||||
const memberName = getChildByField(child, 'name');
|
if (child?.type === 'declEnumValue') {
|
||||||
if (memberName) {
|
const memberName = getChildByField(child, 'name');
|
||||||
this.createNode('enum_member', getNodeText(memberName, this.source), child);
|
if (memberName) {
|
||||||
|
this.createNode('enum_member', getNodeText(memberName, this.source), child);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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);
|
||||||
|
|||||||
Reference in New Issue
Block a user