Skip to content

Commit b455e4f

Browse files
committed
Add clone() support, fix batch error strings, bump to v0.3.7
- Add Database.clone() for shared-engine multi-handle access - Fix use-after-free on batch error strings (ERR_SAVE macro) - Add 6 clone tests (shared data, bidirectional writes, tx, stmt) - Update README with clone documentation - Bump version to 0.3.7
1 parent 7acbdc5 commit b455e4f

14 files changed

Lines changed: 148 additions & 20 deletions

File tree

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ concurrency:
1212
cancel-in-progress: true
1313

1414
env:
15-
STOOLAP_ENGINE_REF: v0.3.5
15+
STOOLAP_ENGINE_REF: v0.3.7
1616

1717
jobs:
1818
build:

.github/workflows/publish.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ permissions:
99
id-token: write
1010

1111
env:
12-
STOOLAP_ENGINE_REF: v0.3.5
12+
STOOLAP_ENGINE_REF: v0.3.7
1313

1414
jobs:
1515
build:

README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ Sync methods run on the main thread. Faster for simple operations but block the
108108
| Method | Returns | Description |
109109
|--------|---------|-------------|
110110
| `Database.openSync(path)` | `Database` | Open a database |
111+
| `clone()` | `Database` | Clone handle (shared engine, own state) |
111112
| `executeSync(sql, params?)` | `RunResult` | Execute DML statement |
112113
| `execSync(sql)` | `void` | Execute a DDL statement |
113114
| `querySync(sql, params?)` | `Object[]` | Query rows as objects |
@@ -189,6 +190,24 @@ Controls the durability vs. performance trade-off:
189190
| `compression` | | Set both `wal_compression` and `snapshot_compression` |
190191
| `compression_threshold` | `64` | Minimum bytes before compressing an entry |
191192

193+
#### Cloning
194+
195+
`clone()` creates a new `Database` handle that shares the same underlying engine (data, indexes, transactions) but has its own executor and error state. Useful for concurrent access patterns such as worker threads.
196+
197+
```js
198+
const db = await Database.open('./mydata');
199+
const db2 = db.clone();
200+
201+
// Both see the same data
202+
await db.execute('INSERT INTO users VALUES ($1, $2)', [1, 'Alice']);
203+
const row = db2.queryOneSync('SELECT * FROM users WHERE id = $1', [1]);
204+
// { id: 1, name: 'Alice' }
205+
206+
// Each clone must be closed independently
207+
await db2.close();
208+
await db.close();
209+
```
210+
192211
#### Raw Query Format
193212

194213
`queryRaw` / `queryRawSync` return `{ columns: string[], rows: any[][] }` instead of an array of objects. Faster when you don't need named keys.

__test__/index.spec.mjs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1641,6 +1641,76 @@ describe('Vector support', () => {
16411641
});
16421642
});
16431643

