@@ -48,6 +48,56 @@ async function getAllDescendantFolderIds(
4848 return ( result as unknown as { id : string } [ ] ) . map ( ( row ) => row . id ) ;
4949}
5050
51+ // Batched version: Get descendants for multiple root folders, grouped by root
52+ async function getAllDescendantsByRootFolder (
53+ rootFolderIds : string [ ] ,
54+ userId : string
55+ ) : Promise < Map < string , string [ ] > > {
56+ if ( rootFolderIds . length === 0 ) {
57+ return new Map ( ) ;
58+ }
59+
60+ const queryStart = Date . now ( ) ;
61+ // Single query that tracks which root folder each descendant belongs to
62+ const result = await db . execute < { root_id : string ; descendant_id : string } > ( sql `
63+ WITH RECURSIVE folder_tree AS (
64+ -- Base case: start with root folders, track the root_id
65+ SELECT id as root_id, id as descendant_id, parent_id
66+ FROM folders
67+ WHERE id IN (${ sql . join (
68+ rootFolderIds . map ( ( id ) => sql `${ id } ` ) ,
69+ sql `, `
70+ ) } )
71+ AND user_id = ${ userId }
72+
73+ UNION ALL
74+
75+ -- Recursive case: get children, preserve root_id
76+ SELECT ft.root_id, f.id as descendant_id, f.parent_id
77+ FROM folders f
78+ INNER JOIN folder_tree ft ON f.parent_id = ft.descendant_id
79+ WHERE f.user_id = ${ userId }
80+ )
81+ SELECT DISTINCT root_id, descendant_id FROM folder_tree
82+ ` ) ;
83+ logger . databaseQuery ( "select_recursive" , "folders" , Date . now ( ) - queryStart , userId ) ;
84+
85+ // Group descendants by root folder
86+ const folderMap = new Map < string , string [ ] > ( ) ;
87+ for ( const rootId of rootFolderIds ) {
88+ folderMap . set ( rootId , [ ] ) ;
89+ }
90+
91+ for ( const row of result as unknown as { root_id : string ; descendant_id : string } [ ] ) {
92+ const descendants = folderMap . get ( row . root_id ) ;
93+ if ( descendants ) {
94+ descendants . push ( row . descendant_id ) ;
95+ }
96+ }
97+
98+ return folderMap ;
99+ }
100+
51101// Optimized helper to get counts for multiple folders in a single query
52102async function getCountsForFolders (
53103 folderIds : string [ ] ,
@@ -157,13 +207,9 @@ export async function warmNotesCountsCache(userId: string): Promise<void> {
157207 > = { } ;
158208
159209 if ( rootFolders . length > 0 ) {
160- // Build a map of folder ID to its descendants
161- const folderToDescendants = new Map < string , string [ ] > ( ) ;
162-
163- for ( const rootFolder of rootFolders ) {
164- const descendants = await getAllDescendantFolderIds ( rootFolder . id , userId ) ;
165- folderToDescendants . set ( rootFolder . id , descendants ) ;
166- }
210+ // Get descendants for ALL root folders in a single query (batched)
211+ const rootFolderIds = rootFolders . map ( ( f ) => f . id ) ;
212+ const folderToDescendants = await getAllDescendantsByRootFolder ( rootFolderIds , userId ) ;
167213
168214 // Get counts for all folders in parallel
169215 const folderCountsPromises = Array . from ( folderToDescendants . entries ( ) ) . map (
@@ -273,14 +319,9 @@ const getNotesCountsHandler: RouteHandler<typeof getNotesCountsRoute> = async (c
273319 return c . json ( { } , 200 ) ;
274320 }
275321
276- // Build a map of folder ID to its descendants (including itself)
277- const folderToDescendants = new Map < string , string [ ] > ( ) ;
278-
279- for ( const childFolder of childFolders ) {
280- // Get descendants for this specific child
281- const descendants = await getAllDescendantFolderIds ( childFolder . id , userId ) ;
282- folderToDescendants . set ( childFolder . id , descendants ) ;
283- }
322+ // Get descendants for ALL child folders in a single query (batched)
323+ const childFolderIds = childFolders . map ( ( f ) => f . id ) ;
324+ const folderToDescendants = await getAllDescendantsByRootFolder ( childFolderIds , userId ) ;
284325
285326 // Get counts for all folders in parallel
286327 const folderCountsPromises = Array . from ( folderToDescendants . entries ( ) ) . map (
@@ -389,13 +430,9 @@ const getNotesCountsHandler: RouteHandler<typeof getNotesCountsRoute> = async (c
389430 > = { } ;
390431
391432 if ( rootFolders . length > 0 ) {
392- // Build a map of folder ID to its descendants (including itself)
393- const folderToDescendants = new Map < string , string [ ] > ( ) ;
394-
395- for ( const rootFolder of rootFolders ) {
396- const descendants = await getAllDescendantFolderIds ( rootFolder . id , userId ) ;
397- folderToDescendants . set ( rootFolder . id , descendants ) ;
398- }
433+ // Get descendants for ALL root folders in a single query (batched)
434+ const rootFolderIds = rootFolders . map ( ( f ) => f . id ) ;
435+ const folderToDescendants = await getAllDescendantsByRootFolder ( rootFolderIds , userId ) ;
399436
400437 // Get counts for all folders in parallel
401438 const folderCountsPromises = Array . from ( folderToDescendants . entries ( ) ) . map (
0 commit comments