Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,62 @@ function generateFinalScript(script_array, scriptHeader, script_body) {
return `${scriptHeader} BEGIN; \n ${script_body} END;`;
}

export function computeDependLevels(rows) {
/* Assign each row a dependLevel such that anything another row depends
on always ends up with a strictly higher level than that row, so that
generateFinalScript() (which writes levels highest-first) emits
dependencies before the objects that need them. Rows are matched by
oid, matching how dependenciesOid is resolved elsewhere in this file.

A row's level is therefore driven by what depends on it (the reverse
of its own "dependencies" list), computed as one more than the
highest level of anything that lists it as a dependency, bottoming
out at 1 for anything nothing else depends on. Circular dependencies
are broken by treating the row currently being resolved as level 1
for that edge, rather than recursing forever. */
const oidToRow = new Map();
rows.forEach((row) => {
if (!_.isUndefined(row.oid) && !_.isNull(row.oid)) {
oidToRow.set(row.oid, row);
}
});

const dependents = new Map();
rows.forEach((row) => {
(row.dependencies || []).forEach((dep) => {
let dependencyRow = oidToRow.get(dep.oid);
if (dependencyRow) {
if (!dependents.has(dependencyRow.id)) dependents.set(dependencyRow.id, []);
dependents.get(dependencyRow.id).push(row);
}
});
});

const levels = new Map();
const resolving = new Set();

function levelOf(row) {
if (levels.has(row.id)) return levels.get(row.id);
if (resolving.has(row.id)) return 1;
resolving.add(row.id);

let level = 1;
(dependents.get(row.id) || []).forEach((dependent) => {
level = Math.max(level, levelOf(dependent) + 1);
});

resolving.delete(row.id);
levels.set(row.id, level);
return level;
}

rows.forEach((row) => {
row.dependLevel = levelOf(row);
});

return rows;
}

function checkAndGetSchemaQuery(data, script_array) {
/* Check whether the selected object belongs to source only schema
if yes then we will have to add create schema statement before creating any other object.*/
Expand Down Expand Up @@ -363,6 +419,7 @@ export function SchemaDiffCompare({ params }) {
if (selectedIds.length > 0) {
let script_array = { 1: [], 2: [], 3: [], 4: [], 5: [] },
script_body = '';
computeDependLevels(rows);
getGenerateScriptData(rows, selectedIds, script_array, selectedFilters);

generatedScript = generateFinalScript(script_array, scriptHeader, script_body);
Expand Down
70 changes: 70 additions & 0 deletions web/regression/javascript/schema_diff/schema_diff_compare_spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2026, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////

import { computeDependLevels } from '../../../pgadmin/tools/schema_diff/static/js/components/SchemaDiffCompare';

describe('computeDependLevels', () => {

it('gives a chain of dependencies strictly increasing levels, deepest first', () => {
// A depends on B, B depends on C. C must end up with a higher level
// than B, and B a higher level than A, so a reverse walk (as
// generateFinalScript performs) writes C, then B, then A.
const rowA = { id: 'a', oid: 1, dependencies: [{ type: 'table', oid: 2 }] };
const rowB = { id: 'b', oid: 2, dependencies: [{ type: 'type', oid: 3 }] };
const rowC = { id: 'c', oid: 3, dependencies: [] };

computeDependLevels([rowA, rowB, rowC]);

expect(rowC.dependLevel).toBeGreaterThan(rowB.dependLevel);
expect(rowB.dependLevel).toBeGreaterThan(rowA.dependLevel);
});

it('leaves unrelated rows all at the base level', () => {
const rowA = { id: 'a', oid: 1, dependencies: [] };
const rowB = { id: 'b', oid: 2, dependencies: [] };

computeDependLevels([rowA, rowB]);

expect(rowA.dependLevel).toBe(1);
expect(rowB.dependLevel).toBe(1);
});

it('does not loop forever on a circular dependency', () => {
const rowA = { id: 'a', oid: 1, dependencies: [{ type: 'table', oid: 2 }] };
const rowB = { id: 'b', oid: 2, dependencies: [{ type: 'table', oid: 1 }] };

computeDependLevels([rowA, rowB]);

expect(Number.isFinite(rowA.dependLevel)).toBe(true);
expect(Number.isFinite(rowB.dependLevel)).toBe(true);
});

it('ignores dependencies that point at an oid not present in the row set', () => {
const rowA = { id: 'a', oid: 1, dependencies: [{ type: 'extension', oid: 999 }] };

computeDependLevels([rowA]);

expect(rowA.dependLevel).toBe(1);
});

it('takes the deepest of several dependency chains converging on the same row', () => {
// Both A and B depend directly on C, but A also depends on D which in
// turn depends on C, so C's level must reflect the longer A -> D -> C
// chain, not just the shorter B -> C one.
const rowA = { id: 'a', oid: 1, dependencies: [{ type: 'table', oid: 3 }, { type: 'table', oid: 4 }] };
const rowB = { id: 'b', oid: 2, dependencies: [{ type: 'table', oid: 3 }] };
const rowC = { id: 'c', oid: 3, dependencies: [] };
const rowD = { id: 'd', oid: 4, dependencies: [{ type: 'table', oid: 3 }] };

computeDependLevels([rowA, rowB, rowC, rowD]);

expect(rowC.dependLevel).toBe(rowD.dependLevel + 1);
expect(rowC.dependLevel).toBeGreaterThan(rowB.dependLevel + 1);
});
});
Loading