From 0469ad6b13315de770ef7ca379268528386e22f3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 06:38:56 +0000 Subject: [PATCH 1/9] Initial plan From 5963d4c02c14b39a7eb0d9e1d6b601b87c4afe90 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 06:42:22 +0000 Subject: [PATCH 2/9] Add support for comma-separated and regex set name filtering Co-authored-by: geoffrey-wu <42471355+geoffrey-wu@users.noreply.github.com> --- database/qbreader/get-query.js | 29 ++++++++++++++++++++++++++++- routes/api/query.js | 4 ++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/database/qbreader/get-query.js b/database/qbreader/get-query.js index 0695d2dba..995725b89 100644 --- a/database/qbreader/get-query.js +++ b/database/qbreader/get-query.js @@ -272,7 +272,34 @@ function buildQueryAggregation ({ query, difficulties, categories, subcategories } if (setName) { - query['set.name'] = setName; + // setName is now an array after being split by commas + if (Array.isArray(setName)) { + // Check if any of the set names contain regex special characters + const hasRegex = setName.some(name => /[.*+?^${}()|[\]\\]/.test(name)); + + if (hasRegex) { + // If any set name contains regex patterns, use $or with $regex for each pattern + const setNameOr = setName.map(name => ({ + 'set.name': { $regex: name, $options: 'i' } + })); + + // Add to existing $and array if present + if (query.$and) { + query.$and.push({ $or: setNameOr }); + } else { + query.$or = setNameOr; + } + } else if (setName.length === 1) { + // Single exact match + query['set.name'] = setName[0]; + } else { + // Multiple exact matches using $in + query['set.name'] = { $in: setName }; + } + } else { + // Backward compatibility: if setName is a string (shouldn't happen after API route change) + query['set.name'] = setName; + } } if (minYear && maxYear) { diff --git a/routes/api/query.js b/routes/api/query.js index 60835396b..d2968dee3 100644 --- a/routes/api/query.js +++ b/routes/api/query.js @@ -38,6 +38,10 @@ router.get('/', async (req, res) => { req.query.subcategories = req.query.subcategories.split(','); } + if (req.query.setName) { + req.query.setName = req.query.setName.split(',').map(s => s.trim()); + } + if (!req.query.tossupPagination) { req.query.tossupPagination = 1; } From 2c65259e54c9700320c1921a6fd335317e88389f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 06:43:48 +0000 Subject: [PATCH 3/9] Update implementation to always use regex for partial matching Co-authored-by: geoffrey-wu <42471355+geoffrey-wu@users.noreply.github.com> --- database/qbreader/get-query.js | 19 ++--- test/set-filter.test.js | 131 +++++++++++++++++++++++++++++++++ 2 files changed, 138 insertions(+), 12 deletions(-) create mode 100644 test/set-filter.test.js diff --git a/database/qbreader/get-query.js b/database/qbreader/get-query.js index 995725b89..a554421b9 100644 --- a/database/qbreader/get-query.js +++ b/database/qbreader/get-query.js @@ -274,11 +274,12 @@ function buildQueryAggregation ({ query, difficulties, categories, subcategories if (setName) { // setName is now an array after being split by commas if (Array.isArray(setName)) { - // Check if any of the set names contain regex special characters - const hasRegex = setName.some(name => /[.*+?^${}()|[\]\\]/.test(name)); - - if (hasRegex) { - // If any set name contains regex patterns, use $or with $regex for each pattern + if (setName.length === 1) { + // Single set name - use regex for partial matching + query['set.name'] = { $regex: setName[0], $options: 'i' }; + } else { + // Multiple set names - use $or with $regex for each pattern + query.$or = query.$or || []; const setNameOr = setName.map(name => ({ 'set.name': { $regex: name, $options: 'i' } })); @@ -289,16 +290,10 @@ function buildQueryAggregation ({ query, difficulties, categories, subcategories } else { query.$or = setNameOr; } - } else if (setName.length === 1) { - // Single exact match - query['set.name'] = setName[0]; - } else { - // Multiple exact matches using $in - query['set.name'] = { $in: setName }; } } else { // Backward compatibility: if setName is a string (shouldn't happen after API route change) - query['set.name'] = setName; + query['set.name'] = { $regex: setName, $options: 'i' }; } } diff --git a/test/set-filter.test.js b/test/set-filter.test.js new file mode 100644 index 000000000..57feeb9ff --- /dev/null +++ b/test/set-filter.test.js @@ -0,0 +1,131 @@ +import 'dotenv/config'; + +import { mongoClient } from '../database/databases.js'; +import getQuery from '../database/qbreader/get-query.js'; + +import { assert } from 'chai'; +import mocha from 'mocha'; + +mocha.describe('Set Name Filtering', function () { + this.timeout(0); + + mocha.before(async () => { + await mongoClient.connect(); + }); + + mocha.after(async () => { + await mongoClient.close(); + }); + + mocha.it('should filter by single set name', async () => { + const result = await getQuery({ + questionType: 'all', + verbose: false, + setName: ['2023 ACF Regionals'] + }); + assert.isOk(result.tossups, 'tossups'); + assert.isOk(result.bonuses, 'bonuses'); + assert.isAbove(result.tossups.count, 0, 'should have tossups'); + // Verify all results match the pattern (case-insensitive) + if (result.tossups.questionArray.length > 0) { + const setNames = result.tossups.questionArray.map(q => q.set.name); + assert.isTrue(setNames.every(name => /2023 ACF Regionals/i.test(name))); + } + }); + + mocha.it('should filter by multiple comma-separated set names', async () => { + const result = await getQuery({ + questionType: 'all', + verbose: false, + setName: ['2023 ACF Regionals', '2023 ACF Fall'] + }); + assert.isOk(result.tossups, 'tossups'); + assert.isOk(result.bonuses, 'bonuses'); + assert.isAbove(result.tossups.count, 0, 'should have tossups'); + // Verify results match one of the patterns + if (result.tossups.questionArray.length > 0) { + const setNames = result.tossups.questionArray.map(q => q.set.name); + assert.isTrue(setNames.every(name => + /2023 ACF Regionals/i.test(name) || /2023 ACF Fall/i.test(name) + )); + } + }); + + mocha.it('should filter by regex pattern', async () => { + const result = await getQuery({ + questionType: 'all', + verbose: false, + setName: ['2023 ACF (Fall|Regionals)'] + }); + assert.isOk(result.tossups, 'tossups'); + assert.isOk(result.bonuses, 'bonuses'); + assert.isAbove(result.tossups.count, 0, 'should have tossups'); + // Verify results are from ACF Fall or Regionals + if (result.tossups.questionArray.length > 0) { + const setNames = result.tossups.questionArray.map(q => q.set.name); + assert.isTrue(setNames.every(name => + name.includes('2023 ACF Fall') || name.includes('2023 ACF Regionals') + )); + } + }); + + mocha.it('should filter by regex pattern with character class', async () => { + const result = await getQuery({ + questionType: 'all', + verbose: false, + setName: ['ACF [FWR]'] + }); + assert.isOk(result.tossups, 'tossups'); + assert.isOk(result.bonuses, 'bonuses'); + assert.isAbove(result.tossups.count, 0, 'should have tossups'); + // Verify results match the pattern + if (result.tossups.questionArray.length > 0) { + const setNames = result.tossups.questionArray.map(q => q.set.name); + // Should match ACF Fall, ACF Winter, or ACF Regionals + assert.isTrue(setNames.every(name => /ACF [FWR]/.test(name))); + } + }); + + mocha.it('should filter by partial set name', async () => { + const result = await getQuery({ + questionType: 'all', + verbose: false, + setName: ['ACF Fall'] + }); + assert.isOk(result.tossups, 'tossups'); + assert.isOk(result.bonuses, 'bonuses'); + assert.isAbove(result.tossups.count, 0, 'should have tossups'); + // Verify all results contain "ACF Fall" (case-insensitive) + if (result.tossups.questionArray.length > 0) { + const setNames = result.tossups.questionArray.map(q => q.set.name); + assert.isTrue(setNames.every(name => /ACF Fall/i.test(name))); + } + }); + + mocha.it('should combine regex patterns with commas', async () => { + const result = await getQuery({ + questionType: 'all', + verbose: false, + setName: ['2023 ACF [FR]', '2024 PACE NSC'] + }); + assert.isOk(result.tossups, 'tossups'); + assert.isOk(result.bonuses, 'bonuses'); + assert.isAbove(result.tossups.count, 0, 'should have tossups'); + }); + + mocha.it('should work with search query and set filter', async () => { + const result = await getQuery({ + queryString: 'newton', + questionType: 'all', + verbose: false, + setName: ['2018 PACE NSC'] + }); + assert.isOk(result.tossups, 'tossups'); + assert.isOk(result.bonuses, 'bonuses'); + // Should have results from 2018 PACE NSC that mention "newton" + if (result.tossups.questionArray.length > 0) { + const setNames = result.tossups.questionArray.map(q => q.set.name); + assert.isTrue(setNames.every(name => /2018 PACE NSC/i.test(name))); + } + }); +}); From 33bbac2d6dceb3074e4c34d1ad87d13f997041ec Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 06:45:59 +0000 Subject: [PATCH 4/9] Fix query logic to properly handle multiple set names Co-authored-by: geoffrey-wu <42471355+geoffrey-wu@users.noreply.github.com> --- database/qbreader/get-query.js | 2 +- test/set-filter.test.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/database/qbreader/get-query.js b/database/qbreader/get-query.js index a554421b9..7aca4c12c 100644 --- a/database/qbreader/get-query.js +++ b/database/qbreader/get-query.js @@ -279,7 +279,6 @@ function buildQueryAggregation ({ query, difficulties, categories, subcategories query['set.name'] = { $regex: setName[0], $options: 'i' }; } else { // Multiple set names - use $or with $regex for each pattern - query.$or = query.$or || []; const setNameOr = setName.map(name => ({ 'set.name': { $regex: name, $options: 'i' } })); @@ -288,6 +287,7 @@ function buildQueryAggregation ({ query, difficulties, categories, subcategories if (query.$and) { query.$and.push({ $or: setNameOr }); } else { + // If there's no existing $and, we can directly set $or query.$or = setNameOr; } } diff --git a/test/set-filter.test.js b/test/set-filter.test.js index 57feeb9ff..304a3d83f 100644 --- a/test/set-filter.test.js +++ b/test/set-filter.test.js @@ -45,7 +45,7 @@ mocha.describe('Set Name Filtering', function () { // Verify results match one of the patterns if (result.tossups.questionArray.length > 0) { const setNames = result.tossups.questionArray.map(q => q.set.name); - assert.isTrue(setNames.every(name => + assert.isTrue(setNames.every(name => /2023 ACF Regionals/i.test(name) || /2023 ACF Fall/i.test(name) )); } From ceaa23b5c8c9c9697fe2d54bc9eaa71299338ef5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 06:47:08 +0000 Subject: [PATCH 5/9] Use $and array for better query composition with multiple set names Co-authored-by: geoffrey-wu <42471355+geoffrey-wu@users.noreply.github.com> --- database/qbreader/get-query.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/database/qbreader/get-query.js b/database/qbreader/get-query.js index 7aca4c12c..5bc661e65 100644 --- a/database/qbreader/get-query.js +++ b/database/qbreader/get-query.js @@ -283,12 +283,12 @@ function buildQueryAggregation ({ query, difficulties, categories, subcategories 'set.name': { $regex: name, $options: 'i' } })); - // Add to existing $and array if present + // Always add to $and array to properly combine with other conditions if (query.$and) { query.$and.push({ $or: setNameOr }); } else { - // If there's no existing $and, we can directly set $or - query.$or = setNameOr; + // Create $and array with the $or condition + query.$and = [{ $or: setNameOr }]; } } } else { From 86b13a18b84130d08909e01bf976e88229268ec2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 06:49:14 +0000 Subject: [PATCH 6/9] Add clarifying comments and improve test assertions Co-authored-by: geoffrey-wu <42471355+geoffrey-wu@users.noreply.github.com> --- database/qbreader/get-query.js | 2 ++ test/set-filter.test.js | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/database/qbreader/get-query.js b/database/qbreader/get-query.js index 5bc661e65..400845ef0 100644 --- a/database/qbreader/get-query.js +++ b/database/qbreader/get-query.js @@ -288,6 +288,8 @@ function buildQueryAggregation ({ query, difficulties, categories, subcategories query.$and.push({ $or: setNameOr }); } else { // Create $and array with the $or condition + // Note: Other conditions (difficulty, category, etc.) are direct properties + // and will be ANDed with this $and at the top level by MongoDB query.$and = [{ $or: setNameOr }]; } } diff --git a/test/set-filter.test.js b/test/set-filter.test.js index 304a3d83f..4af67b39a 100644 --- a/test/set-filter.test.js +++ b/test/set-filter.test.js @@ -111,6 +111,13 @@ mocha.describe('Set Name Filtering', function () { assert.isOk(result.tossups, 'tossups'); assert.isOk(result.bonuses, 'bonuses'); assert.isAbove(result.tossups.count, 0, 'should have tossups'); + // Verify results match one of the patterns + if (result.tossups.questionArray.length > 0) { + const setNames = result.tossups.questionArray.map(q => q.set.name); + assert.isTrue(setNames.every(name => + (/2023 ACF [FR]/i.test(name) || /2024 PACE NSC/i.test(name)) + ), 'All results should match one of the specified patterns'); + } }); mocha.it('should work with search query and set filter', async () => { From e4592694264ec3feeabbbed99477a6e5b03fc46b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 06:49:56 +0000 Subject: [PATCH 7/9] Update set name input placeholder to indicate new features Co-authored-by: geoffrey-wu <42471355+geoffrey-wu@users.noreply.github.com> --- client/db/index.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/db/index.jsx b/client/db/index.jsx index 107e3c2ef..7a0e6a63d 100644 --- a/client/db/index.jsx +++ b/client/db/index.jsx @@ -305,7 +305,7 @@ function QueryForm () { { setMaxReturnLength(event.target.value); }} />
- { setSetName(event.target.value); }} /> + { setSetName(event.target.value); }} />
From 527b2be4342b8221ed476e9c62331c161021916b Mon Sep 17 00:00:00 2001 From: Geoffrey Wu Date: Sun, 1 Mar 2026 23:02:49 +0000 Subject: [PATCH 8/9] Delete set-filter.test.js --- test/set-filter.test.js | 138 ---------------------------------------- 1 file changed, 138 deletions(-) delete mode 100644 test/set-filter.test.js diff --git a/test/set-filter.test.js b/test/set-filter.test.js deleted file mode 100644 index 4af67b39a..000000000 --- a/test/set-filter.test.js +++ /dev/null @@ -1,138 +0,0 @@ -import 'dotenv/config'; - -import { mongoClient } from '../database/databases.js'; -import getQuery from '../database/qbreader/get-query.js'; - -import { assert } from 'chai'; -import mocha from 'mocha'; - -mocha.describe('Set Name Filtering', function () { - this.timeout(0); - - mocha.before(async () => { - await mongoClient.connect(); - }); - - mocha.after(async () => { - await mongoClient.close(); - }); - - mocha.it('should filter by single set name', async () => { - const result = await getQuery({ - questionType: 'all', - verbose: false, - setName: ['2023 ACF Regionals'] - }); - assert.isOk(result.tossups, 'tossups'); - assert.isOk(result.bonuses, 'bonuses'); - assert.isAbove(result.tossups.count, 0, 'should have tossups'); - // Verify all results match the pattern (case-insensitive) - if (result.tossups.questionArray.length > 0) { - const setNames = result.tossups.questionArray.map(q => q.set.name); - assert.isTrue(setNames.every(name => /2023 ACF Regionals/i.test(name))); - } - }); - - mocha.it('should filter by multiple comma-separated set names', async () => { - const result = await getQuery({ - questionType: 'all', - verbose: false, - setName: ['2023 ACF Regionals', '2023 ACF Fall'] - }); - assert.isOk(result.tossups, 'tossups'); - assert.isOk(result.bonuses, 'bonuses'); - assert.isAbove(result.tossups.count, 0, 'should have tossups'); - // Verify results match one of the patterns - if (result.tossups.questionArray.length > 0) { - const setNames = result.tossups.questionArray.map(q => q.set.name); - assert.isTrue(setNames.every(name => - /2023 ACF Regionals/i.test(name) || /2023 ACF Fall/i.test(name) - )); - } - }); - - mocha.it('should filter by regex pattern', async () => { - const result = await getQuery({ - questionType: 'all', - verbose: false, - setName: ['2023 ACF (Fall|Regionals)'] - }); - assert.isOk(result.tossups, 'tossups'); - assert.isOk(result.bonuses, 'bonuses'); - assert.isAbove(result.tossups.count, 0, 'should have tossups'); - // Verify results are from ACF Fall or Regionals - if (result.tossups.questionArray.length > 0) { - const setNames = result.tossups.questionArray.map(q => q.set.name); - assert.isTrue(setNames.every(name => - name.includes('2023 ACF Fall') || name.includes('2023 ACF Regionals') - )); - } - }); - - mocha.it('should filter by regex pattern with character class', async () => { - const result = await getQuery({ - questionType: 'all', - verbose: false, - setName: ['ACF [FWR]'] - }); - assert.isOk(result.tossups, 'tossups'); - assert.isOk(result.bonuses, 'bonuses'); - assert.isAbove(result.tossups.count, 0, 'should have tossups'); - // Verify results match the pattern - if (result.tossups.questionArray.length > 0) { - const setNames = result.tossups.questionArray.map(q => q.set.name); - // Should match ACF Fall, ACF Winter, or ACF Regionals - assert.isTrue(setNames.every(name => /ACF [FWR]/.test(name))); - } - }); - - mocha.it('should filter by partial set name', async () => { - const result = await getQuery({ - questionType: 'all', - verbose: false, - setName: ['ACF Fall'] - }); - assert.isOk(result.tossups, 'tossups'); - assert.isOk(result.bonuses, 'bonuses'); - assert.isAbove(result.tossups.count, 0, 'should have tossups'); - // Verify all results contain "ACF Fall" (case-insensitive) - if (result.tossups.questionArray.length > 0) { - const setNames = result.tossups.questionArray.map(q => q.set.name); - assert.isTrue(setNames.every(name => /ACF Fall/i.test(name))); - } - }); - - mocha.it('should combine regex patterns with commas', async () => { - const result = await getQuery({ - questionType: 'all', - verbose: false, - setName: ['2023 ACF [FR]', '2024 PACE NSC'] - }); - assert.isOk(result.tossups, 'tossups'); - assert.isOk(result.bonuses, 'bonuses'); - assert.isAbove(result.tossups.count, 0, 'should have tossups'); - // Verify results match one of the patterns - if (result.tossups.questionArray.length > 0) { - const setNames = result.tossups.questionArray.map(q => q.set.name); - assert.isTrue(setNames.every(name => - (/2023 ACF [FR]/i.test(name) || /2024 PACE NSC/i.test(name)) - ), 'All results should match one of the specified patterns'); - } - }); - - mocha.it('should work with search query and set filter', async () => { - const result = await getQuery({ - queryString: 'newton', - questionType: 'all', - verbose: false, - setName: ['2018 PACE NSC'] - }); - assert.isOk(result.tossups, 'tossups'); - assert.isOk(result.bonuses, 'bonuses'); - // Should have results from 2018 PACE NSC that mention "newton" - if (result.tossups.questionArray.length > 0) { - const setNames = result.tossups.questionArray.map(q => q.set.name); - assert.isTrue(setNames.every(name => /2018 PACE NSC/i.test(name))); - } - }); -}); From 709f2af306462c720f188e597823a26c09869221 Mon Sep 17 00:00:00 2001 From: Geoffrey Wu Date: Sun, 1 Mar 2026 23:27:21 +0000 Subject: [PATCH 9/9] simplify logic --- database/qbreader/get-query.js | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/database/qbreader/get-query.js b/database/qbreader/get-query.js index 400845ef0..16fdf4496 100644 --- a/database/qbreader/get-query.js +++ b/database/qbreader/get-query.js @@ -274,25 +274,7 @@ function buildQueryAggregation ({ query, difficulties, categories, subcategories if (setName) { // setName is now an array after being split by commas if (Array.isArray(setName)) { - if (setName.length === 1) { - // Single set name - use regex for partial matching - query['set.name'] = { $regex: setName[0], $options: 'i' }; - } else { - // Multiple set names - use $or with $regex for each pattern - const setNameOr = setName.map(name => ({ - 'set.name': { $regex: name, $options: 'i' } - })); - - // Always add to $and array to properly combine with other conditions - if (query.$and) { - query.$and.push({ $or: setNameOr }); - } else { - // Create $and array with the $or condition - // Note: Other conditions (difficulty, category, etc.) are direct properties - // and will be ANDed with this $and at the top level by MongoDB - query.$and = [{ $or: setNameOr }]; - } - } + query['set.name'] = { $in: setName.map(name => new RegExp(name, 'i')) }; } else { // Backward compatibility: if setName is a string (shouldn't happen after API route change) query['set.name'] = { $regex: setName, $options: 'i' };