diff --git a/packages/schemainspect/src/pg/obj.ts b/packages/schemainspect/src/pg/obj.ts index 75204078..926c0001 100644 --- a/packages/schemainspect/src/pg/obj.ts +++ b/packages/schemainspect/src/pg/obj.ts @@ -1427,16 +1427,24 @@ export class PostgreSQL extends DBInspector { const get_related_for_item = ( item: InspectedSelectable | InspectedTrigger | InspectedEnum, att: 'dependent_on' | 'dependents', + visited: Set = new Set(), ): string[] => { + if (visited.has(item.signature)) { + return [] + } + visited.add(item.signature) const related = item[att].map((child: string) => this.get_dependency_by_signature(child)) - return [item.signature, ...related.filter(d => d).flatMap(d => get_related_for_item(d, att))] + return [item.signature, ...related.filter(d => d).flatMap(d => get_related_for_item(d, att, visited))] } for (const [k, x] of Object.entries(this.selectables)) { - let d_all = get_related_for_item(x, 'dependent_on').slice(1) + const rootSig = x.signature + // Get all dependencies and remove the root item itself (first occurrence via slice(1)) + // Then filter out any remaining occurrences of root (from circular dependencies) + let d_all = get_related_for_item(x, 'dependent_on').slice(1).filter(sig => sig !== rootSig) d_all.sort() x.dependent_on_all = d_all - d_all = get_related_for_item(x, 'dependents').slice(1) + d_all = get_related_for_item(x, 'dependents').slice(1).filter(sig => sig !== rootSig) d_all.sort() x.dependents_all = d_all } diff --git a/packages/schemainspect/test/circular-deps.test.ts b/packages/schemainspect/test/circular-deps.test.ts new file mode 100644 index 00000000..95ead16b --- /dev/null +++ b/packages/schemainspect/test/circular-deps.test.ts @@ -0,0 +1,36 @@ +import {expect, test} from 'vitest' +import {InspectedSelectable, PostgreSQL} from '../src/pg' + +function makeSelectable(name: string) { + return new InspectedSelectable({ + name, + schema: 'public', + columns: {}, + definition: '', + relationtype: 'v', + comment: '', + }) +} + +test('load_deps_all does not blow the stack on circular view dependencies', async () => { + const db = PostgreSQL.empty() + + const a = makeSelectable('a') + const b = makeSelectable('b') + + // Two views that depend on each other -- this can happen with e.g. matching pg_depend rows for + // mutually-referencing views/rules, and previously caused infinite recursion in + // get_related_for_item since it revisited the same signature forever. + a.dependent_on.push(b.signature) + b.dependents.push(a.signature) + b.dependent_on.push(a.signature) + a.dependents.push(b.signature) + + db.selectables[a.signature] = a + db.selectables[b.signature] = b + + await expect(db.load_deps_all()).resolves.not.toThrow() + + expect(a.dependent_on_all).toEqual([b.signature]) + expect(b.dependent_on_all).toEqual([a.signature]) +})