diff --git a/.scripts/compare-database.js b/.scripts/compare-database.js index a9443c1ebbd0..c81a4c4096c9 100644 --- a/.scripts/compare-database.js +++ b/.scripts/compare-database.js @@ -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); } @@ -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( @@ -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) + ); +} diff --git a/.scripts/compare-database.test.js b/.scripts/compare-database.test.js new file mode 100644 index 000000000000..8cdf3818fb62 --- /dev/null +++ b/.scripts/compare-database.test.js @@ -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); +}); diff --git a/package.json b/package.json index 8153a5b9b74f..3203f7aaffb4 100644 --- a/package.json +++ b/package.json @@ -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",