Skip to content
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
Binary file modified src/lib/validator.ts
Binary file not shown.
28 changes: 26 additions & 2 deletions tests/levels.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import initSqlJs, { type Database } from 'sql.js';
import { levels } from '@/data/levels';
import { seedDatabase } from '@/lib/seed';
import { runReadOnlyQuery, assertReadOnlyQuery } from '@/lib/sqlRunner';
import { compareResults, solutionRequiresOrder } from '@/lib/validator';
import { compareResults, extractOrderKeyIndices, solutionRequiresOrder } from '@/lib/validator';
import type { Epoch, QueryResult } from '@/types';

let db: Database;
Expand Down Expand Up @@ -52,6 +52,30 @@ describe.each(levels)('level $id — $title', (level) => {
return; // seed scaffold doesn't even parse — that's fine
}
const orderMatters = level.orderMatters ?? solutionRequiresOrder(level.solutionQuery);
expect(compareResults(seedResult, expected, orderMatters)).not.toBeNull();
const orderKeys = orderMatters
? extractOrderKeyIndices(level.solutionQuery, expected.columns)
: null;
expect(compareResults(seedResult, expected, orderMatters, orderKeys)).not.toBeNull();
});
});

describe('order-only ties accept any tie order (level 20 regression)', () => {
it('accepts a correct query whose tied rows come out in a different order', () => {
const level = levels.find((l) => l.id === 20)!;
const userQuery = `SELECT
customer_name,
count(distinct account_id) as account_count
FROM customers c
JOIN accounts a ON c.customer_id = a.customer_id
GROUP BY customer_name
HAVING account_count > 1
ORDER BY account_count desc`;
const expected = runReadOnlyQuery(db, level.solutionQuery);
const user = runReadOnlyQuery(db, userQuery);
const orderMatters = level.orderMatters ?? solutionRequiresOrder(level.solutionQuery);
const orderKeys = extractOrderKeyIndices(level.solutionQuery, expected.columns);
expect(orderMatters).toBe(true);
expect(orderKeys).toEqual([1]);
expect(compareResults(user, expected, orderMatters, orderKeys)).toBeNull();
});
});
60 changes: 59 additions & 1 deletion tests/validator.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { describe, expect, it } from 'vitest';
import { compareResults, solutionRequiresOrder, valuesEqual } from '@/lib/validator';
import {
compareResults,
extractOrderKeyIndices,
solutionRequiresOrder,
valuesEqual,
} from '@/lib/validator';
import type { QueryResult } from '@/types';

const result = (columns: string[], values: unknown[][]): QueryResult => ({ columns, values });
Expand Down Expand Up @@ -75,6 +80,59 @@ describe('compareResults', () => {
});
});

describe('compareResults with ORDER BY tie groups', () => {
// ORDER BY cnt DESC: 'Ann'/'Bob' tie on cnt, 'Cy' does not.
const expected = result(['name', 'cnt'], [['Bob', 2], ['Ann', 2], ['Cy', 1]]);

it('accepts any order among rows tied on the ORDER BY keys', () => {
const user = result(['name', 'cnt'], [['Ann', 2], ['Bob', 2], ['Cy', 1]]);
expect(compareResults(user, expected, true)).not.toBeNull();
expect(compareResults(user, expected, true, [1])).toBeNull();
});

it('rejects rows placed outside their tie group (wrong ORDER BY)', () => {
const user = result(['name', 'cnt'], [['Cy', 1], ['Ann', 2], ['Bob', 2]]);
expect(compareResults(user, expected, true, [1])).toContain('ORDER BY');
});

it('rejects wrong values within a tie group', () => {
const user = result(['name', 'cnt'], [['Bob', 2], ['Zed', 2], ['Cy', 1]]);
expect(compareResults(user, expected, true, [1])).not.toBeNull();
});
});

describe('extractOrderKeyIndices', () => {
const columns = ['customer_name', 'account_count'];

it('maps plain and qualified terms (with ASC/DESC) to column indices', () => {
expect(
extractOrderKeyIndices('SELECT ... ORDER BY account_count DESC', columns)
).toEqual([1]);
expect(
extractOrderKeyIndices('SELECT ... ORDER BY c.customer_name, account_count ASC', columns)
).toEqual([0, 1]);
});

it('supports positional terms and ignores a trailing LIMIT', () => {
expect(extractOrderKeyIndices('SELECT ... ORDER BY 2 DESC LIMIT 5', columns)).toEqual([1]);
});

it('ignores ORDER BY inside parentheses', () => {
expect(
extractOrderKeyIndices(
'SELECT RANK() OVER (ORDER BY balance) AS r FROM t ORDER BY account_count',
columns
)
).toEqual([1]);
});

it('returns null when a term cannot be resolved to a column', () => {
expect(extractOrderKeyIndices('SELECT ... ORDER BY COUNT(*) DESC', columns)).toBeNull();
expect(extractOrderKeyIndices('SELECT ... ORDER BY 3', columns)).toBeNull();
expect(extractOrderKeyIndices('SELECT a FROM t', columns)).toBeNull();
});
});

describe('solutionRequiresOrder', () => {
it('detects a top-level ORDER BY', () => {
expect(solutionRequiresOrder('SELECT a FROM t ORDER BY a')).toBe(true);
Expand Down
Loading