-- name: CreateFolder :one INSERT INTO folders (path, parentId, clientId, createdBy) VALUES ($1, $2, $3, $4) RETURNING *; -- name: GetFolderByID :one SELECT * FROM folders WHERE id = $1; -- name: GetFolderByPath :one SELECT * FROM folders WHERE clientId = $1 AND path = $2; -- name: GetFoldersByClientID :many SELECT * FROM folders WHERE clientId = $1 ORDER BY path; -- name: GetFoldersByParentID :many SELECT * FROM folders WHERE parentId = $1 ORDER BY path; -- name: RenameFolder :exec UPDATE folders SET path = $2 WHERE id = $1; -- name: GetFolderTree :many WITH RECURSIVE folder_tree AS ( -- Base case: start with the target folder SELECT id, path, parentId, clientId, createdAt, createdBy FROM folders WHERE id = $1 UNION ALL -- Recursive case: find all child folders SELECT f.id, f.path, f.parentId, f.clientId, f.createdAt, f.createdBy FROM folders f INNER JOIN folder_tree ft ON f.parentId = ft.id ) SELECT id, path, parentId, clientId, createdAt, createdBy FROM folder_tree ORDER BY path; -- name: GetRootFolders :many -- Returns folders with null parentId (should only be the "/" root folder for each client) SELECT * FROM folders WHERE clientId = $1 AND parentId IS NULL ORDER BY path; -- name: GetTopLevelFolders :many -- Returns folders that are direct children of the root folder "/" -- These are the user-visible top-level folders (e.g., /folder1, /folder2) SELECT f.* FROM folders f INNER JOIN folders root ON f.parentId = root.id WHERE f.clientId = $1 AND root.path = '/' ORDER BY f.path; -- name: GetClientRootFolder :one -- Returns the root folder "/" for a client SELECT * FROM folders WHERE clientId = $1 AND path = '/'; -- name: GetDocumentsByFolder :many SELECT * FROM documents WHERE folderId = $1 ORDER BY id; -- name: CountDocumentsByFolder :one SELECT COUNT(*) as total FROM documents WHERE folderId = $1; -- name: GetFolderLabelCounts :many SELECT dl.label, COUNT(DISTINCT dl.documentId) as count FROM documents d INNER JOIN documentLabels dl ON d.id = dl.documentId WHERE d.folderId = $1 GROUP BY dl.label ORDER BY dl.label; -- name: UpsertFolder :one -- Insert a folder if it doesn't exist, or return the existing one -- Used for auto-creating folder hierarchies during document upload INSERT INTO folders (path, parentId, clientId, createdBy) VALUES ($1, $2, $3, $4) ON CONFLICT (clientId, path) DO UPDATE SET path = EXCLUDED.path RETURNING *; -- name: CreateFolderIfAbsent :one -- Insert a folder only when it does not already exist. -- Batch uploads use this to distinguish newly-created folder paths from -- pre-existing paths without mutating existing folders. INSERT INTO folders (path, parentId, clientId, createdBy) VALUES (@path, @parent_id, @client_id, @created_by) ON CONFLICT (clientId, path) DO NOTHING RETURNING *; -- name: GetDocumentsByFolderEnriched :many -- Gets documents in a folder with all enriched metadata fields SELECT d.id, d.hash, d.folderId, d.filename, d.originalPath, d.file_size_bytes FROM documents d WHERE d.folderId = $1 ORDER BY d.id; -- name: GetFolderTreeIDs :many -- Get all folder IDs in a folder tree (recursive) for cascade delete operations WITH RECURSIVE folder_tree AS ( SELECT id, path FROM folders WHERE id = @folder_id UNION ALL SELECT f.id, f.path FROM folders f JOIN folder_tree ft ON f.parentId = ft.id ) SELECT id, path FROM folder_tree; -- name: CountDocumentsInFolderTree :one -- Count documents in a folder tree to check before deletion without include_documents WITH RECURSIVE folder_tree AS ( SELECT id FROM folders WHERE id = @folder_id UNION ALL SELECT f.id FROM folders f JOIN folder_tree ft ON f.parentId = ft.id ) SELECT COUNT(*) FROM documents WHERE folderId IN (SELECT id FROM folder_tree); -- name: GetDocumentIDsInFolderTree :many -- Get all document IDs in a folder tree for cascade deletion WITH RECURSIVE folder_tree AS ( SELECT id FROM folders WHERE id = @folder_id UNION ALL SELECT f.id FROM folders f JOIN folder_tree ft ON f.parentId = ft.id ) SELECT id FROM documents WHERE folderId IN (SELECT id FROM folder_tree); -- name: GetDocumentIDsInFolderTreeForClient :many -- Chatbot-scoped recursive folder tree walk. Plan ยง5 mandates that the -- clientId filter applies at every recursive step AND at the final -- document selection so a cross-client root cannot leak documents. -- -- Step-by-step: -- - Base case: select the root folder only when its clientId matches. -- - Recursive case: descend through folders.parentId, requiring the -- child folder's clientId to match. A folder owned by a different -- client cannot enter the CTE even if its parent did (which it -- cannot, because the base case already rejects cross-client roots). -- - Final selection: documents must reside in folder_tree AND share -- the same clientId, defending against any future case where a -- document row references a folder owned by a different client. WITH RECURSIVE folder_tree AS ( SELECT id FROM folders WHERE id = @folder_id::uuid AND clientId = @client_id::varchar UNION ALL SELECT f.id FROM folders f JOIN folder_tree ft ON f.parentId = ft.id WHERE f.clientId = @client_id::varchar ) SELECT id FROM documents WHERE folderId IN (SELECT id FROM folder_tree) AND clientId = @client_id::varchar; -- name: DeleteDocumentUploadsInFolderTree :exec -- @sqlc-vet-disable -- Delete documentUploads rows that reference folders in the tree WITH RECURSIVE folder_tree AS ( SELECT id FROM folders WHERE id = @folder_id UNION ALL SELECT f.id FROM folders f JOIN folder_tree ft ON f.parentId = ft.id ) DELETE FROM documentUploads WHERE folder_id IN (SELECT id FROM folder_tree); -- name: HardDeleteFolderTree :execrows -- @sqlc-vet-disable -- Delete all folders in the tree. PostgreSQL handles ordering within the CTE. -- Returns the number of rows deleted. WITH RECURSIVE folder_tree AS ( SELECT id FROM folders WHERE id = @folder_id UNION ALL SELECT f.id FROM folders f JOIN folder_tree ft ON f.parentId = ft.id ) DELETE FROM folders WHERE id IN (SELECT id FROM folder_tree); -- name: ListSafeAutoCreatedFolderCandidates :many -- @sqlc-vet-disable -- Candidate folders can be deleted only after every batch that touched that -- folder has finished folder cleanup. The path equality check protects renamed -- folders from deletion. SELECT DISTINCT f.id, f.path FROM folders f JOIN batch_folder_candidates bfc ON bfc.folder_id = f.id WHERE bfc.existed_before = false AND bfc.path = f.path AND f.path <> '/' AND NOT EXISTS ( SELECT 1 FROM batch_folder_candidates other_bfc JOIN batch_uploads bu ON bu.id = other_bfc.batch_id WHERE other_bfc.folder_id = f.id AND bu.folder_cleanup_completed_at IS NULL ) ORDER BY f.path DESC LIMIT sqlc.arg(limit_count); -- name: DeleteSafeAutoCreatedFolder :execrows -- @sqlc-vet-disable -- Delete one auto-created folder only when it is currently empty and no longer -- referenced by documents, uploads, or child folders. DELETE FROM folders f WHERE f.id = @folder_id AND f.path <> '/' AND NOT EXISTS (SELECT 1 FROM documents d WHERE d.folderId = f.id) AND NOT EXISTS (SELECT 1 FROM documentUploads du WHERE du.folder_id = f.id) AND NOT EXISTS (SELECT 1 FROM folders child WHERE child.parentId = f.id);