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:
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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})
|
||||
)`;
|
||||
}
|
||||
Reference in New Issue
Block a user