1644+
// ============================================================
1645+
// Clone
1646+
// ============================================================
1647+
1648+
describe('Database clone', () => {
1649+
let db;
1650+
1651+
before(async () => {
1652+
db = await Database.open(':memory:');
1653+
await db.exec('CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT)');
1654+
await db.execute('INSERT INTO items VALUES ($1, $2)', [1, 'Alpha']);
1655+
await db.execute('INSERT INTO items VALUES ($1, $2)', [2, 'Beta']);
1656+
});
1657+
1658+
after(async () => {
1659+
await db.close();
1660+
});
1661+
1662+
it('should clone a database handle and share data', async () => {
1663+
const db2 = db.clone();
1664+
const rows = db2.querySync('SELECT * FROM items ORDER BY id');
1665+
assert.equal(rows.length, 2);
1666+
assert.equal(rows[0].name, 'Alpha');
1667+
assert.equal(rows[1].name, 'Beta');
1668+
await db2.close();
1669+
});
1670+
1671+
it('should see writes from the original in the clone', async () => {
1672+
const db2 = db.clone();
1673+
await db.execute('INSERT INTO items VALUES ($1, $2)', [3, 'Gamma']);
1674+
const row = db2.queryOneSync('SELECT * FROM items WHERE id = $1', [3]);
1675+
assert.equal(row.name, 'Gamma');
1676+
await db2.close();
1677+
});
1678+
1679+
it('should see writes from the clone in the original', async () => {
1680+
const db2 = db.clone();
1681+
db2.executeSync('INSERT INTO items VALUES ($1, $2)', [4, 'Delta']);
1682+
const row = await db.queryOne('SELECT * FROM items WHERE id = $1', [4]);
1683+
assert.equal(row.name, 'Delta');
1684+
await db2.close();
1685+
});
1686+
1687+
it('should close independently without affecting the original', async () => {
1688+
const db2 = db.clone();
1689+
await db2.close();
1690+
const rows = db.querySync('SELECT * FROM items ORDER BY id');
1691+
assert.ok(rows.length > 0);
1692+
});
1693+
1694+
it('should support transactions on cloned handle', async () => {
1695+
const db2 = db.clone();
1696+
const tx = db2.beginSync();
1697+
tx.executeSync('INSERT INTO items VALUES ($1, $2)', [5, 'Epsilon']);
1698+
tx.commitSync();
1699+
const row = db2.queryOneSync('SELECT * FROM items WHERE id = $1', [5]);
1700+
assert.equal(row.name, 'Epsilon');
1701+
await db2.close();
1702+
});
1703+
1704+
it('should support prepared statements on cloned handle', async () => {
1705+
const db2 = db.clone();
1706+
const stmt = db2.prepare('SELECT * FROM items WHERE id = $1');
1707+
const row = stmt.queryOneSync([1]);
1708+
assert.equal(row.name, 'Alpha');
1709+
stmt.finalize();
1710+
await db2.close();
1711+
});
1712+
});
1713+
16441714
// ============================================================
16451715
// Declaration coverage
16461716
// ============================================================

index.d.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ export declare class Database {
1717
* Open a database synchronously.
1818
*/
1919
static openSync(path: string): Database;
20+
/**
21+
* Clone this database handle. The clone shares the same underlying engine
22+
* (data, indexes, transactions) but has its own executor and error state.
23+
* Each clone must be closed independently.
24+
*/
25+
clone(): Database;
2026
/**
2127
* Execute a DDL/DML statement. Returns Promise<{ changes: number }>.
2228
*

lib/database.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,14 @@ class Database {
128128
return this.#stoolap.dbExecBatchBuf(this.#ptr, sql, batchBuf);
129129
}
130130

131+
// -- Clone --
132+
133+
clone() {
134+
this.#checkOpen();
135+
const clonedPtr = this.#stoolap.dbClone(this.#ptr);
136+
return new Database(clonedPtr, this.#stoolap);
137+
}
138+
131139
// -- Execute (DML) --
132140

133141
async execute(sql, params) {

npm/darwin-arm64/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@stoolap/lib-darwin-arm64",
3-
"version": "0.3.5",
3+
"version": "0.3.7",
44
"main": "libstoolap.dylib",
55
"files": [
66
"libstoolap.dylib"

npm/darwin-x64/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@stoolap/lib-darwin-x64",
3-
"version": "0.3.5",
3+
"version": "0.3.7",
44
"main": "libstoolap.dylib",
55
"files": [
66
"libstoolap.dylib"

npm/linux-arm64-gnu/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@stoolap/lib-linux-arm64-gnu",
3-
"version": "0.3.5",
3+
"version": "0.3.7",
44
"main": "libstoolap.so",
55
"files": [
66
"libstoolap.so"

npm/linux-x64-gnu/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@stoolap/lib-linux-x64-gnu",
3-
"version": "0.3.5",
3+
"version": "0.3.7",
44
"main": "libstoolap.so",
55
"files": [
66
"libstoolap.so"

0 commit comments

Comments
 (0)