@dxos/sql-sqlite 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +8 -0
- package/README.md +21 -0
- package/package.json +65 -0
- package/src/OpfsWorker.ts +5 -0
- package/src/SqlExport.ts +13 -0
- package/src/SqliteClient.ts +5 -0
- package/src/SqliteMigrator.ts +5 -0
- package/src/index.ts +8 -0
- package/src/platform/browser.ts +27 -0
- package/src/platform/bun.ts +33 -0
- package/src/platform/node.ts +33 -0
- package/src/testing/opfs-worker.browser.test.ts +34 -0
- package/src/testing/opfs-worker.ts +13 -0
- package/src/testing/sqlite-effect-idb.browser.test.ts +265 -0
- package/src/testing/sqlite-idb.browser.test.ts +113 -0
- package/src/testing/sqlite-memory.browser.test.ts +78 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
Copyright (c) 2022 DXOS
|
|
3
|
+
|
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
5
|
+
|
|
6
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
7
|
+
|
|
8
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# @dxos/@dxos/sql-sqlite
|
|
2
|
+
|
|
3
|
+
Sqlite client
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm i @dxos/@dxos/sql-sqlite
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## DXOS Resources
|
|
12
|
+
|
|
13
|
+
- [Website](https://dxos.org)
|
|
14
|
+
- [Developer Documentation](https://docs.dxos.org)
|
|
15
|
+
- Talk to us on [Discord](https://dxos.org/discord)
|
|
16
|
+
|
|
17
|
+
## Contributions
|
|
18
|
+
|
|
19
|
+
Your ideas, issues, and code are most welcome. Please take a look at our [community code of conduct](https://github.com/dxos/dxos/blob/main/CODE_OF_CONDUCT.md), the [issue guide](https://github.com/dxos/dxos/blob/main/CONTRIBUTING.md#submitting-issues), and the [PR contribution guide](https://github.com/dxos/dxos/blob/main/CONTRIBUTING.md#submitting-prs).
|
|
20
|
+
|
|
21
|
+
License: [MIT](./LICENSE) Copyright 2022 © DXOS
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dxos/sql-sqlite",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "Sqlite client",
|
|
5
|
+
"homepage": "https://dxos.org",
|
|
6
|
+
"bugs": "https://github.com/dxos/dxos/issues",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"author": "info@dxos.org",
|
|
9
|
+
"sideEffects": true,
|
|
10
|
+
"type": "module",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/types/src/index.d.ts",
|
|
14
|
+
"browser": "./dist/lib/browser/index.mjs",
|
|
15
|
+
"node": "./dist/lib/node-esm/index.mjs"
|
|
16
|
+
},
|
|
17
|
+
"./SqliteClient": {
|
|
18
|
+
"types": "./dist/types/src/SqliteClient.d.ts",
|
|
19
|
+
"browser": "./dist/lib/browser/SqliteClient.mjs",
|
|
20
|
+
"node": "./dist/lib/node-esm/SqliteClient.mjs"
|
|
21
|
+
},
|
|
22
|
+
"./OpfsWorker": {
|
|
23
|
+
"types": "./dist/types/src/OpfsWorker.d.ts",
|
|
24
|
+
"browser": "./dist/lib/browser/OpfsWorker.mjs",
|
|
25
|
+
"node": "./dist/lib/node-esm/OpfsWorker.mjs"
|
|
26
|
+
},
|
|
27
|
+
"./SqliteMigrator": {
|
|
28
|
+
"types": "./dist/types/src/SqliteMigrator.d.ts",
|
|
29
|
+
"browser": "./dist/lib/browser/SqliteMigrator.mjs",
|
|
30
|
+
"node": "./dist/lib/node-esm/SqliteMigrator.mjs"
|
|
31
|
+
},
|
|
32
|
+
"./platform": {
|
|
33
|
+
"types": "./dist/types/src/platform/browser.d.ts",
|
|
34
|
+
"browser": "./dist/lib/browser/platform/browser.mjs",
|
|
35
|
+
"bun": "./dist/lib/node-esm/platform/bun.mjs",
|
|
36
|
+
"node": "./dist/lib/node-esm/platform/node.mjs"
|
|
37
|
+
},
|
|
38
|
+
"./SqlExport": {
|
|
39
|
+
"types": "./dist/types/src/SqlExport.d.ts",
|
|
40
|
+
"browser": "./dist/lib/browser/SqlExport.mjs",
|
|
41
|
+
"node": "./dist/lib/node-esm/SqlExport.mjs"
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"files": [
|
|
45
|
+
"dist",
|
|
46
|
+
"src"
|
|
47
|
+
],
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"@dxos/wa-sqlite": "^0.2.1",
|
|
50
|
+
"@effect/experimental": "0.57.11",
|
|
51
|
+
"@effect/platform": "0.93.6",
|
|
52
|
+
"@effect/sql": "0.48.6",
|
|
53
|
+
"@effect/sql-sqlite-bun": "0.49.0",
|
|
54
|
+
"@effect/sql-sqlite-node": "0.49.1",
|
|
55
|
+
"effect": "3.19.11"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@effect/sql-sqlite-wasm": "0.49.0",
|
|
59
|
+
"@effect/wa-sqlite": "^0.1.2"
|
|
60
|
+
},
|
|
61
|
+
"publishConfig": {
|
|
62
|
+
"access": "public"
|
|
63
|
+
},
|
|
64
|
+
"beast": {}
|
|
65
|
+
}
|
package/src/SqlExport.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import type * as SqlError from '@effect/sql/SqlError';
|
|
6
|
+
import * as Context from 'effect/Context';
|
|
7
|
+
import type * as Effect from 'effect/Effect';
|
|
8
|
+
|
|
9
|
+
export interface Service {
|
|
10
|
+
export: Effect.Effect<Uint8Array, SqlError.SqlError>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class SqlExport extends Context.Tag('@dxos/sql-sqlite/SqlExport')<SqlExport, Service>() {}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
export * from '@effect/sql-sqlite-wasm';
|
|
6
|
+
export * as OpfsWorker from '@effect/sql-sqlite-wasm/OpfsWorker';
|
|
7
|
+
export * as SqliteClient from '@effect/sql-sqlite-wasm/SqliteClient';
|
|
8
|
+
export * as SqliteMigrator from '@effect/sql-sqlite-wasm/SqliteMigrator';
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import type * as SqlClient from '@effect/sql/SqlClient';
|
|
6
|
+
import type * as SqlError from '@effect/sql/SqlError';
|
|
7
|
+
import * as SqliteClient from '@effect/sql-sqlite-wasm/SqliteClient';
|
|
8
|
+
import type * as ConfigError from 'effect/ConfigError';
|
|
9
|
+
import * as Effect from 'effect/Effect';
|
|
10
|
+
import * as Layer from 'effect/Layer';
|
|
11
|
+
|
|
12
|
+
import * as SqlExport from '../SqlExport';
|
|
13
|
+
|
|
14
|
+
const sqlExportBrowser: Layer.Layer<SqlExport.SqlExport, SqlError.SqlError, SqliteClient.SqliteClient> = Layer.effect(
|
|
15
|
+
SqlExport.SqlExport,
|
|
16
|
+
Effect.gen(function* () {
|
|
17
|
+
const sql = yield* SqliteClient.SqliteClient;
|
|
18
|
+
return {
|
|
19
|
+
export: sql.export,
|
|
20
|
+
} satisfies SqlExport.Service;
|
|
21
|
+
}),
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
export const layerMemory: Layer.Layer<
|
|
25
|
+
SqlClient.SqlClient | SqlExport.SqlExport,
|
|
26
|
+
ConfigError.ConfigError | SqlError.SqlError
|
|
27
|
+
> = sqlExportBrowser.pipe(Layer.provideMerge(SqliteClient.layerMemory({})));
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import type * as SqlClient from '@effect/sql/SqlClient';
|
|
6
|
+
import type * as SqlError from '@effect/sql/SqlError';
|
|
7
|
+
import * as SqliteClient from '@effect/sql-sqlite-bun/SqliteClient';
|
|
8
|
+
import type * as ConfigError from 'effect/ConfigError';
|
|
9
|
+
import * as Effect from 'effect/Effect';
|
|
10
|
+
import * as Layer from 'effect/Layer';
|
|
11
|
+
|
|
12
|
+
import * as SqlExport from '../SqlExport';
|
|
13
|
+
|
|
14
|
+
const sqlExportBun: Layer.Layer<SqlExport.SqlExport, SqlError.SqlError, SqliteClient.SqliteClient> = Layer.effect(
|
|
15
|
+
SqlExport.SqlExport,
|
|
16
|
+
Effect.gen(function* () {
|
|
17
|
+
const sql = yield* SqliteClient.SqliteClient;
|
|
18
|
+
return {
|
|
19
|
+
export: sql.export,
|
|
20
|
+
} satisfies SqlExport.Service;
|
|
21
|
+
}),
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
export const layerMemory: Layer.Layer<
|
|
25
|
+
SqlClient.SqlClient | SqliteClient.SqliteClient | SqlExport.SqlExport,
|
|
26
|
+
ConfigError.ConfigError | SqlError.SqlError
|
|
27
|
+
> = sqlExportBun.pipe(
|
|
28
|
+
Layer.provideMerge(
|
|
29
|
+
SqliteClient.layer({
|
|
30
|
+
filename: ':memory:',
|
|
31
|
+
}),
|
|
32
|
+
),
|
|
33
|
+
);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import type * as SqlClient from '@effect/sql/SqlClient';
|
|
6
|
+
import type * as SqlError from '@effect/sql/SqlError';
|
|
7
|
+
import * as SqliteClient from '@effect/sql-sqlite-node/SqliteClient';
|
|
8
|
+
import type * as ConfigError from 'effect/ConfigError';
|
|
9
|
+
import * as Effect from 'effect/Effect';
|
|
10
|
+
import * as Layer from 'effect/Layer';
|
|
11
|
+
|
|
12
|
+
import * as SqlExport from '../SqlExport';
|
|
13
|
+
|
|
14
|
+
const sqlExportNode: Layer.Layer<SqlExport.SqlExport, SqlError.SqlError, SqliteClient.SqliteClient> = Layer.effect(
|
|
15
|
+
SqlExport.SqlExport,
|
|
16
|
+
Effect.gen(function* () {
|
|
17
|
+
const sql = yield* SqliteClient.SqliteClient;
|
|
18
|
+
return {
|
|
19
|
+
export: sql.export,
|
|
20
|
+
} satisfies SqlExport.Service;
|
|
21
|
+
}),
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
export const layerMemory: Layer.Layer<
|
|
25
|
+
SqlClient.SqlClient | SqliteClient.SqliteClient,
|
|
26
|
+
ConfigError.ConfigError | SqlError.SqlError
|
|
27
|
+
> = sqlExportNode.pipe(
|
|
28
|
+
Layer.provideMerge(
|
|
29
|
+
SqliteClient.layer({
|
|
30
|
+
filename: ':memory:',
|
|
31
|
+
}),
|
|
32
|
+
),
|
|
33
|
+
);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import * as Reactivity from '@effect/experimental/Reactivity';
|
|
6
|
+
import * as SqlClient from '@effect/sql/SqlClient';
|
|
7
|
+
import { describe, expect, it } from '@effect/vitest';
|
|
8
|
+
import * as Effect from 'effect/Effect';
|
|
9
|
+
import * as Layer from 'effect/Layer';
|
|
10
|
+
|
|
11
|
+
import * as SqliteClient from '../SqliteClient';
|
|
12
|
+
|
|
13
|
+
describe.skip('opfs-worker browser test', () => {
|
|
14
|
+
it.effect(
|
|
15
|
+
'should run the opfs worker',
|
|
16
|
+
Effect.fnUntraced(function* () {
|
|
17
|
+
const clientLayer = SqliteClient.layer({
|
|
18
|
+
worker: Effect.sync(() => new Worker(new URL('./opfs-worker.ts', import.meta.url), { type: 'module' })),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
yield* Effect.gen(function* () {
|
|
22
|
+
const client = yield* SqlClient.SqlClient;
|
|
23
|
+
yield* client`CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)`;
|
|
24
|
+
yield* client`INSERT INTO users (name) VALUES ('Alice')`;
|
|
25
|
+
yield* client`INSERT INTO users (name) VALUES ('Bob')`;
|
|
26
|
+
const results = yield* client`SELECT * FROM users`;
|
|
27
|
+
|
|
28
|
+
expect(results).toHaveLength(2);
|
|
29
|
+
expect(results[0].name).toBe('Alice');
|
|
30
|
+
expect(results[1].name).toBe('Bob');
|
|
31
|
+
}).pipe(Effect.provide(clientLayer.pipe(Layer.provideMerge(Reactivity.layer))));
|
|
32
|
+
}),
|
|
33
|
+
);
|
|
34
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
/// <reference lib="webworker" />
|
|
6
|
+
|
|
7
|
+
import * as Effect from 'effect/Effect';
|
|
8
|
+
|
|
9
|
+
import * as OpfsWorker from '../OpfsWorker';
|
|
10
|
+
|
|
11
|
+
const DB_NAME = 'DXOS';
|
|
12
|
+
|
|
13
|
+
void Effect.runFork(OpfsWorker.run({ port: self, dbName: DB_NAME }));
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import * as Reactivity from '@effect/experimental/Reactivity';
|
|
6
|
+
import * as SqlClient from '@effect/sql/SqlClient';
|
|
7
|
+
import type * as SqlConnection from '@effect/sql/SqlConnection';
|
|
8
|
+
import * as SqlError from '@effect/sql/SqlError';
|
|
9
|
+
import * as Statement from '@effect/sql/Statement';
|
|
10
|
+
import { describe, expect, it } from '@effect/vitest';
|
|
11
|
+
import * as Chunk from 'effect/Chunk';
|
|
12
|
+
import * as Effect from 'effect/Effect';
|
|
13
|
+
import * as Function from 'effect/Function';
|
|
14
|
+
import * as Layer from 'effect/Layer';
|
|
15
|
+
import * as Scope from 'effect/Scope';
|
|
16
|
+
import * as Stream from 'effect/Stream';
|
|
17
|
+
|
|
18
|
+
// @ts-expect-error
|
|
19
|
+
import { SQLITE_OPEN_CREATE, SQLITE_OPEN_READWRITE } from '@dxos/wa-sqlite';
|
|
20
|
+
// @ts-expect-error
|
|
21
|
+
import * as WaSqlite from '@dxos/wa-sqlite';
|
|
22
|
+
// @ts-expect-error
|
|
23
|
+
import SQLiteAsyncESMFactory from '@dxos/wa-sqlite/dist/wa-sqlite-async.mjs';
|
|
24
|
+
// @ts-expect-error
|
|
25
|
+
import { IDBBatchAtomicVFS } from '@dxos/wa-sqlite/src/examples/IDBBatchAtomicVFS.js';
|
|
26
|
+
|
|
27
|
+
//
|
|
28
|
+
// Copyright 2025 DXOS.org
|
|
29
|
+
//
|
|
30
|
+
|
|
31
|
+
const TEST_VFS_NAME = 'idbbatchvfs';
|
|
32
|
+
|
|
33
|
+
// Resolve WASM URL explicitly for Vite compatibility.
|
|
34
|
+
const wasmUrl = new URL('@dxos/wa-sqlite/dist/wa-sqlite-async.wasm', import.meta.url).href;
|
|
35
|
+
|
|
36
|
+
const ATTR_DB_SYSTEM_NAME = 'db.system.name';
|
|
37
|
+
|
|
38
|
+
export interface SqliteClientIDBConfig {
|
|
39
|
+
readonly dbName: string;
|
|
40
|
+
readonly installReactivityHooks?: boolean;
|
|
41
|
+
readonly spanAttributes?: Record<string, unknown>;
|
|
42
|
+
readonly transformResultNames?: (str: string) => string;
|
|
43
|
+
readonly transformQueryNames?: (str: string) => string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @category type ids
|
|
48
|
+
* @since 1.0.0
|
|
49
|
+
*/
|
|
50
|
+
export const TypeId: unique symbol = Symbol.for('@effect/sql-sqlite-wasm/SqliteClient');
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @category type ids
|
|
54
|
+
* @since 1.0.0
|
|
55
|
+
*/
|
|
56
|
+
export type TypeId = typeof TypeId;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @category constructor
|
|
60
|
+
* @since 1.0.0
|
|
61
|
+
*/
|
|
62
|
+
export const makeIdb = (
|
|
63
|
+
options: SqliteClientIDBConfig,
|
|
64
|
+
): Effect.Effect<SqlClient.SqlClient, SqlError.SqlError, Scope.Scope | Reactivity.Reactivity> =>
|
|
65
|
+
Effect.gen(function* () {
|
|
66
|
+
const reactivity = yield* Reactivity.Reactivity;
|
|
67
|
+
const compiler = Statement.makeCompilerSqlite(options.transformQueryNames);
|
|
68
|
+
const transformRows = options.transformResultNames
|
|
69
|
+
? Statement.defaultTransforms(options.transformResultNames).array
|
|
70
|
+
: undefined;
|
|
71
|
+
|
|
72
|
+
const makeConnection = Effect.gen(function* () {
|
|
73
|
+
const factory = yield* Effect.promise(() =>
|
|
74
|
+
SQLiteAsyncESMFactory({
|
|
75
|
+
locateFile: (path: string) => (path.endsWith('.wasm') ? wasmUrl : path),
|
|
76
|
+
}),
|
|
77
|
+
);
|
|
78
|
+
const sqlite3 = WaSqlite.Factory(factory);
|
|
79
|
+
|
|
80
|
+
const vfs = yield* Effect.promise(() => IDBBatchAtomicVFS.create(TEST_VFS_NAME, factory));
|
|
81
|
+
console.log({ reg: sqlite3.vfs_register(vfs as any, false) });
|
|
82
|
+
// }
|
|
83
|
+
const db = yield* Effect.acquireRelease(
|
|
84
|
+
Effect.try({
|
|
85
|
+
try: () => sqlite3.open_v2(options.dbName, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, TEST_VFS_NAME),
|
|
86
|
+
catch: (cause) => new SqlError.SqlError({ cause, message: 'Failed to open database' }),
|
|
87
|
+
}),
|
|
88
|
+
(db) => Effect.sync(() => sqlite3.close(db)),
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
if (options.installReactivityHooks) {
|
|
92
|
+
sqlite3.update_hook(db, (_op: any, _db: any, table: any, rowid: any) => {
|
|
93
|
+
if (!table) return;
|
|
94
|
+
const id = String(Number(rowid));
|
|
95
|
+
reactivity.unsafeInvalidate({ [table]: [id] });
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const run = (sql: string, params: ReadonlyArray<unknown> = [], rowMode: 'object' | 'array' = 'object') =>
|
|
100
|
+
Effect.try({
|
|
101
|
+
try: () => {
|
|
102
|
+
const results: Array<any> = [];
|
|
103
|
+
for (const stmt of sqlite3.statements(db, sql)) {
|
|
104
|
+
let columns: Array<string> | undefined;
|
|
105
|
+
sqlite3.bind_collection(stmt, params as any);
|
|
106
|
+
while (sqlite3.step(stmt) === WaSqlite.SQLITE_ROW) {
|
|
107
|
+
columns = columns ?? sqlite3.column_names(stmt);
|
|
108
|
+
const row = sqlite3.row(stmt);
|
|
109
|
+
if (rowMode === 'object') {
|
|
110
|
+
const obj: Record<string, any> = {};
|
|
111
|
+
for (let i = 0; i < columns!.length; i++) {
|
|
112
|
+
obj[columns![i]] = row[i];
|
|
113
|
+
}
|
|
114
|
+
results.push(obj);
|
|
115
|
+
} else {
|
|
116
|
+
results.push(row);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return results;
|
|
121
|
+
},
|
|
122
|
+
catch: (cause) => new SqlError.SqlError({ cause, message: 'Failed to execute statement' }),
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
return Function.identity<SqlConnection.Connection>({
|
|
126
|
+
execute: (sql, params, transformRows) =>
|
|
127
|
+
transformRows ? Effect.map(run(sql, params), transformRows) : run(sql, params),
|
|
128
|
+
executeRaw: (sql, params) => run(sql, params),
|
|
129
|
+
executeValues: (sql, params) => run(sql, params, 'array'),
|
|
130
|
+
executeUnprepared(sql, params, transformRows) {
|
|
131
|
+
return this.execute(sql, params, transformRows);
|
|
132
|
+
},
|
|
133
|
+
executeStream: (sql, params, transformRows) => {
|
|
134
|
+
function* stream() {
|
|
135
|
+
for (const stmt of sqlite3.statements(db, sql)) {
|
|
136
|
+
let columns: Array<string> | undefined;
|
|
137
|
+
sqlite3.bind_collection(stmt, params as any);
|
|
138
|
+
while (sqlite3.step(stmt) === WaSqlite.SQLITE_ROW) {
|
|
139
|
+
columns = columns ?? sqlite3.column_names(stmt);
|
|
140
|
+
const row = sqlite3.row(stmt);
|
|
141
|
+
const obj: Record<string, any> = {};
|
|
142
|
+
for (let i = 0; i < columns!.length; i++) {
|
|
143
|
+
obj[columns![i]] = row[i];
|
|
144
|
+
}
|
|
145
|
+
yield obj;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return Stream.suspend(() => Stream.fromIteratorSucceed(stream()[Symbol.iterator]())).pipe(
|
|
150
|
+
transformRows
|
|
151
|
+
? Stream.mapChunks((chunk) => Chunk.unsafeFromArray(transformRows(Chunk.toReadonlyArray(chunk))))
|
|
152
|
+
: Function.identity,
|
|
153
|
+
Stream.mapError((cause) => new SqlError.SqlError({ cause, message: 'Failed to execute statement' })),
|
|
154
|
+
);
|
|
155
|
+
},
|
|
156
|
+
// export: Effect.try({
|
|
157
|
+
// try: () => sqlite3.serialize(db, 'main'),
|
|
158
|
+
// catch: (cause) => new SqlError({ cause, message: 'Failed to export database' }),
|
|
159
|
+
// }),
|
|
160
|
+
// import: (data) =>
|
|
161
|
+
// Effect.try({
|
|
162
|
+
// try: () => sqlite3.deserialize(db, 'main', data, data.length, data.length, 1 | 2),
|
|
163
|
+
// catch: (cause) => new SqlError({ cause, message: 'Failed to import database' }),
|
|
164
|
+
// }),
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const semaphore = yield* Effect.makeSemaphore(1);
|
|
169
|
+
const connection = yield* makeConnection;
|
|
170
|
+
|
|
171
|
+
const acquirer = semaphore.withPermits(1)(Effect.succeed(connection));
|
|
172
|
+
const transactionAcquirer = Effect.uninterruptibleMask((restore) =>
|
|
173
|
+
Effect.as(
|
|
174
|
+
Effect.zipRight(
|
|
175
|
+
restore(semaphore.take(1)),
|
|
176
|
+
Effect.tap(Effect.scope, (scope) => Scope.addFinalizer(scope, semaphore.release(1))),
|
|
177
|
+
),
|
|
178
|
+
connection,
|
|
179
|
+
),
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
return Object.assign(
|
|
183
|
+
(yield* SqlClient.make({
|
|
184
|
+
acquirer,
|
|
185
|
+
compiler,
|
|
186
|
+
transactionAcquirer,
|
|
187
|
+
spanAttributes: [
|
|
188
|
+
...(options.spanAttributes ? Object.entries(options.spanAttributes) : []),
|
|
189
|
+
[ATTR_DB_SYSTEM_NAME, 'sqlite'],
|
|
190
|
+
],
|
|
191
|
+
transformRows,
|
|
192
|
+
})) as SqlClient.SqlClient,
|
|
193
|
+
{
|
|
194
|
+
[TypeId]: TypeId as TypeId,
|
|
195
|
+
config: options,
|
|
196
|
+
// export: semaphore.withPermits(1)(connection.export),
|
|
197
|
+
// import: (data: Uint8Array) => semaphore.withPermits(1)(connection.import(data)),
|
|
198
|
+
},
|
|
199
|
+
);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
const TestLayer = Layer.scoped(
|
|
203
|
+
SqlClient.SqlClient,
|
|
204
|
+
makeIdb({
|
|
205
|
+
dbName: 'testing',
|
|
206
|
+
}),
|
|
207
|
+
).pipe(Layer.provideMerge(Reactivity.layer));
|
|
208
|
+
|
|
209
|
+
// Doesn't work yet.
|
|
210
|
+
describe.skip('effect SQLite with IDBBatchAtomicVFS', () => {
|
|
211
|
+
it.effect(
|
|
212
|
+
'basic CRUD operations',
|
|
213
|
+
Effect.fnUntraced(function* () {
|
|
214
|
+
const sql = yield* SqlClient.SqlClient;
|
|
215
|
+
|
|
216
|
+
// Create table.
|
|
217
|
+
yield* sql`
|
|
218
|
+
CREATE TABLE IF NOT EXISTS users (
|
|
219
|
+
id INTEGER PRIMARY KEY,
|
|
220
|
+
name TEXT NOT NULL,
|
|
221
|
+
email TEXT
|
|
222
|
+
)
|
|
223
|
+
`;
|
|
224
|
+
|
|
225
|
+
// Insert data.
|
|
226
|
+
yield* sql`
|
|
227
|
+
INSERT INTO users (name, email) VALUES ('Alice', 'alice@example.com');
|
|
228
|
+
INSERT INTO users (name, email) VALUES ('Bob', 'bob@example.com');
|
|
229
|
+
`;
|
|
230
|
+
|
|
231
|
+
// Select data.
|
|
232
|
+
const results = yield* sql`SELECT * FROM users ORDER BY id`;
|
|
233
|
+
|
|
234
|
+
expect(results).toHaveLength(2);
|
|
235
|
+
expect(results[0].name).toBe('Alice');
|
|
236
|
+
expect(results[1].name).toBe('Bob');
|
|
237
|
+
|
|
238
|
+
// // Update data.
|
|
239
|
+
// await sqlite3.exec(db, `UPDATE users SET email = 'alice.updated@example.com' WHERE name = 'Alice'`);
|
|
240
|
+
|
|
241
|
+
// // Verify update.
|
|
242
|
+
// const updatedResults: Array<{ email: string }> = [];
|
|
243
|
+
// await sqlite3.exec(db, `SELECT email FROM users WHERE name = 'Alice'`, (row: any[]) => {
|
|
244
|
+
// updatedResults.push({ email: row[0] });
|
|
245
|
+
// });
|
|
246
|
+
// expect(updatedResults[0].email).toBe('alice.updated@example.com');
|
|
247
|
+
|
|
248
|
+
// // Delete data.
|
|
249
|
+
// await sqlite3.exec(db, `DELETE FROM users WHERE name = 'Bob'`);
|
|
250
|
+
|
|
251
|
+
// // Verify deletion.
|
|
252
|
+
// const remainingResults: Array<{ name: string }> = [];
|
|
253
|
+
// await sqlite3.exec(db, 'SELECT name FROM users', (row: any[]) => {
|
|
254
|
+
// remainingResults.push({ name: row[0] });
|
|
255
|
+
// });
|
|
256
|
+
// expect(remainingResults).toHaveLength(1);
|
|
257
|
+
// expect(remainingResults[0].name).toBe('Alice');
|
|
258
|
+
|
|
259
|
+
// // Close database.
|
|
260
|
+
// await sqlite3.close(db);
|
|
261
|
+
|
|
262
|
+
// await sleep(10000000);
|
|
263
|
+
}, Effect.provide(TestLayer)),
|
|
264
|
+
);
|
|
265
|
+
});
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { afterEach, describe, expect, test } from 'vitest';
|
|
6
|
+
|
|
7
|
+
// @ts-expect-error
|
|
8
|
+
import * as WaSqlite from '@dxos/wa-sqlite';
|
|
9
|
+
// @ts-expect-error
|
|
10
|
+
import { SQLITE_ROW } from '@dxos/wa-sqlite';
|
|
11
|
+
// @ts-expect-error - No type declarations for this module.
|
|
12
|
+
import SQLiteAsyncESMFactory from '@dxos/wa-sqlite/dist/wa-sqlite-async.mjs';
|
|
13
|
+
// @ts-expect-error - No type declarations for this module.
|
|
14
|
+
import { IDBBatchAtomicVFS } from '@dxos/wa-sqlite/src/examples/IDBBatchAtomicVFS.js';
|
|
15
|
+
|
|
16
|
+
const TEST_VFS_NAME = 'test-idb-vfs';
|
|
17
|
+
|
|
18
|
+
// Resolve WASM URL explicitly for Vite compatibility.
|
|
19
|
+
const wasmUrl = new URL('@dxos/wa-sqlite/dist/wa-sqlite-async.wasm', import.meta.url).href;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Delete IndexedDB database.
|
|
23
|
+
*/
|
|
24
|
+
const deleteDatabase = (name: string): Promise<void> =>
|
|
25
|
+
new Promise((resolve, reject) => {
|
|
26
|
+
const request = indexedDB.deleteDatabase(name);
|
|
27
|
+
request.onsuccess = () => resolve();
|
|
28
|
+
request.onerror = () => reject(request.error);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// Doesn't work yet.
|
|
32
|
+
describe.skip('wa-sqlite with IDBBatchAtomicVFS', () => {
|
|
33
|
+
let sqlite3: ReturnType<typeof WaSqlite.Factory>;
|
|
34
|
+
let vfs: any;
|
|
35
|
+
let db: number;
|
|
36
|
+
|
|
37
|
+
afterEach(async () => {
|
|
38
|
+
// Close database.
|
|
39
|
+
if (db && sqlite3) {
|
|
40
|
+
await sqlite3.close(db);
|
|
41
|
+
db = 0;
|
|
42
|
+
}
|
|
43
|
+
// Note: Skip vfs.close() - it fails with "Cannot read properties of undefined (reading 'flags')".
|
|
44
|
+
// Just clean up IndexedDB.
|
|
45
|
+
await deleteDatabase(TEST_VFS_NAME).catch(() => {});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('basic CRUD operations', async () => {
|
|
49
|
+
// Initialize async WASM module.
|
|
50
|
+
console.log(1);
|
|
51
|
+
const module = await SQLiteAsyncESMFactory({
|
|
52
|
+
locateFile: (path: string) => (path.endsWith('.wasm') ? wasmUrl : path),
|
|
53
|
+
});
|
|
54
|
+
sqlite3 = WaSqlite.Factory(module);
|
|
55
|
+
console.log(2);
|
|
56
|
+
// Create and register IDBBatchAtomicVFS.
|
|
57
|
+
vfs = IDBBatchAtomicVFS.create('demo', module, { lockPolicy: 'shared+hint' });
|
|
58
|
+
console.log(3);
|
|
59
|
+
console.log('vfs methods:', Object.keys(vfs));
|
|
60
|
+
console.log('vfs.name:', vfs.name);
|
|
61
|
+
const reg = sqlite3.vfs_register(vfs, true);
|
|
62
|
+
console.log(4, { reg });
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
// Open database.
|
|
66
|
+
db = await sqlite3.open_v2('foo');
|
|
67
|
+
console.log(5, { db });
|
|
68
|
+
} catch (error) {
|
|
69
|
+
console.error(error);
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
console.log(6, '___START___');
|
|
74
|
+
// Create table.
|
|
75
|
+
await sqlite3.exec(
|
|
76
|
+
db,
|
|
77
|
+
`
|
|
78
|
+
CREATE TABLE IF NOT EXISTS users (
|
|
79
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
80
|
+
name TEXT NOT NULL,
|
|
81
|
+
email TEXT
|
|
82
|
+
)
|
|
83
|
+
`,
|
|
84
|
+
);
|
|
85
|
+
console.log(7, '___CREATE TABLE___');
|
|
86
|
+
// Insert data.
|
|
87
|
+
await sqlite3.exec(db, `INSERT INTO users (name, email) VALUES ('Alice', 'alice@example.com')`);
|
|
88
|
+
await sqlite3.exec(db, `INSERT INTO users (name, email) VALUES ('Bob', 'bob@example.com')`);
|
|
89
|
+
console.log(8, '___INSERT DATA___');
|
|
90
|
+
// Select data using statements API.
|
|
91
|
+
const results: Array<{ id: number; name: string; email: string }> = [];
|
|
92
|
+
for await (const stmt of sqlite3.statements(db, 'SELECT * FROM users ORDER BY id')) {
|
|
93
|
+
let columns: string[] | undefined;
|
|
94
|
+
while ((await sqlite3.step(stmt)) === SQLITE_ROW) {
|
|
95
|
+
columns = columns ?? sqlite3.column_names(stmt);
|
|
96
|
+
const row = sqlite3.row(stmt);
|
|
97
|
+
results.push({
|
|
98
|
+
id: row[0] as number,
|
|
99
|
+
name: row[1] as string,
|
|
100
|
+
email: row[2] as string,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
console.log(9, '___SELECT DATA___');
|
|
105
|
+
console.log(10, { results });
|
|
106
|
+
console.log(11, '___EXPECTATIONS___');
|
|
107
|
+
expect(results).toHaveLength(2);
|
|
108
|
+
console.log(12, '___EXPECTATIONS___');
|
|
109
|
+
expect(results[0].name).toBe('Alice');
|
|
110
|
+
expect(results[0].email).toBe('alice@example.com');
|
|
111
|
+
expect(results[1].name).toBe('Bob');
|
|
112
|
+
});
|
|
113
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, test } from 'vitest';
|
|
6
|
+
|
|
7
|
+
// @ts-expect-error
|
|
8
|
+
import * as WaSqlite from '@dxos/wa-sqlite';
|
|
9
|
+
// @ts-expect-error
|
|
10
|
+
import { SQLITE_OPEN_CREATE, SQLITE_OPEN_READWRITE } from '@dxos/wa-sqlite';
|
|
11
|
+
// @ts-expect-error
|
|
12
|
+
import { SQLITE_ROW } from '@dxos/wa-sqlite';
|
|
13
|
+
// @ts-expect-error
|
|
14
|
+
import SQLiteAsyncESMFactory from '@dxos/wa-sqlite/dist/wa-sqlite-async.mjs';
|
|
15
|
+
|
|
16
|
+
// Resolve WASM URL explicitly for Vite compatibility.
|
|
17
|
+
const wasmUrl = new URL('@dxos/wa-sqlite/dist/wa-sqlite-async.wasm', import.meta.url).href;
|
|
18
|
+
|
|
19
|
+
describe('wa-sqlite in-memory database', () => {
|
|
20
|
+
let sqlite3: ReturnType<typeof WaSqlite.Factory>;
|
|
21
|
+
let db: number;
|
|
22
|
+
|
|
23
|
+
beforeEach(async () => {
|
|
24
|
+
// Initialize async WASM module.
|
|
25
|
+
const module = await SQLiteAsyncESMFactory({
|
|
26
|
+
locateFile: (path: string) => wasmUrl,
|
|
27
|
+
});
|
|
28
|
+
sqlite3 = WaSqlite.Factory(module);
|
|
29
|
+
|
|
30
|
+
// Open in-memory database (no VFS needed).
|
|
31
|
+
db = sqlite3.open_v2(':memory:', SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
afterEach(async () => {
|
|
35
|
+
if (db && sqlite3) {
|
|
36
|
+
sqlite3.close(db);
|
|
37
|
+
db = 0;
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('basic CRUD operations', async () => {
|
|
42
|
+
// Create table.
|
|
43
|
+
sqlite3.exec(
|
|
44
|
+
db,
|
|
45
|
+
`
|
|
46
|
+
CREATE TABLE users (
|
|
47
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
48
|
+
name TEXT NOT NULL,
|
|
49
|
+
email TEXT
|
|
50
|
+
)
|
|
51
|
+
`,
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
// Insert data.
|
|
55
|
+
sqlite3.exec(db, `INSERT INTO users (name, email) VALUES ('Alice', 'alice@example.com')`);
|
|
56
|
+
sqlite3.exec(db, `INSERT INTO users (name, email) VALUES ('Bob', 'bob@example.com')`);
|
|
57
|
+
|
|
58
|
+
// Select data using statements API.
|
|
59
|
+
const results: Array<{ id: number; name: string; email: string }> = [];
|
|
60
|
+
for (const stmt of sqlite3.statements(db, 'SELECT * FROM users ORDER BY id')) {
|
|
61
|
+
let columns: string[] | undefined;
|
|
62
|
+
while (sqlite3.step(stmt) === SQLITE_ROW) {
|
|
63
|
+
columns = columns ?? sqlite3.column_names(stmt);
|
|
64
|
+
const row = sqlite3.row(stmt);
|
|
65
|
+
results.push({
|
|
66
|
+
id: row[0] as number,
|
|
67
|
+
name: row[1] as string,
|
|
68
|
+
email: row[2] as string,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
expect(results).toHaveLength(2);
|
|
74
|
+
expect(results[0].name).toBe('Alice');
|
|
75
|
+
expect(results[0].email).toBe('alice@example.com');
|
|
76
|
+
expect(results[1].name).toBe('Bob');
|
|
77
|
+
});
|
|
78
|
+
});
|