@statewalker/db-sqlite-node 0.1.1
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 +21 -0
- package/README.md +33 -0
- package/dist/index.d.mts +11 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +29 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +50 -0
- package/src/index.ts +2 -0
- package/src/node-sqlite-db.test.ts +147 -0
- package/src/node-sqlite-db.ts +30 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mikhail Kotelnikov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# @statewalker/db-sqlite-node
|
|
2
|
+
|
|
3
|
+
libSQL/SQLite Node.js driver implementing `@statewalker/db-api`, backed by `@libsql/client`.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
pnpm add @statewalker/db-sqlite-node
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { newNodeSqliteDb } from "@statewalker/db-sqlite-node";
|
|
15
|
+
|
|
16
|
+
// In-memory (omit `path`) or file-backed persistence.
|
|
17
|
+
const db = await newNodeSqliteDb({ path: "./data.db" });
|
|
18
|
+
await db.exec("CREATE TABLE t (x INTEGER)");
|
|
19
|
+
await db.query("SELECT * FROM t");
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## API
|
|
23
|
+
|
|
24
|
+
- `newNodeSqliteDb(options?)` — opens a local file-backed (`options.path`) or in-memory libSQL database and returns a `Db`.
|
|
25
|
+
|
|
26
|
+
> Note: the underlying `@libsql/client` also supports remote Turso-cloud connections
|
|
27
|
+
> (`libsql://…turso.io` + `authToken`). That is a libSQL/Turso *product* feature and is
|
|
28
|
+
> out of scope for this package, which targets local/in-memory SQLite.
|
|
29
|
+
|
|
30
|
+
## Related
|
|
31
|
+
|
|
32
|
+
- `@statewalker/db-api` — interface contract.
|
|
33
|
+
- `@statewalker/db-sqlite-browser` — Browser-side counterpart.
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Db, Db as Db$1, DbEntry, DbOptions, DbOptions as DbOptions$1 } from "@statewalker/db-api";
|
|
2
|
+
//#region src/node-sqlite-db.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Create a libSQL-backed {@link Db} using the native Node.js client.
|
|
5
|
+
*
|
|
6
|
+
* @param options.path File path for persistent storage. Omit for in-memory.
|
|
7
|
+
*/
|
|
8
|
+
declare function newNodeSqliteDb(options?: DbOptions$1): Promise<Db$1>;
|
|
9
|
+
//#endregion
|
|
10
|
+
export { type Db, type DbEntry, type DbOptions, newNodeSqliteDb };
|
|
11
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/node-sqlite-db.ts"],"mappings":";;;;;;;iBASsB,gBAAgB,UAAU,cAAY,QAAQ"}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { createClient } from "@libsql/client";
|
|
2
|
+
//#region src/node-sqlite-db.ts
|
|
3
|
+
/**
|
|
4
|
+
* Create a libSQL-backed {@link Db} using the native Node.js client.
|
|
5
|
+
*
|
|
6
|
+
* @param options.path File path for persistent storage. Omit for in-memory.
|
|
7
|
+
*/
|
|
8
|
+
async function newNodeSqliteDb(options) {
|
|
9
|
+
const url = options?.path ? `file:${options.path}` : ":memory:";
|
|
10
|
+
const client = createClient({ url });
|
|
11
|
+
return {
|
|
12
|
+
async query(sql, params) {
|
|
13
|
+
return (await client.execute(params && params.length > 0 ? {
|
|
14
|
+
sql,
|
|
15
|
+
args: params
|
|
16
|
+
} : sql)).rows;
|
|
17
|
+
},
|
|
18
|
+
async exec(sql) {
|
|
19
|
+
await client.execute(sql);
|
|
20
|
+
},
|
|
21
|
+
async close() {
|
|
22
|
+
client.close();
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
//#endregion
|
|
27
|
+
export { newNodeSqliteDb };
|
|
28
|
+
|
|
29
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/node-sqlite-db.ts"],"sourcesContent":["import type { InValue } from \"@libsql/client\";\nimport { createClient } from \"@libsql/client\";\nimport type { Db, DbEntry, DbOptions } from \"@statewalker/db-api\";\n\n/**\n * Create a libSQL-backed {@link Db} using the native Node.js client.\n *\n * @param options.path File path for persistent storage. Omit for in-memory.\n */\nexport async function newNodeSqliteDb(options?: DbOptions): Promise<Db> {\n const url = options?.path ? `file:${options.path}` : \":memory:\";\n const client = createClient({ url });\n\n return {\n async query<T = DbEntry>(sql: string, params?: unknown[]): Promise<T[]> {\n const result = await client.execute(\n params && params.length > 0 ? { sql, args: params as InValue[] } : sql,\n );\n return result.rows as unknown as T[];\n },\n\n async exec(sql: string): Promise<void> {\n await client.execute(sql);\n },\n\n async close(): Promise<void> {\n client.close();\n },\n };\n}\n"],"mappings":";;;;;;;AASA,eAAsB,gBAAgB,SAAkC;CACtE,MAAM,MAAM,SAAS,OAAO,QAAQ,QAAQ,SAAS;CACrD,MAAM,SAAS,aAAa,EAAE,IAAI,CAAC;CAEnC,OAAO;EACL,MAAM,MAAmB,KAAa,QAAkC;GAItE,QAAO,MAHc,OAAO,QAC1B,UAAU,OAAO,SAAS,IAAI;IAAE;IAAK,MAAM;GAAoB,IAAI,GACrE,EAAA,CACc;EAChB;EAEA,MAAM,KAAK,KAA4B;GACrC,MAAM,OAAO,QAAQ,GAAG;EAC1B;EAEA,MAAM,QAAuB;GAC3B,OAAO,MAAM;EACf;CACF;AACF"}
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@statewalker/db-sqlite-node",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "libSQL/SQLite client driver for @statewalker/db-api, targeting Node.js environments.",
|
|
7
|
+
"homepage": "https://github.com/statewalker/statewalker-db",
|
|
8
|
+
"author": {
|
|
9
|
+
"name": "Mikhail Kotelnikov",
|
|
10
|
+
"email": "mikhail.kotelnikov@gmail.com"
|
|
11
|
+
},
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+ssh://git@github.com/statewalker/statewalker-db.git"
|
|
16
|
+
},
|
|
17
|
+
"exports": {
|
|
18
|
+
".": "./src/index.ts"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"src"
|
|
23
|
+
],
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@libsql/client": "^0.17.4",
|
|
26
|
+
"@statewalker/db-api": "0.1.1"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/node": "^26.2.0",
|
|
30
|
+
"rimraf": "^6.1.3",
|
|
31
|
+
"tsdown": "^0.22.14",
|
|
32
|
+
"typescript": "^7.0.2",
|
|
33
|
+
"vitest": "^4.1.10",
|
|
34
|
+
"@statewalker/db-tests": "0.1.1"
|
|
35
|
+
},
|
|
36
|
+
"sideEffects": false,
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "tsdown",
|
|
42
|
+
"dev": "tsdown --watch",
|
|
43
|
+
"test": "vitest run",
|
|
44
|
+
"test:watch": "vitest",
|
|
45
|
+
"typecheck": "tsc --noEmit",
|
|
46
|
+
"clean": "rimraf dist",
|
|
47
|
+
"lint": "biome check --write .",
|
|
48
|
+
"format": "biome format --write ."
|
|
49
|
+
}
|
|
50
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import type { Db } from "@statewalker/db-api";
|
|
2
|
+
import { runDbConformance } from "@statewalker/db-tests";
|
|
3
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
4
|
+
import { newNodeSqliteDb } from "./node-sqlite-db.js";
|
|
5
|
+
|
|
6
|
+
// Common contract (CRUD, parameter binding, cardinality, close, file
|
|
7
|
+
// persistence, error paths) is covered by the shared conformance suite.
|
|
8
|
+
// libsql dialect: `?` placeholder; the spread normalizer drops libsql Row's
|
|
9
|
+
// non-enumerable positional keys + length.
|
|
10
|
+
runDbConformance(newNodeSqliteDb, {
|
|
11
|
+
placeholder: "?",
|
|
12
|
+
normalizeRow: (row) => ({ ...row }),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
// Dialect-specific behavior kept per-adapter: libsql FTS5 + native vector index.
|
|
16
|
+
describe("newNodeSqliteDb", () => {
|
|
17
|
+
let db: Db;
|
|
18
|
+
|
|
19
|
+
afterEach(async () => {
|
|
20
|
+
if (db) {
|
|
21
|
+
await db.close();
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe("FTS5", () => {
|
|
26
|
+
it("returns empty results for unmatched search terms", async () => {
|
|
27
|
+
db = await newNodeSqliteDb();
|
|
28
|
+
await db.exec("CREATE VIRTUAL TABLE docs2_fts USING fts5(content)");
|
|
29
|
+
await db.exec(
|
|
30
|
+
"INSERT INTO docs2_fts (rowid, content) VALUES (1, 'hello world'), (2, 'goodbye world')",
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const rows = await db.query<{ content: string }>(
|
|
34
|
+
"SELECT content FROM docs2_fts WHERE docs2_fts MATCH 'nonexistent'",
|
|
35
|
+
);
|
|
36
|
+
expect(rows).toHaveLength(0);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("searches across multiple text columns", async () => {
|
|
40
|
+
db = await newNodeSqliteDb();
|
|
41
|
+
await db.exec("CREATE VIRTUAL TABLE articles_fts USING fts5(title, body, tokenize='porter')");
|
|
42
|
+
await db.exec(`
|
|
43
|
+
INSERT INTO articles_fts (rowid, title, body) VALUES
|
|
44
|
+
(1, 'Travel Guide', 'Visit the beautiful lakes of Switzerland'),
|
|
45
|
+
(2, 'Cooking Tips', 'How to make a perfect lake trout'),
|
|
46
|
+
(3, 'Programming', 'Learn about SQL databases')
|
|
47
|
+
`);
|
|
48
|
+
|
|
49
|
+
const rows = await db.query<{ rowid: number; title: string; score: number }>(`
|
|
50
|
+
SELECT rowid, title, bm25(articles_fts) AS score
|
|
51
|
+
FROM articles_fts
|
|
52
|
+
WHERE articles_fts MATCH 'lake'
|
|
53
|
+
ORDER BY score
|
|
54
|
+
`);
|
|
55
|
+
expect(rows).toHaveLength(2);
|
|
56
|
+
const titles = rows.map((r) => r.title);
|
|
57
|
+
expect(titles).toContain("Travel Guide");
|
|
58
|
+
expect(titles).toContain("Cooking Tips");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("creates an FTS5 index and performs full-text search", async () => {
|
|
62
|
+
db = await newNodeSqliteDb();
|
|
63
|
+
await db.exec("CREATE VIRTUAL TABLE docs_fts USING fts5(content)");
|
|
64
|
+
await db.exec(`
|
|
65
|
+
INSERT INTO docs_fts (rowid, content) VALUES
|
|
66
|
+
(1, 'the quick brown fox jumps over the lazy dog'),
|
|
67
|
+
(2, 'a lazy cat sleeps on the mat'),
|
|
68
|
+
(3, 'the fox and the hound are friends')
|
|
69
|
+
`);
|
|
70
|
+
|
|
71
|
+
const rows = await db.query<{ rowid: number; content: string; score: number }>(`
|
|
72
|
+
SELECT rowid, content, bm25(docs_fts) AS score
|
|
73
|
+
FROM docs_fts
|
|
74
|
+
WHERE docs_fts MATCH 'fox'
|
|
75
|
+
ORDER BY score
|
|
76
|
+
`);
|
|
77
|
+
|
|
78
|
+
expect(rows.length).toBeGreaterThanOrEqual(1);
|
|
79
|
+
const rowids = rows.map((r) => r.rowid);
|
|
80
|
+
expect(rowids).toContain(1);
|
|
81
|
+
expect(rowids).toContain(3);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe("vector search", () => {
|
|
86
|
+
it("creates index and performs vector similarity search", async () => {
|
|
87
|
+
db = await newNodeSqliteDb();
|
|
88
|
+
|
|
89
|
+
await db.exec("CREATE TABLE embeddings (id INTEGER PRIMARY KEY, vec F32_BLOB(3))");
|
|
90
|
+
await db.exec("CREATE INDEX vec_idx ON embeddings (libsql_vector_idx(vec))");
|
|
91
|
+
await db.exec(`
|
|
92
|
+
INSERT INTO embeddings VALUES
|
|
93
|
+
(1, vector32('[1.0, 0.0, 0.0]')),
|
|
94
|
+
(2, vector32('[0.0, 1.0, 0.0]')),
|
|
95
|
+
(3, vector32('[0.0, 0.0, 1.0]'))
|
|
96
|
+
`);
|
|
97
|
+
|
|
98
|
+
const rows = await db.query<{ id: number }>(
|
|
99
|
+
"SELECT e.id FROM vector_top_k('vec_idx', '[1.0, 0.1, 0.0]', 1) AS v JOIN embeddings e ON e.rowid = v.id",
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
expect(rows).toHaveLength(1);
|
|
103
|
+
expect(rows[0]?.id).toBe(1);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("returns k nearest neighbors ordered by distance", async () => {
|
|
107
|
+
db = await newNodeSqliteDb();
|
|
108
|
+
|
|
109
|
+
await db.exec("CREATE TABLE vectors (id INTEGER PRIMARY KEY, vec F32_BLOB(3))");
|
|
110
|
+
await db.exec("CREATE INDEX vec_idx2 ON vectors (libsql_vector_idx(vec))");
|
|
111
|
+
await db.exec(`
|
|
112
|
+
INSERT INTO vectors VALUES
|
|
113
|
+
(1, vector32('[1.0, 0.0, 0.0]')),
|
|
114
|
+
(2, vector32('[0.9, 0.1, 0.0]')),
|
|
115
|
+
(3, vector32('[0.0, 1.0, 0.0]')),
|
|
116
|
+
(4, vector32('[0.0, 0.0, 1.0]'))
|
|
117
|
+
`);
|
|
118
|
+
|
|
119
|
+
const rows = await db.query<{ id: number }>(
|
|
120
|
+
"SELECT e.id FROM vector_top_k('vec_idx2', '[1.0, 0.0, 0.0]', 2) AS v JOIN vectors e ON e.rowid = v.id",
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
expect(rows).toHaveLength(2);
|
|
124
|
+
const ids = rows.map((r) => r.id);
|
|
125
|
+
expect(ids).toContain(1);
|
|
126
|
+
expect(ids).toContain(2);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("works without index (brute-force scan)", async () => {
|
|
130
|
+
db = await newNodeSqliteDb();
|
|
131
|
+
|
|
132
|
+
await db.exec("CREATE TABLE vecs_no_idx (id INTEGER PRIMARY KEY, vec F32_BLOB(3))");
|
|
133
|
+
await db.exec(`
|
|
134
|
+
INSERT INTO vecs_no_idx VALUES
|
|
135
|
+
(1, vector32('[1.0, 0.0, 0.0]')),
|
|
136
|
+
(2, vector32('[0.0, 1.0, 0.0]'))
|
|
137
|
+
`);
|
|
138
|
+
|
|
139
|
+
const rows = await db.query<{ id: number }>(
|
|
140
|
+
"SELECT id FROM vecs_no_idx ORDER BY vector_distance_cos(vec, vector32('[0.0, 0.9, 0.1]')) LIMIT 1",
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
expect(rows).toHaveLength(1);
|
|
144
|
+
expect(rows[0]?.id).toBe(2);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { InValue } from "@libsql/client";
|
|
2
|
+
import { createClient } from "@libsql/client";
|
|
3
|
+
import type { Db, DbEntry, DbOptions } from "@statewalker/db-api";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Create a libSQL-backed {@link Db} using the native Node.js client.
|
|
7
|
+
*
|
|
8
|
+
* @param options.path File path for persistent storage. Omit for in-memory.
|
|
9
|
+
*/
|
|
10
|
+
export async function newNodeSqliteDb(options?: DbOptions): Promise<Db> {
|
|
11
|
+
const url = options?.path ? `file:${options.path}` : ":memory:";
|
|
12
|
+
const client = createClient({ url });
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
async query<T = DbEntry>(sql: string, params?: unknown[]): Promise<T[]> {
|
|
16
|
+
const result = await client.execute(
|
|
17
|
+
params && params.length > 0 ? { sql, args: params as InValue[] } : sql,
|
|
18
|
+
);
|
|
19
|
+
return result.rows as unknown as T[];
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
async exec(sql: string): Promise<void> {
|
|
23
|
+
await client.execute(sql);
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
async close(): Promise<void> {
|
|
27
|
+
client.close();
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|