Files
query-orchestration/internal/database/queries/folders.sql
T
Jay Brown 72de690894 Merged in feature/chatbot1 (pull request #223)
Chatbot functionality

* baseline working

* missing test file

* more tests
2026-05-07 20:56:18 +00:00

183 lines
5.7 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: 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);