5
5
eq ,
6
6
getTableColumns ,
7
7
inArray ,
8
+ InferSelectModel ,
8
9
isNull ,
9
10
or ,
10
11
sql ,
@@ -86,11 +87,17 @@ type Params = {
86
87
difficulty ?: string ;
87
88
} ;
88
89
90
+ type IGetRandomQuestionResponse = InferSelectModel < typeof QUESTIONS_TABLE > & {
91
+ attemptCount : number ;
92
+ } ;
93
+
89
94
// 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 > => {
94
101
// If an attempt contains either user's ID
95
102
const ids = [ userId1 , userId2 ] ;
96
103
const userIdClause = [
@@ -103,35 +110,61 @@ export const getRandomQuestion = async ({ userId1, userId2, topics, difficulty }
103
110
or ( ...userIdClause ) ,
104
111
] ;
105
112
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
+ ] ;
110
127
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
+ ) ;
113
138
}
114
139
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 ) ;
118
147
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
+ }
126
152
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 ) ;
130
161
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
135
168
. select ( {
136
169
...getTableColumns ( QUESTIONS_TABLE ) ,
137
170
user1Count :
@@ -145,20 +178,31 @@ export const getRandomQuestion = async ({ userId1, userId2, topics, difficulty }
145
178
} )
146
179
. from ( QUESTIONS_TABLE )
147
180
. 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
+ }
160
204
}
161
205
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 ;
164
208
} ;
0 commit comments