LCOV - code coverage report
Current view: top level - dashql-app/src/session/attachments - attachment_mounts.ts Coverage Total Hit
Test: DashQL 915828b Lines: 88.5 % 61 54
Test Date: 2026-11-06 19:30:32

            Line data    Source code
       1              : import { getOPFSRoot } from '../../platform/opfs_root.js';
       2              : import type { ParquetDatabase } from '../../platform/hyperdb/hyperdb_parquet_export.js';
       3              : import { STORAGE_ATTACHMENTS_FOLDER, STORAGE_SESSIONS_FOLDER } from '../../storage/storage_backend.js';
       4              : import { attachmentPath, attachmentPrefix } from './session_attachments.js';
       5              : 
       6              : /// The mounts of the attachments of the sessions in the embedded Hyper.
       7              : ///
       8              : /// The embedded Hyper reads the attachments of a session from the OPFS folder `sessions/<id>/attachments` (a native
       9              : /// session mirrors its attachments there), mounted read-only at `/mnt/external/attachments/<id>`. A session is
      10              : /// mounted before a plan scans its attachments. A change of the attachments marks the mount stale, the next use
      11              : /// refreshes it, so the database looks up the files it opened again.
      12              : interface AttachmentMount {
      13              :     /// The database that mounted the folder
      14              :     database: ParquetDatabase;
      15              :     /// Did the attachments change since the mount or its last refresh?
      16              :     stale: boolean;
      17              : }
      18              : 
      19            1 : const MOUNTS = new Map<string, AttachmentMount>();
      20              : /// Mounts and refreshes run one after the other
      21            1 : let pending: Promise<unknown> = Promise.resolve();
      22              : 
      23            1 : function serialize<T>(f: () => Promise<T>): Promise<T> {
      24            1 :     const next = pending.then(f, f);
      25            1 :     pending = next.catch(() => {});
      26            1 :     return next;
      27            1 : }
      28              : 
      29              : /// Mount the attachments of a session in a database, refresh the mount if they changed. Returns the mount path.
      30            1 : export function mountSessionAttachments(
      31            1 :     database: ParquetDatabase,
      32            1 :     sessionId: string,
      33            1 :     getRoot: () => Promise<FileSystemDirectoryHandle> = getOPFSRoot,
      34            1 : ): Promise<string> {
      35            1 :     return serialize(async () => {
      36            1 :         const path = attachmentPrefix(sessionId);
      37            1 :         let mount = MOUNTS.get(sessionId);
      38            1 :         if (mount != null && mount.database !== database) {
      39            0 :             await mount.database.removeExternalDirectory(path).catch(() => {});
      40            0 :             MOUNTS.delete(sessionId);
      41            0 :             mount = undefined;
      42            0 :         }
      43            1 :         if (mount == null) {
      44            1 :             const sessions = await (await getRoot()).getDirectoryHandle(STORAGE_SESSIONS_FOLDER, { create: true });
      45            1 :             const session = await sessions.getDirectoryHandle(sessionId, { create: true });
      46            1 :             const folder = await session.getDirectoryHandle(STORAGE_ATTACHMENTS_FOLDER, { create: true });
      47            1 :             await database.registerExternalDirectory(folder, path, 'read');
      48            1 :             MOUNTS.set(sessionId, { database, stale: false });
      49            1 :         } else if (mount.stale) {
      50              :             // Changes during the refresh mark the mount stale again
      51            1 :             mount.stale = false;
      52            1 :             try {
      53            1 :                 await database.refreshExternalDirectory(path);
      54            1 :             } catch (error) {
      55            0 :                 mount.stale = true;
      56            0 :                 throw error;
      57            0 :             }
      58            1 :         }
      59            1 :         return path;
      60            1 :     });
      61            1 : }
      62              : 
      63              : /// The attachments of a session changed, the next use of the mount refreshes it
      64            1 : export function markSessionAttachmentsChanged(sessionId: string): void {
      65            1 :     const mount = MOUNTS.get(sessionId);
      66            1 :     if (mount != null) mount.stale = true;
      67            1 : }
      68              : 
      69              : /// Remove the mount of the attachments of a session
      70            1 : export function unmountSessionAttachments(sessionId: string): Promise<void> {
      71            1 :     return serialize(async () => {
      72            1 :         const mount = MOUNTS.get(sessionId);
      73            1 :         if (mount == null) return;
      74            1 :         MOUNTS.delete(sessionId);
      75            1 :         await mount.database.removeExternalDirectory(attachmentPrefix(sessionId)).catch(() => {});
      76            1 :     });
      77            1 : }
      78              : 
      79              : /// Read the columns of a Parquet attachment with the embedded Hyper
      80            1 : export async function readAttachmentColumns(database: ParquetDatabase, sessionId: string, fileName: string): Promise<string[]> {
      81            1 :     await mountSessionAttachments(database, sessionId);
      82            1 :     const connection = await database.connect({ pooled: true });
      83            1 :     try {
      84            1 :         const path = attachmentPath(sessionId, fileName).replace(/'/g, "''");
      85            1 :         const table = await connection.query(`select * from external('${path}', format => 'parquet') limit 0`);
      86            1 :         return table.schema.fields.map(field => field.name);
      87            1 :     } finally {
      88            1 :         await connection.close();
      89            1 :     }
      90            1 : }
        

Generated by: LCOV version 2.0-1