55 eq ,
66 getTableColumns ,
77 inArray ,
8+ InferSelectModel ,
89 isNull ,
910 or ,
1011 sql ,
@@ -86,11 +87,17 @@ type Params = {
8687 difficulty ?: string ;
8788} ;
8889
90+ type IGetRandomQuestionResponse = InferSelectModel < typeof QUESTIONS_TABLE > & {
91+ attemptCount : number ;
92+ } ;
93+
8994// Fetch an unattempted question or fallback to the least attempted one
90- export const getRandomQuestion = async ( { userId1, userId2, topics, difficulty } : Params ) => {
91- /**
92- * 1. Both Unattempted
93- */
95+ export const getRandomQuestion = async ( {
96+ userId1,
97+ userId2,
98+ topics,
99+ difficulty,
100+ } : Params ) : Promise < IGetRandomQuestionResponse | null > => {
94101 // If an attempt contains either user's ID
95102 const ids = [ userId1 , userId2 ] ;
96103 const userIdClause = [
@@ -103,35 +110,61 @@ export const getRandomQuestion = async ({ userId1, userId2, topics, difficulty }
103110 or ( ...userIdClause ) ,
104111 ] ;
105112
106- // Build the filter clause
107- // - attempt ID null: No attempts
108- // - topics: If specified, must intersect using Array Intersect
109- const filterClause = [ ] ;
113+ // Try different filter combinations in order of specificity
114+ const filterCombinations = [
115+ // Exact match
116+ topics && difficulty
117+ ? [ arrayOverlaps ( QUESTIONS_TABLE . topic , topics ) , eq ( QUESTIONS_TABLE . difficulty , difficulty ) ]
118+ : // Topic only
119+ topics
120+ ? [ arrayOverlaps ( QUESTIONS_TABLE . topic , topics ) ]
121+ : // Difficulty only
122+ difficulty
123+ ? [ eq ( QUESTIONS_TABLE . difficulty , difficulty ) ]
124+ : // No filters
125+ [ ] ,
126+ ] ;
110127
111- if ( topics ) {
112- filterClause . push ( arrayOverlaps ( QUESTIONS_TABLE . topic , topics ) ) ;
128+ // Additional combinations if both topic and difficulty are provided
129+ if ( topics && difficulty ) {
130+ filterCombinations . push (
131+ // Topic only
132+ [ arrayOverlaps ( QUESTIONS_TABLE . topic , topics ) ] ,
133+ // Difficulty only
134+ [ eq ( QUESTIONS_TABLE . difficulty , difficulty ) ] ,
135+ // No filters
136+ [ ]
137+ ) ;
113138 }
114139
115- if ( difficulty ) {
116- filterClause . push ( eq ( QUESTIONS_TABLE . difficulty , difficulty ) ) ;
117- }
140+ for ( const filterClause of filterCombinations ) {
141+ // Check if AT LEAST 1 question exists with current filters
142+ const questionCounts = await db
143+ . select ( { id : QUESTIONS_TABLE . id } )
144+ . from ( QUESTIONS_TABLE )
145+ . where ( and ( ...filterClause ) )
146+ . limit ( 1 ) ;
118147
119- const bothUnattempted = await db
120- . select ( { question : QUESTIONS_TABLE } )
121- . from ( QUESTIONS_TABLE )
122- . leftJoin ( QUESTION_ATTEMPTS_TABLE , and ( ...joinClause ) )
123- . where ( and ( isNull ( QUESTION_ATTEMPTS_TABLE . attemptId ) , ...filterClause ) )
124- . orderBy ( sql `RANDOM()` )
125- . limit ( 1 ) ;
148+ // No questions exist with the filter.
149+ if ( ! questionCounts || ! questionCounts . length ) {
150+ continue ;
151+ }
126152
127- if ( bothUnattempted && bothUnattempted . length > 0 ) {
128- return bothUnattempted [ 0 ] . question ;
129- }
153+ // Try to find an unattempted question with current filters
154+ const bothUnattempted = await db
155+ . select ( { question : QUESTIONS_TABLE } )
156+ . from ( QUESTIONS_TABLE )
157+ . leftJoin ( QUESTION_ATTEMPTS_TABLE , and ( ...joinClause ) )
158+ . where ( and ( isNull ( QUESTION_ATTEMPTS_TABLE . attemptId ) , ...filterClause ) )
159+ . orderBy ( sql `RANDOM()` )
160+ . limit ( 1 ) ;
130161
131- // 2. At least one user has attempted.
132- // - Fetch all questions, summing attempts by both users, ranking and selecting the lowest count.
133- const attempts = db . $with ( 'at' ) . as (
134- db
162+ if ( bothUnattempted && bothUnattempted . length > 0 ) {
163+ return { ...bothUnattempted [ 0 ] . question , attemptCount : 0 } ;
164+ }
165+
166+ // If no unattempted question, try least attempted
167+ let nestedQuery = db
135168 . select ( {
136169 ...getTableColumns ( QUESTIONS_TABLE ) ,
137170 user1Count :
@@ -145,20 +178,31 @@ export const getRandomQuestion = async ({ userId1, userId2, topics, difficulty }
145178 } )
146179 . from ( QUESTIONS_TABLE )
147180 . innerJoin ( QUESTION_ATTEMPTS_TABLE , and ( ...joinClause ) )
148- . where ( and ( ...filterClause ) )
149- . groupBy ( QUESTIONS_TABLE . id )
150- ) ;
151- const result = await db
152- . with ( attempts )
153- . select ( )
154- . from ( attempts )
155- . orderBy ( asc ( sql `COALESCE(user1_attempts,0) + COALESCE(user2_attempts,0)` ) )
156- . limit ( 1 ) ;
157-
158- if ( result && result . length > 0 ) {
159- return { ...result [ 0 ] , user1Count : undefined , user2Count : undefined } ;
181+ . $dynamic ( ) ;
182+
183+ if ( filterClause . length ) {
184+ nestedQuery = nestedQuery . where ( and ( ...filterClause ) ) ;
185+ }
186+
187+ nestedQuery = nestedQuery . groupBy ( QUESTIONS_TABLE . id ) ;
188+
189+ const attempts = db . $with ( 'at' ) . as ( nestedQuery ) ;
190+
191+ const result = await db
192+ . with ( attempts )
193+ . select ( )
194+ . from ( attempts )
195+ . orderBy ( asc ( sql `COALESCE(user1_attempts,0) + COALESCE(user2_attempts,0)` ) )
196+ . limit ( 1 ) ;
197+
198+ if ( result && result . length > 0 ) {
199+ const { user1Count, user2Count, ...details } = result [ 0 ] ;
200+ const attemptCount =
201+ ( user1Count ? ( user1Count as number ) : 0 ) + ( user2Count ? ( user2Count as number ) : 0 ) ;
202+ return { ...details , attemptCount } ;
203+ }
160204 }
161205
162- // This branch should not be reached
163- logger . info ( 'Unreachable Branch - If first query fails, second query must return something' ) ;
206+ logger . error ( 'No questions found with any filter combination' ) ;
207+ return null ;
164208} ;
0 commit comments