a080ca59d8
Implement v2 upload batch cleanup * Implement v2 upload batch cleanup * Merge remote-tracking branch 'origin/main' into jmathison/v2-upload-batch * Address upload batch review feedback * Raise batch worker coverage * Fix batch cleanup review issues Approved-by: Jay Brown
224 lines
7.2 KiB
SQL
224 lines
7.2 KiB
SQL
-- 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);
|