From 0c0c212d6eee8ddf124ab76b2b853ab23f8f80b2 Mon Sep 17 00:00:00 2001 From: Dallas Hoffman Date: Sat, 6 Jul 2024 15:59:47 -0400 Subject: [PATCH] Add getDatabaseInfo method --- docs/.vitepress/config.ts | 4 ++++ docs/api/getdatabaseinfo.md | 30 +++++++++++++++++++++++ src/client.ts | 16 ++++++++++++- src/processor.ts | 44 ++++++++++++++++++++++++++++++++++ src/types.ts | 20 +++++++++++++++- test/get-database-info.test.ts | 29 ++++++++++++++++++++++ 6 files changed, 141 insertions(+), 2 deletions(-) create mode 100644 docs/api/getdatabaseinfo.md create mode 100644 test/get-database-info.test.ts diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 6793142..9bf6340 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -41,6 +41,10 @@ export default defineConfig({ text: 'transaction', link: '/api/transaction', }, + { + text: 'getDatabaseInfo', + link: '/api/getdatabaseinfo', + }, { text: 'getDatabaseFile', link: '/api/getdatabasefile', diff --git a/docs/api/getdatabaseinfo.md b/docs/api/getdatabaseinfo.md new file mode 100644 index 0000000..f635088 --- /dev/null +++ b/docs/api/getdatabaseinfo.md @@ -0,0 +1,30 @@ +# getDatabaseInfo + +Retrieve information about the SQLite database file. + +## Usage + +Access or destructure `getDatabaseInfo` from the `SQLocal` client. + +```javascript +import { SQLocal } from 'sqlocal'; + +export const { getDatabaseInfo } = new SQLocal('database.sqlite3'); +``` + + + +The `getDatabaseInfo` method takes no arguments. It will return a `Promise` for an object that contains information about the database file being used by the `SQLocal` instance. + +```javascript +const databaseInfo = await getDatabaseInfo(); +``` + +The returned object contains the following properties: + +- **`databasePath`** (`string`) - The name of the database file. This will be identical to the value passed to the `SQLocal` constructor at initialization. +- **`databaseSizeBytes`** (`number`) - An integer representing the current file size of the database in bytes. +- **`storageType`** (`'memory' | 'opfs'`) - A string indicating whether the database is saved in the origin private file system or in memory. The database only falls back to being saved in memory if the OPFS cannot be used, such as when the browser does not support it. +- **`persisted`** (`boolean`) - This is `true` if the database is saved in the origin private file system _and_ the application has used [`navigator.storage.persist()`](https://developer.mozilla.org/en-US/docs/Web/API/StorageManager/persist) to instruct the browser not to automatically evict the site's storage. + +If the `SQLocal` instance failed to initialize a database connection, these properties may be `undefined`. diff --git a/src/client.ts b/src/client.ts index ebbe13b..4e8ea4a 100644 --- a/src/client.ts +++ b/src/client.ts @@ -15,6 +15,7 @@ import type { BatchMessage, WorkerProxy, ScalarUserFunction, + GetInfoMessage, } from './types.js'; export class SQLocal { @@ -54,6 +55,7 @@ export class SQLocal { case 'success': case 'data': case 'error': + case 'info': if (message.queryKey && queries.has(message.queryKey)) { const [resolve, reject] = queries.get(message.queryKey)!; if (message.type === 'error') { @@ -84,6 +86,7 @@ export class SQLocal { | DestroyMessage | FunctionMessage | ImportMessage + | GetInfoMessage > ) => { if (this.isWorkerDestroyed === true) { @@ -112,7 +115,8 @@ export class SQLocal { | QueryMessage | BatchMessage | DestroyMessage - | FunctionMessage); + | FunctionMessage + | GetInfoMessage); break; } @@ -257,6 +261,16 @@ export class SQLocal { this.proxy[`_sqlocal_func_${funcName}`] = func; }; + getDatabaseInfo = async () => { + const message = await this.createQuery({ type: 'getinfo' }); + + if (message.type === 'info') { + return message.info; + } else { + throw new Error('The database failed to return valid information.'); + } + }; + getDatabaseFile = async () => { const opfs = await navigator.storage.getDirectory(); const fileHandle = await opfs.getFileHandle(this.databasePath); diff --git a/src/processor.ts b/src/processor.ts index 1cc10aa..7a4824b 100644 --- a/src/processor.ts +++ b/src/processor.ts @@ -15,12 +15,15 @@ import type { ImportMessage, WorkerProxy, RawResultData, + GetInfoMessage, + Sqlite3StorageType, } from './types.js'; export class SQLocalProcessor { protected proxy: WorkerProxy; protected sqlite3: Sqlite3 | undefined; protected db: Sqlite3Db | undefined; + protected dbStorageType: Sqlite3StorageType | undefined; protected config: ProcessorConfig = {}; protected queuedMessages: InputMessage[] = []; protected userFunctions = new Map(); @@ -43,12 +46,15 @@ export class SQLocalProcessor { if (this.db) { this.db?.close(); this.db = undefined; + this.dbStorageType = undefined; } if ('opfs' in this.sqlite3) { this.db = new this.sqlite3.oo1.OpfsDb(this.config.databasePath, 'cw'); + this.dbStorageType = 'opfs'; } else { this.db = new this.sqlite3.oo1.DB(this.config.databasePath, 'cw'); + this.dbStorageType = 'memory'; console.warn( `The origin private file system is not available, so ${this.config.databasePath} will not be persisted. Make sure your web server is configured to use the correct HTTP response headers (See https://sqlocal.dallashoffman.com/guide/setup#cross-origin-isolation).` ); @@ -62,6 +68,7 @@ export class SQLocalProcessor { this.db?.close(); this.db = undefined; + this.dbStorageType = undefined; return; } @@ -90,6 +97,9 @@ export class SQLocalProcessor { case 'function': this.createUserFunction(message); break; + case 'getinfo': + this.getDatabaseInfo(message); + break; case 'import': this.importDb(message); break; @@ -200,6 +210,39 @@ export class SQLocalProcessor { } }; + protected getDatabaseInfo = async (message: GetInfoMessage) => { + try { + const databasePath = this.config.databasePath; + const storageType = this.dbStorageType; + const persisted = + storageType !== undefined + ? storageType !== 'memory' + ? await navigator.storage?.persisted() + : false + : undefined; + + const sizeResult = this.db?.exec({ + sql: 'SELECT page_count * page_size AS size FROM pragma_page_count(), pragma_page_size()', + returnValue: 'resultRows', + rowMode: 'array', + }); + const size = sizeResult?.[0]?.[0]; + const databaseSizeBytes = typeof size === 'number' ? size : undefined; + + this.emitMessage({ + type: 'info', + queryKey: message.queryKey, + info: { databasePath, databaseSizeBytes, storageType, persisted }, + }); + } catch (error) { + this.emitMessage({ + type: 'error', + queryKey: message.queryKey, + error, + }); + } + }; + protected createUserFunction = (message: FunctionMessage) => { const { functionName, functionType, queryKey } = message; let func; @@ -305,6 +348,7 @@ export class SQLocalProcessor { this.db.exec({ sql: 'PRAGMA optimize' }); this.db.close(); this.db = undefined; + this.dbStorageType = undefined; } this.emitMessage({ diff --git a/src/types.ts b/src/types.ts index 126eda0..4a236bb 100644 --- a/src/types.ts +++ b/src/types.ts @@ -3,6 +3,7 @@ import type { Database, Sqlite3Static } from '@sqlite.org/sqlite-wasm'; export type Sqlite3 = Sqlite3Static; export type Sqlite3Db = Database; export type Sqlite3Method = 'get' | 'all' | 'run' | 'values'; +export type Sqlite3StorageType = 'memory' | 'opfs'; export type QueryKey = string; export type RawResultData = { rows: unknown[] | unknown[][]; @@ -14,6 +15,12 @@ export type WorkerProxy = ProxyHandler & export type ProcessorConfig = { databasePath?: string; }; +export type DatabaseInfo = { + databasePath?: string; + databaseSizeBytes?: number; + storageType?: Sqlite3StorageType; + persisted?: boolean; +}; export type Message = InputMessage | OutputMessage; export type OmitQueryKey = T extends Message ? Omit : never; @@ -24,6 +31,7 @@ export type InputMessage = | FunctionMessage | ConfigMessage | ImportMessage + | GetInfoMessage | DestroyMessage; export type QueryMessage = { type: 'query'; @@ -57,6 +65,10 @@ export type ImportMessage = { queryKey: QueryKey; database: ArrayBuffer | Uint8Array; }; +export type GetInfoMessage = { + type: 'getinfo'; + queryKey: QueryKey; +}; export type DestroyMessage = { type: 'destroy'; queryKey: QueryKey; @@ -66,7 +78,8 @@ export type OutputMessage = | SuccessMessage | ErrorMessage | DataMessage - | CallbackMessage; + | CallbackMessage + | InfoMessage; export type SuccessMessage = { type: 'success'; queryKey: QueryKey; @@ -89,6 +102,11 @@ export type CallbackMessage = { name: string; args: unknown[]; }; +export type InfoMessage = { + type: 'info'; + queryKey: QueryKey; + info: DatabaseInfo; +}; export type UserFunction = CallbackUserFunction | ScalarUserFunction; export type CallbackUserFunction = { diff --git a/test/get-database-info.test.ts b/test/get-database-info.test.ts new file mode 100644 index 0000000..585a14b --- /dev/null +++ b/test/get-database-info.test.ts @@ -0,0 +1,29 @@ +import { afterEach, describe, expect, it } from 'vitest'; +import { SQLocal } from '../src/index'; + +describe('getDatabaseInfo', () => { + const { sql, getDatabaseInfo } = new SQLocal( + 'get-database-info-test.sqlite3' + ); + + afterEach(async () => { + const opfs = await navigator.storage.getDirectory(); + await opfs.removeEntry('get-database-info-test.sqlite3'); + }); + + it('should return information about the database', async () => { + const info1 = await getDatabaseInfo(); + expect(info1).toEqual({ + databasePath: 'get-database-info-test.sqlite3', + databaseSizeBytes: 0, + storageType: 'opfs', + persisted: false, + }); + + await sql`CREATE TABLE nums (num INTEGER NOT NULL)`; + await sql`INSERT INTO nums (num) VALUES (493), (820), (361), (125)`; + + const info2 = await getDatabaseInfo(); + expect(info2.databaseSizeBytes).toBeGreaterThan(0); + }); +});