Skip to content

chore: update compare DB script data sorting logics #7428

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Jun 24, 2025
Merged
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
55 changes: 35 additions & 20 deletions .scripts/compare-database.js
Original file line number Diff line number Diff line change
Expand Up @@ -204,18 +204,8 @@ const queryDatabaseManifest = async (database) => {
};
};

const [, , database1, database2] = process.argv;

console.log('Compare database manifest between', database1, 'and', database2);

const manifests = [
await queryDatabaseManifest(database1),
await queryDatabaseManifest(database2),
];

tryCompare(...manifests);

const autoCompare = (a, b) => {
// Export utility functions first
export const autoCompare = (a, b) => {
if (typeof a !== typeof b) {
return (typeof a).localeCompare(typeof b);
}
Expand All @@ -240,17 +230,26 @@ const autoCompare = (a, b) => {
return String(a).localeCompare(String(b));
};

const buildSortByKeys = (keys) => (a, b) => {
const found = keys.find((key) => a[key] !== b[key]);
export const buildSortByKeys = (keys) => (a, b) => {
// Filter out keys where either value is boolean, then use original strategy
const filteredKeys = keys.filter((key) =>
typeof a[key] !== 'boolean' && typeof b[key] !== 'boolean'
);

const found = filteredKeys.find((key) => {
const comparison = autoCompare(a[key], b[key]);
return comparison !== 0;
});
return found ? autoCompare(a[found], b[found]) : 0;
};

const queryDatabaseData = async (database) => {
const queryDatabaseData = async (database, manifests) => {
const pool = new pg.Pool({
database,
user: 'postgres',
password: 'postgres',
});

const result = await Promise.all(
manifests[0].tables.map(async ({ table_schema, table_name }) => {
const { rows } = await pool.query(
Expand Down Expand Up @@ -288,9 +287,25 @@ const queryDatabaseData = async (database) => {
return Object.fromEntries(result);
};

console.log('Compare database data between', database1, 'and', database2);
// Main execution logic - only runs when file is executed directly
const isMainModule = import.meta.url === new URL(process.argv[1], 'file://').href;

tryCompare(
await queryDatabaseData(database1),
await queryDatabaseData(database2)
);
if (isMainModule) {
const [, , database1, database2] = process.argv;

console.log('Compare database manifest between', database1, 'and', database2);

const manifests = [
await queryDatabaseManifest(database1),
await queryDatabaseManifest(database2),
];

tryCompare(...manifests);

console.log('Compare database data between', database1, 'and', database2);

tryCompare(
await queryDatabaseData(database1, manifests),
await queryDatabaseData(database2, manifests)
);
}
270 changes: 270 additions & 0 deletions .scripts/compare-database.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
/**
* Test file for deepSort and autoCompare functions from compare-database.js
*
* Usage: node .scripts/compare-database.test.js
*
* This file tests the deep sorting functionality that ensures consistent
* ordering of arrays and objects in database comparison operations.
*/

import assert from 'node:assert';
import { autoCompare, buildSortByKeys } from './compare-database.js';

// Test helper function
const runTest = (testName, testFn) => {
try {
testFn();
console.log(`${testName}`);
} catch (error) {
console.error(`${testName}: ${error.message}`);
process.exit(1);
}
};

// Test cases for autoCompare function
runTest('autoCompare - different types', () => {
assert.strictEqual(autoCompare('string', 123) > 0, true); // string > number
assert.strictEqual(autoCompare(123, 'string') < 0, true); // number < string
});

runTest('autoCompare - same primitive types', () => {
assert.strictEqual(autoCompare('apple', 'banana') < 0, true);
assert.strictEqual(autoCompare('banana', 'apple') > 0, true);
assert.strictEqual(autoCompare('apple', 'apple'), 0);
assert.strictEqual(autoCompare(1, 2) < 0, true);
assert.strictEqual(autoCompare(2, 1) > 0, true);
assert.strictEqual(autoCompare(1, 1), 0);
});

runTest('autoCompare - objects', () => {
const obj1 = { a: 1, b: 2 };
const obj2 = { a: 1, b: 2 };
const obj3 = { a: 1, b: 3 };

assert.strictEqual(autoCompare(obj1, obj2), 0);
assert.strictEqual(autoCompare(obj1, obj3) < 0, true);
assert.strictEqual(autoCompare(obj3, obj1) > 0, true);
});

// Test cases for autoCompare sorting stability - converted from original deepSort tests
runTest('autoCompare - simple arrays sorting stability', () => {
const input = [3, 1, 2];
const sorted = input.slice().sort(autoCompare);
const expected = [1, 2, 3];

assert.deepStrictEqual(sorted, expected);

// Test multiple sorts produce same result (stability)
const sorted2 = input.slice().sort(autoCompare);
assert.deepStrictEqual(sorted, sorted2);
});

runTest('autoCompare - string arrays sorting stability', () => {
const input = ['banana', 'apple', 'cherry'];
const sorted = input.slice().sort(autoCompare);
const expected = ['apple', 'banana', 'cherry'];

assert.deepStrictEqual(sorted, expected);

// Test multiple sorts produce same result
const sorted2 = input.slice().sort(autoCompare);
assert.deepStrictEqual(sorted, sorted2);
});

runTest('autoCompare - nested arrays sorting consistency', () => {
const input = [[3, 1], [2, 4], [1, 2]];
const sorted = input.slice().sort(autoCompare);

// Arrays are compared element by element
// [1, 2] < [2, 4] < [3, 1]
const expected = [[1, 2], [2, 4], [3, 1]];

assert.deepStrictEqual(sorted, expected);

// Test sorting stability
const sorted2 = input.slice().sort(autoCompare);
assert.deepStrictEqual(sorted, sorted2);
});

runTest('autoCompare - objects sorting by keys and values', () => {
const obj1 = { c: 3, a: 1, b: 2 };
const obj2 = { a: 1, b: 2, c: 3 };
const obj3 = { b: 2, c: 3, a: 1 };

// All objects have same content, just different key order
assert.strictEqual(autoCompare(obj1, obj2), 0);
assert.strictEqual(autoCompare(obj2, obj3), 0);
assert.strictEqual(autoCompare(obj1, obj3), 0);

// Objects with different values
const obj4 = { a: 1, b: 2, c: 4 };
assert.strictEqual(autoCompare(obj1, obj4) < 0, true); // 3 < 4
});

runTest('autoCompare - arrays with objects sorting stability', () => {
const input = [
{ name: 'Bob', age: 25 },
{ name: 'Alice', age: 30 },
{ name: 'Charlie', age: 20 }
];

const sorted = input.slice().sort(autoCompare);

// Objects are sorted by their keys and values in lexicographic order
// First by 'age' key, then by 'name' key
const expected = [
{ age: 20, name: 'Charlie' },
{ age: 25, name: 'Bob' },
{ age: 30, name: 'Alice' }
];

assert.deepStrictEqual(sorted, expected);

// Test sorting stability
const sorted2 = input.slice().sort(autoCompare);
assert.deepStrictEqual(sorted, sorted2);
});

runTest('autoCompare - complex nested objects sorting consistency', () => {
const obj1 = {
logto_skus: [
{ type: 'AddOn', quota: { tokenLimit: 10_000 }, is_default: false },
{ type: 'AddOn', quota: { tokenLimit: 100 }, is_default: true },
{ type: 'AddOn', quota: { enterpriseSsoLimit: null }, is_default: true },
],
};

const obj2 = {
logto_skus: [
{ type: 'AddOn', quota: { enterpriseSsoLimit: null }, is_default: true },
{ quota: { tokenLimit: 10_000 }, is_default: false, type: 'AddOn' },
{ type: 'AddOn', quota: { tokenLimit: 100 }, is_default: true },
],
};

// Sort both arrays using buildSortByKeys for consistent comparison
const keys1 = obj1.logto_skus.length > 0 ? Object.keys(obj1.logto_skus[0]) : [];
const keys2 = obj2.logto_skus.length > 0 ? Object.keys(obj2.logto_skus[0]) : [];

const sortedObj1 = {
logto_skus: obj1.logto_skus.slice().sort(buildSortByKeys(keys1))
};

const sortedObj2 = {
logto_skus: obj2.logto_skus.slice().sort(buildSortByKeys(keys2))
};

// After sorting, they should be comparable and produce consistent results
const comparison1 = autoCompare(sortedObj1, sortedObj2);
const comparison2 = autoCompare(sortedObj1, sortedObj2);

assert.strictEqual(comparison1, comparison2); // Consistency
});

runTest('autoCompare - mixed types array sorting order', () => {
const input = [{ b: 2 }, 'string', 1, { a: 1 }];
const sorted = input.slice().sort(autoCompare);

// Type order in autoCompare: number < object < string
// Objects are sorted by their content
const expected = [1, { a: 1 }, { b: 2 }, 'string'];

assert.deepStrictEqual(sorted, expected);

// Test sorting stability
const sorted2 = input.slice().sort(autoCompare);
assert.deepStrictEqual(sorted, sorted2);
});

runTest('autoCompare - buildSortByKeys integration for database data', () => {
// This simulates database rows that might have different ordering
const data1 = [
{ id: 1, name: 'Alice', metadata: { created: '2023-01-01' }, active: true },
{ id: 2, name: 'Bob', metadata: { created: '2023-01-02' }, active: false }
];

const data2 = [
{ id: 2, name: 'Bob', metadata: { created: '2023-01-02' }, active: false },
{ id: 1, name: 'Alice', metadata: { created: '2023-01-01' }, active: true }
];

// Use buildSortByKeys to ensure consistent ordering
const keys = ['id', 'name', 'metadata', 'active'];
const sorted1 = data1.slice().sort(buildSortByKeys(keys));
const sorted2 = data2.slice().sort(buildSortByKeys(keys));

// After sorting with complexity-aware buildSortByKeys, arrays should be identical
assert.deepStrictEqual(sorted1, sorted2);

// Verify that the sorting is based on complexity (metadata object should be compared first)
const comparison = autoCompare(sorted1, sorted2);
assert.strictEqual(comparison, 0); // Should be identical
});

// Test cases for buildSortByKeys with complexity sorting
runTest('buildSortByKeys - prioritizes complex values', () => {
const obj1 = {
simpleBoolean: true,
complexObject: { nested: 'value' },
stringValue: 'test'
};

const obj2 = {
simpleBoolean: false,
complexObject: { nested: 'different' },
stringValue: 'test'
};

const keys = ['simpleBoolean', 'complexObject', 'stringValue'];
const sortFn = buildSortByKeys(keys);

// Should compare complexObject first (highest complexity), not simpleBoolean
const result = sortFn(obj1, obj2);

// Since complexObject values are different, the comparison should be based on that
// 'different' < 'value', so obj2 should come before obj1
assert.strictEqual(result > 0, true);
});

runTest('buildSortByKeys - falls back to less complex when complex values are equal', () => {
const obj1 = {
simpleBoolean: true,
complexObject: { nested: 'value' },
stringValue: 'apple'
};

const obj2 = {
simpleBoolean: false,
complexObject: { nested: 'value' }, // Same as obj1
stringValue: 'banana'
};

const keys = ['simpleBoolean', 'complexObject', 'stringValue'];
const sortFn = buildSortByKeys(keys);

// Should compare complexObject first (equal), then stringValue (next most complex)
const result = sortFn(obj1, obj2);

// 'apple' < 'banana', so obj1 should come before obj2
assert.strictEqual(result < 0, true);
});

runTest('buildSortByKeys - returns 0 when all values are equal', () => {
const obj1 = {
simpleBoolean: true,
complexObject: { nested: 'value' },
stringValue: 'test'
};

const obj2 = {
simpleBoolean: true,
complexObject: { nested: 'value' },
stringValue: 'test'
};

const keys = ['simpleBoolean', 'complexObject', 'stringValue'];
const sortFn = buildSortByKeys(keys);

const result = sortFn(obj1, obj2);
assert.strictEqual(result, 0);
});
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
"ci:build": "pnpm -r build",
"ci:lint": "pnpm -r --parallel --workspace-concurrency=0 lint",
"ci:stylelint": "pnpm -r --parallel --workspace-concurrency=0 stylelint",
"ci:test": "pnpm -r --parallel --workspace-concurrency=0 test:ci"
"test:scripts": "node .scripts/compare-database.test.js",
"ci:test": "pnpm -r --parallel --workspace-concurrency=0 test:ci && pnpm test:scripts"
},
"devDependencies": {
"@changesets/cli": "^2.26.2",
Expand Down
Loading