From 73631313f153203ef638bad452a9b19423237523 Mon Sep 17 00:00:00 2001 From: Vitaliy Bravikov Date: Mon, 19 Oct 2026 09:54:17 +0200 Subject: [PATCH] Sorting is performed on backend queries, and additional check is performed on NULL values. --- backend/src/db/sightings.ts | 4 +-- backend/src/schema.db.test.ts | 51 +++++++++++++++++++++++++++++++++-- backend/src/schema.test.ts | 11 ++++---- 3 files changed, 57 insertions(+), 9 deletions(-) diff --git a/backend/src/db/sightings.ts b/backend/src/db/sightings.ts index 79e71e8..12fbe78 100644 --- a/backend/src/db/sightings.ts +++ b/backend/src/db/sightings.ts @@ -41,8 +41,8 @@ const SELECT = ` const ORDER_BY = { DATE_DESC: 's.SIGHTING_DATETIME DESC, s.UFO_SIGHTING_ID DESC', DATE_ASC: 's.SIGHTING_DATETIME ASC, s.UFO_SIGHTING_ID ASC', - CITY_ASC: 'l.CITY ASC, s.UFO_SIGHTING_ID ASC', - SHAPE_ASC: 'sh.NAME ASC, s.UFO_SIGHTING_ID ASC', + CITY_ASC: "NULLIF(l.CITY, '') IS NULL, l.CITY ASC, s.UFO_SIGHTING_ID ASC", + SHAPE_ASC: "NULLIF(sh.NAME, '') IS NULL, sh.NAME ASC, s.UFO_SIGHTING_ID ASC", } // MySQL gives "2005-01-01 00:30:00"; the API uses ISO 8601 without timezone. diff --git a/backend/src/schema.db.test.ts b/backend/src/schema.db.test.ts index bbaf46a..92aff27 100644 --- a/backend/src/schema.db.test.ts +++ b/backend/src/schema.db.test.ts @@ -11,7 +11,12 @@ import { resolvers, typeDefs } from './schema.ts' // npx tsx --env-file=.env --test src/schema.db.test.ts const hasDatabase = Boolean(process.env.DB_NAME) -type Node = { id: string; datetime: string | null; shape: string | null } +type Node = { + id: string + datetime: string | null + city: string | null + shape: string | null +} type Page = { totalCount: number pageInfo: { hasNextPage: boolean; endCursor: string | null } @@ -29,7 +34,7 @@ async function sightings(variables: Record): Promise { first: $first, after: $after) { totalCount pageInfo { hasNextPage endCursor } - edges { node { id datetime shape } } + edges { node { id datetime city shape } } } }`, variables, @@ -98,6 +103,48 @@ describe('sightings against the real database', { skip: !hasDatabase }, () => { }) } + for (const [sort, field] of [ + ['CITY_ASC', 'city'], + ['SHAPE_ASC', 'shape'], + ] as const) { + it(`puts missing values last and returns every sighting once (${sort})`, async () => { + const seen = new Set() + let missingSeen = false + let after: string | null = null + let page: Page + + do { + page = await sightings({ sort, first: 100, after }) + assert.ok(page.edges.length > 0, 'the dataset should contain sightings') + if (after === null) { + assert.ok(page.edges[0].node[field], 'real names should come first') + } + + for (const { node } of page.edges) { + assert.ok(!seen.has(node.id), `duplicate sighting ${node.id}`) + seen.add(node.id) + if (node[field] === null || node[field] === '') { + missingSeen = true + } else { + assert.equal( + missingSeen, + false, + 'a real name followed a missing value', + ) + } + } + + after = page.pageInfo.endCursor + } while (page.pageInfo.hasNextPage) + + assert.equal( + seen.size, + page.totalCount, + 'every sighting should be returned', + ) + }) + } + it('sorts by date across page boundaries', async () => { const dates: string[] = [] let after: string | null = null diff --git a/backend/src/schema.test.ts b/backend/src/schema.test.ts index be8611c..1ae2214 100644 --- a/backend/src/schema.test.ts +++ b/backend/src/schema.test.ts @@ -88,10 +88,11 @@ describe('sightings search', () => { describe('sightings sort', () => { const orders = { - DATE_DESC: 's.SIGHTING_DATETIME DESC', - DATE_ASC: 's.SIGHTING_DATETIME ASC', - CITY_ASC: 'l.CITY ASC', - SHAPE_ASC: 'sh.NAME ASC', + DATE_DESC: 's.SIGHTING_DATETIME DESC, s.UFO_SIGHTING_ID DESC', + DATE_ASC: 's.SIGHTING_DATETIME ASC, s.UFO_SIGHTING_ID ASC', + CITY_ASC: "NULLIF(l.CITY, '') IS NULL, l.CITY ASC, s.UFO_SIGHTING_ID ASC", + SHAPE_ASC: + "NULLIF(sh.NAME, '') IS NULL, sh.NAME ASC, s.UFO_SIGHTING_ID ASC", } for (const [sort, clause] of Object.entries(orders)) { @@ -99,7 +100,7 @@ describe('sightings sort', () => { fakeDatabase([]) await run(SIGHTINGS, { sort }) - assert.ok(listCall().sql.includes(`ORDER BY ${clause}`)) + assert.ok(listCall().sql.includes(`ORDER BY ${clause} LIMIT ? OFFSET ?`)) }) }