@@ -88,9 +88,6 @@ type Params = {
88
88
89
89
// Fetch an unattempted question or fallback to the least attempted one
90
90
export const getRandomQuestion = async ( { userId1, userId2, topics, difficulty } : Params ) => {
91
- /**
92
- * 1. Both Unattempted
93
- */
94
91
// If an attempt contains either user's ID
95
92
const ids = [ userId1 , userId2 ] ;
96
93
const userIdClause = [
@@ -103,62 +100,90 @@ export const getRandomQuestion = async ({ userId1, userId2, topics, difficulty }
103
100
or ( ...userIdClause ) ,
104
101
] ;
105
102
106
- // Build the filter clause
107
- // - attempt ID null: No attempts
108
- // - topics: If specified, must intersect using Array Intersect
109
- const filterClause = [ ] ;
110
-
111
- if ( topics ) {
112
- filterClause . push ( arrayOverlaps ( QUESTIONS_TABLE . topic , topics ) ) ;
113
- }
103
+ // Try different filter combinations in order of specificity
104
+ const filterCombinations = [
105
+ // Exact match
106
+ topics && difficulty
107
+ ? [ arrayOverlaps ( QUESTIONS_TABLE . topic , topics ) , eq ( QUESTIONS_TABLE . difficulty , difficulty ) ]
108
+ : // Topic only
109
+ topics
110
+ ? [ arrayOverlaps ( QUESTIONS_TABLE . topic , topics ) ]
111
+ : // Difficulty only
112
+ difficulty
113
+ ? [ eq ( QUESTIONS_TABLE . difficulty , difficulty ) ]
114
+ : // No filters
115
+ [ ] ,
116
+ ] ;
114
117
115
- if ( difficulty ) {
116
- filterClause . push ( eq ( QUESTIONS_TABLE . difficulty , difficulty ) ) ;
118
+ // Additional combinations if both topic and difficulty are provided
119
+ if ( topics && difficulty ) {
120
+ filterCombinations . push (
121
+ // Topic only
122
+ [ arrayOverlaps ( QUESTIONS_TABLE . topic , topics ) ] ,
123
+ // Difficulty only
124
+ [ eq ( QUESTIONS_TABLE . difficulty , difficulty ) ] ,
125
+ // No filters
126
+ [ ]
127
+ ) ;
117
128
}
118
129
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 ) ;
130
+ for ( const filterClause of filterCombinations ) {
131
+ // Check if questions exist with current filters
132
+ const questionExists = await db
133
+ . select ( { count : sql < number > `count(*)` } )
134
+ . from ( QUESTIONS_TABLE )
135
+ . where ( and ( ... filterClause ) )
136
+ . then ( ( result ) => Number ( result [ 0 ] . count ) > 0 ) ;
126
137
127
- if ( bothUnattempted && bothUnattempted . length > 0 ) {
128
- return bothUnattempted [ 0 ] . question ;
129
- }
138
+ if ( ! questionExists ) {
139
+ continue ;
140
+ }
130
141
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
135
- . select ( {
136
- ...getTableColumns ( QUESTIONS_TABLE ) ,
137
- user1Count :
138
- sql `SUM(CASE WHEN ${ QUESTION_ATTEMPTS_TABLE . userId1 } = ${ userId1 } ::uuid OR ${ QUESTION_ATTEMPTS_TABLE . userId2 } = ${ userId1 } ::uuid THEN 1 END)` . as (
139
- 'user1_attempts'
140
- ) ,
141
- user2Count :
142
- sql `SUM(CASE WHEN ${ QUESTION_ATTEMPTS_TABLE . userId1 } = ${ userId2 } ::uuid OR ${ QUESTION_ATTEMPTS_TABLE . userId2 } = ${ userId2 } ::uuid THEN 1 END)` . as (
143
- 'user2_attempts'
144
- ) ,
145
- } )
142
+ // Try to find an unattempted question with current filters
143
+ const bothUnattempted = await db
144
+ . select ( { question : QUESTIONS_TABLE } )
146
145
. from ( QUESTIONS_TABLE )
147
- . 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 ) ;
146
+ . leftJoin ( QUESTION_ATTEMPTS_TABLE , and ( ...joinClause ) )
147
+ . where ( and ( isNull ( QUESTION_ATTEMPTS_TABLE . attemptId ) , ...filterClause ) )
148
+ . orderBy ( sql `RANDOM()` )
149
+ . limit ( 1 ) ;
150
+
151
+ if ( bothUnattempted && bothUnattempted . length > 0 ) {
152
+ return bothUnattempted [ 0 ] . question ;
153
+ }
154
+
155
+ // If no unattempted question, try least attempted
156
+ const attempts = db . $with ( 'at' ) . as (
157
+ db
158
+ . select ( {
159
+ ...getTableColumns ( QUESTIONS_TABLE ) ,
160
+ user1Count :
161
+ sql `SUM(CASE WHEN ${ QUESTION_ATTEMPTS_TABLE . userId1 } = ${ userId1 } ::uuid OR ${ QUESTION_ATTEMPTS_TABLE . userId2 } = ${ userId1 } ::uuid THEN 1 END)` . as (
162
+ 'user1_attempts'
163
+ ) ,
164
+ user2Count :
165
+ sql `SUM(CASE WHEN ${ QUESTION_ATTEMPTS_TABLE . userId1 } = ${ userId2 } ::uuid OR ${ QUESTION_ATTEMPTS_TABLE . userId2 } = ${ userId2 } ::uuid THEN 1 END)` . as (
166
+ 'user2_attempts'
167
+ ) ,
168
+ } )
169
+ . from ( QUESTIONS_TABLE )
170
+ . innerJoin ( QUESTION_ATTEMPTS_TABLE , and ( ...joinClause ) )
171
+ . where ( and ( ...filterClause ) )
172
+ . groupBy ( QUESTIONS_TABLE . id )
173
+ ) ;
174
+
175
+ const result = await db
176
+ . with ( attempts )
177
+ . select ( )
178
+ . from ( attempts )
179
+ . orderBy ( asc ( sql `COALESCE(user1_attempts,0) + COALESCE(user2_attempts,0)` ) )
180
+ . limit ( 1 ) ;
157
181
158
- if ( result && result . length > 0 ) {
159
- return { ...result [ 0 ] , user1Count : undefined , user2Count : undefined } ;
182
+ if ( result && result . length > 0 ) {
183
+ return { ...result [ 0 ] , user1Count : undefined , user2Count : undefined } ;
184
+ }
160
185
}
161
186
162
- // This branch should not be reached
163
- logger . info ( 'Unreachable Branch - If first query fails, second query must return something' ) ;
187
+ logger . error ( 'No questions found with any filter combination' ) ;
188
+ return null ;
164
189
} ;
0 commit comments