Skip to content

Commit

Permalink
Add getDatabaseInfo method
Browse files Browse the repository at this point in the history
  • Loading branch information
DallasHoff committed Jul 6, 2024
1 parent 406eaef commit 0c0c212
Show file tree
Hide file tree
Showing 6 changed files with 141 additions and 2 deletions.
4 changes: 4 additions & 0 deletions docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ export default defineConfig({
text: 'transaction',
link: '/api/transaction',
},
{
text: 'getDatabaseInfo',
link: '/api/getdatabaseinfo',
},
{
text: 'getDatabaseFile',
link: '/api/getdatabasefile',
Expand Down
30 changes: 30 additions & 0 deletions docs/api/getdatabaseinfo.md
Original file line number Diff line number Diff line change
@@ -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');
```

<!-- @include: ../_partials/initialization-note.md -->

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`.
16 changes: 15 additions & 1 deletion src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type {
BatchMessage,
WorkerProxy,
ScalarUserFunction,
GetInfoMessage,
} from './types.js';

export class SQLocal {
Expand Down Expand Up @@ -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') {
Expand Down Expand Up @@ -84,6 +86,7 @@ export class SQLocal {
| DestroyMessage
| FunctionMessage
| ImportMessage
| GetInfoMessage
>
) => {
if (this.isWorkerDestroyed === true) {
Expand Down Expand Up @@ -112,7 +115,8 @@ export class SQLocal {
| QueryMessage
| BatchMessage
| DestroyMessage
| FunctionMessage);
| FunctionMessage
| GetInfoMessage);
break;
}

Expand Down Expand Up @@ -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);
Expand Down
44 changes: 44 additions & 0 deletions src/processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, UserFunction>();
Expand All @@ -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).`
);
Expand All @@ -62,6 +68,7 @@ export class SQLocalProcessor {

this.db?.close();
this.db = undefined;
this.dbStorageType = undefined;
return;
}

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -305,6 +348,7 @@ export class SQLocalProcessor {
this.db.exec({ sql: 'PRAGMA optimize' });
this.db.close();
this.db = undefined;
this.dbStorageType = undefined;
}

this.emitMessage({
Expand Down
20 changes: 19 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[][];
Expand All @@ -14,6 +15,12 @@ export type WorkerProxy = ProxyHandler<Worker> &
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> = T extends Message ? Omit<T, 'queryKey'> : never;
Expand All @@ -24,6 +31,7 @@ export type InputMessage =
| FunctionMessage
| ConfigMessage
| ImportMessage
| GetInfoMessage
| DestroyMessage;
export type QueryMessage = {
type: 'query';
Expand Down Expand Up @@ -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;
Expand All @@ -66,7 +78,8 @@ export type OutputMessage =
| SuccessMessage
| ErrorMessage
| DataMessage
| CallbackMessage;
| CallbackMessage
| InfoMessage;
export type SuccessMessage = {
type: 'success';
queryKey: QueryKey;
Expand All @@ -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 = {
Expand Down
29 changes: 29 additions & 0 deletions test/get-database-info.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});

0 comments on commit 0c0c212

Please sign in to comment.