Skip to content

Commit 7637434

Browse files
committed
fix: audit stale is_latest rows cleared outside the batch
Signed-off-by: anilb <epipav@gmail.com>
1 parent 45c0709 commit 7637434

2 files changed

Lines changed: 42 additions & 2 deletions

File tree

services/apps/packages_worker/src/packagist/__tests__/dueSelection.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
markPackagistDailyProcessed,
1414
markPackagistMetadataScanned,
1515
} from '@crowd/data-access-layer/src/packages/packagistPackageState'
16+
import { upsertPackagistVersions } from '@crowd/data-access-layer/src/packages/versions'
1617
import type { QueryExecutor } from '@crowd/data-access-layer/src/queryExecutor'
1718

1819
// Executes the real DAL functions against a capturing stand-in executor — no database.
@@ -256,3 +257,35 @@ describe('getCriticalPackagistPackageCount', () => {
256257
expect(sql).toMatch(/is_critical/)
257258
})
258259
})
260+
261+
// The stale-is_latest cleanup only ever flips rows OUTSIDE the upsert batch (batch rows
262+
// already had is_latest set by the upsert CTE), so its row count must feed changedFields
263+
// separately or those real changes never reach the audit log.
264+
describe('upsertPackagistVersions is_latest cleanup audit', () => {
265+
const versions = [
266+
{ number: '2.0.0', publishedAt: null, isLatest: true, isPrerelease: false, licenses: null },
267+
]
268+
269+
it("appends versions.is_latest when the cleanup cleared a stale row the CTE diff can't see", async () => {
270+
qx.selectOne.mockResolvedValue({ changed_fields: [], version_ids: [] })
271+
qx.result.mockResolvedValue(1)
272+
273+
const { changedFields } = await upsertPackagistVersions(asQx(qx), '7', versions, '2.0.0')
274+
275+
const [cleanupSql] = qx.result.mock.calls[0]
276+
expect(cleanupSql).toMatch(/is_latest = false/)
277+
expect(changedFields).toContain('versions.is_latest')
278+
})
279+
280+
it('reports nothing extra when the cleanup cleared no rows, and never duplicates the CTE diff', async () => {
281+
qx.selectOne.mockResolvedValue({ changed_fields: ['versions.is_latest'], version_ids: [] })
282+
qx.result.mockResolvedValue(0)
283+
284+
const noClear = await upsertPackagistVersions(asQx(qx), '7', versions, '2.0.0')
285+
expect(noClear.changedFields).toEqual(['versions.is_latest'])
286+
287+
qx.result.mockResolvedValue(2)
288+
const cleared = await upsertPackagistVersions(asQx(qx), '7', versions, '2.0.0')
289+
expect(cleared.changedFields.filter((f) => f === 'versions.is_latest')).toHaveLength(1)
290+
})
291+
})

services/libs/data-access-layer/src/packages/versions.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,17 +155,24 @@ export async function upsertPackagistVersions(
155155
// records were updated differently can't keep is_latest = true alongside the new latest. When
156156
// no latest is known, leave flags untouched rather than wipe all. last_synced_at must move too
157157
// — it's the Tinybird ENGINE_VER, and this row's is_latest is a real change.
158+
const changedFields = [...row.changed_fields]
158159
if (latestNumber != null) {
159-
await qx.result(
160+
const cleared = await qx.result(
160161
`UPDATE versions SET is_latest = false, last_synced_at = NOW()
161162
WHERE package_id = $(packageId)::bigint
162163
AND is_latest = true
163164
AND number <> $(latestNumber)`,
164165
{ packageId, latestNumber },
165166
)
167+
// The cleanup only ever flips rows OUTSIDE this batch (batch rows already got their
168+
// is_latest set by the upsert above, so `is_latest = true` skips them) — the CTE diff
169+
// can't see these changes, so surface them for the audit log here.
170+
if (cleared > 0 && !changedFields.includes('versions.is_latest')) {
171+
changedFields.push('versions.is_latest')
172+
}
166173
}
167174

168-
return { changedFields: row.changed_fields, versionIds }
175+
return { changedFields, versionIds }
169176
}
170177

171178
export interface PypiVersionInput {

0 commit comments

Comments
 (0)