Fix match-all people/tags filters in gallery search API.

Raw SQL uses GROUP BY + HAVING for peopleMode/tagsMode=all; add unit tests.
This commit is contained in:
2026-08-04 21:33:49 -04:00
parent 5b00f3a3ab
commit d0b9a0c40d
3 changed files with 48 additions and 11 deletions
+5 -11
View File
@@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from 'next/server';
import { prisma, prismaAuth } from '@/lib/db';
import { serializePhotos } from '@/lib/serialize';
import { auth } from '@/app/api/auth/[...nextauth]/route';
import { photoIdsMatchingAll } from '@/lib/search-filter-sql';
export async function GET(request: NextRequest) {
try {
@@ -184,20 +185,13 @@ export async function GET(request: NextRequest) {
}
}
// Handle people filter - embed IDs directly since they're safe integers
// People / tags filters — honour any vs all (raw SQL path must match Prisma where above)
if (people.length > 0) {
const peopleIds = people.join(',');
whereConditions.push(`id IN (
SELECT DISTINCT photo_id FROM faces WHERE person_id IN (${peopleIds})
)`);
whereConditions.push(photoIdsMatchingAll('faces', 'person_id', people, peopleMode));
}
// Handle tags filter - embed IDs directly since they're safe integers
if (tags.length > 0) {
const tagIds = tags.join(',');
whereConditions.push(`id IN (
SELECT DISTINCT photo_id FROM phototaglinkage WHERE tag_id IN (${tagIds})
)`);
whereConditions.push(photoIdsMatchingAll('phototaglinkage', 'tag_id', tags, tagsMode));
}
// Handle favorites filter - embed IDs directly since they're safe integers
@@ -0,0 +1,23 @@
import { describe, expect, it } from 'vitest';
import { photoIdsMatchingAll } from './search-filter-sql';
describe('photoIdsMatchingAll', () => {
it('uses DISTINCT for any mode', () => {
const sql = photoIdsMatchingAll('faces', 'person_id', [1, 2], 'any');
expect(sql).toContain('DISTINCT photo_id');
expect(sql).toContain('person_id IN (1,2)');
expect(sql).not.toContain('GROUP BY');
});
it('uses HAVING COUNT for all mode with multiple ids', () => {
const sql = photoIdsMatchingAll('faces', 'person_id', [1, 2, 3], 'all');
expect(sql).toContain('GROUP BY photo_id');
expect(sql).toContain('HAVING COUNT(DISTINCT person_id) = 3');
});
it('falls back to DISTINCT when all mode has single id', () => {
const sql = photoIdsMatchingAll('phototaglinkage', 'tag_id', [5], 'all');
expect(sql).toContain('DISTINCT photo_id');
expect(sql).not.toContain('GROUP BY');
});
});
+20
View File
@@ -0,0 +1,20 @@
/** Build a photo_id subquery for people/tags filters (any = OR, all = AND). */
export function photoIdsMatchingAll(
table: 'faces' | 'phototaglinkage',
column: 'person_id' | 'tag_id',
ids: number[],
mode: 'any' | 'all'
): string {
const idList = ids.join(',');
if (mode === 'all' && ids.length > 1) {
return `id IN (
SELECT photo_id FROM ${table}
WHERE ${column} IN (${idList})
GROUP BY photo_id
HAVING COUNT(DISTINCT ${column}) = ${ids.length}
)`;
}
return `id IN (
SELECT DISTINCT photo_id FROM ${table} WHERE ${column} IN (${idList})
)`;
}