Skip to content

Commit 299e82e

Browse files
committed
instance cache in api
1 parent bfab8fa commit 299e82e

File tree

6 files changed

+158
-13
lines changed

6 files changed

+158
-13
lines changed

api/pkgs/@duckdb/node-api/README.md

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,17 @@ console.log(duckdb.version());
4545
console.log(duckdb.configurationOptionDescriptions());
4646
```
4747

48+
### Connect
49+
50+
```ts
51+
import { DuckDBConnection } from '@duckdb/node-api';
52+
53+
const connection = await DuckDBConnection.create();
54+
```
55+
56+
This uses the default instance.
57+
For advanced usage, you can create instances explicitly.
58+
4859
### Create Instance
4960

5061
```ts
@@ -66,14 +77,33 @@ Read from and write to a database file, which is created if needed:
6677
const instance = await DuckDBInstance.create('my_duckdb.db');
6778
```
6879

69-
Set configuration options:
80+
Set [configuration options](https://duckdb.org/docs/stable/configuration/overview.html#configuration-reference):
7081
```ts
7182
const instance = await DuckDBInstance.create('my_duckdb.db', {
7283
threads: '4'
7384
});
7485
```
7586

76-
### Connect
87+
### Instance Cache
88+
89+
Multiple instances in the same process should not
90+
attach the same database.
91+
92+
To prevent this, an instance cache can be used:
93+
```ts
94+
const instance = await DuckDBInstance.fromCache('my_duckdb.db');
95+
```
96+
97+
This uses the default instance cache. For advanced usage, you can create
98+
instance caches explicitly:
99+
```ts
100+
import { DuckDBInstanceCache } from '@duckdb/node-api';
101+
102+
const cache = new DuckDBInstanceCache();
103+
const instance = await cache.getOrCreateInstance('my_duckdb.db');
104+
```
105+
106+
### Connect to Instance
77107

78108
```ts
79109
const connection = await instance.connect();

api/src/DuckDBConnection.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,12 @@ export class DuckDBConnection {
1616
this.connection = connection;
1717
}
1818
public static async create(
19-
instance: DuckDBInstance
19+
instance?: DuckDBInstance
2020
): Promise<DuckDBConnection> {
21-
return instance.connect();
21+
if (instance) {
22+
return instance.connect();
23+
}
24+
return (await DuckDBInstance.fromCache()).connect();
2225
}
2326
/** Same as disconnect. */
2427
public close() {

api/src/DuckDBInstance.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,30 @@
11
import duckdb from '@duckdb/node-bindings';
2+
import { createConfig } from './createConfig';
23
import { DuckDBConnection } from './DuckDBConnection';
4+
import { DuckDBInstanceCache } from './DuckDBInstanceCache';
35

46
export class DuckDBInstance {
57
private readonly db: duckdb.Database;
8+
69
constructor(db: duckdb.Database) {
710
this.db = db;
811
}
12+
913
public static async create(
1014
path?: string,
1115
options?: Record<string, string>
1216
): Promise<DuckDBInstance> {
13-
const config = duckdb.create_config();
14-
// Set the default duckdb_api value for the api. Can be overridden.
15-
duckdb.set_config(config, 'duckdb_api', 'node-neo-api');
16-
if (options) {
17-
for (const optionName in options) {
18-
const optionValue = String(options[optionName]);
19-
duckdb.set_config(config, optionName, optionValue);
20-
}
21-
}
17+
const config = createConfig(options);
2218
return new DuckDBInstance(await duckdb.open(path, config));
2319
}
20+
21+
public static async fromCache(
22+
path?: string,
23+
options?: Record<string, string>
24+
): Promise<DuckDBInstance> {
25+
return DuckDBInstanceCache.singleton.getOrCreateInstance(path, options);
26+
}
27+
2428
public async connect(): Promise<DuckDBConnection> {
2529
return new DuckDBConnection(await duckdb.connect(this.db));
2630
}

api/src/DuckDBInstanceCache.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import duckdb from '@duckdb/node-bindings';
2+
import { DuckDBInstance } from './DuckDBInstance';
3+
import { createConfig } from './createConfig';
4+
5+
export class DuckDBInstanceCache {
6+
private readonly cache: duckdb.InstanceCache;
7+
8+
constructor() {
9+
this.cache = duckdb.create_instance_cache();
10+
}
11+
12+
public async getOrCreateInstance(
13+
path?: string,
14+
options?: Record<string, string>
15+
): Promise<DuckDBInstance> {
16+
const config = createConfig(options);
17+
const db = await duckdb.get_or_create_from_cache(this.cache, path, config);
18+
return new DuckDBInstance(db);
19+
}
20+
21+
private static singletonInstance: DuckDBInstanceCache;
22+
23+
public static get singleton(): DuckDBInstanceCache {
24+
if (!DuckDBInstanceCache.singletonInstance) {
25+
DuckDBInstanceCache.singletonInstance = new DuckDBInstanceCache();
26+
}
27+
return DuckDBInstanceCache.singletonInstance;
28+
}
29+
}

api/src/createConfig.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import duckdb from '@duckdb/node-bindings';
2+
3+
export function createConfig(options?: Record<string, string>): duckdb.Config {
4+
const config = duckdb.create_config();
5+
// Set the default duckdb_api value for the api. Can be overridden.
6+
duckdb.set_config(config, 'duckdb_api', 'node-neo-api');
7+
if (options) {
8+
for (const optionName in options) {
9+
const optionValue = String(options[optionName]);
10+
duckdb.set_config(config, optionName, optionValue);
11+
}
12+
}
13+
return config;
14+
}

api/test/api.test.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import fs from 'fs';
12
import { assert, beforeAll, describe, test } from 'vitest';
23
import {
34
ANY,
@@ -111,6 +112,7 @@ import {
111112
uuidValue,
112113
version,
113114
} from '../src';
115+
import { DuckDBInstanceCache } from '../src/DuckDBInstanceCache';
114116
import { replaceSqlNullWithInteger } from './util/replaceSqlNullWithInteger';
115117
import {
116118
ColumnNameAndType,
@@ -1333,6 +1335,69 @@ describe('api', () => {
13331335
]);
13341336
}
13351337
});
1338+
test('instance cache - same instance', async () => {
1339+
const cache = new DuckDBInstanceCache();
1340+
const instance1 = await cache.getOrCreateInstance();
1341+
const connection1 = await instance1.connect();
1342+
await connection1.run(`attach ':memory:' as mem1`);
1343+
1344+
const instance2 = await cache.getOrCreateInstance();
1345+
const connection2 = await instance2.connect();
1346+
await connection2.run(`create table mem1.main.t1 as select 1`);
1347+
});
1348+
test('instance cache - different instances', async () => {
1349+
try {
1350+
const cache = new DuckDBInstanceCache();
1351+
const instance1 = await cache.getOrCreateInstance(
1352+
'instance_cache_test_a.db'
1353+
);
1354+
const connection1 = await instance1.connect();
1355+
await connection1.run(`attach ':memory:' as mem1`);
1356+
1357+
const instance2 = await cache.getOrCreateInstance(
1358+
'instance_cache_test_b.db'
1359+
);
1360+
const connection2 = await instance2.connect();
1361+
try {
1362+
await connection2.run(`create table mem1.main.t1 as select 1`);
1363+
assert.fail('should throw');
1364+
} catch (err) {
1365+
assert.deepEqual(
1366+
err,
1367+
new Error(`Catalog Error: Catalog with name mem1 does not exist!`)
1368+
);
1369+
}
1370+
} finally {
1371+
fs.rmSync('instance_cache_test_a.db');
1372+
fs.rmSync('instance_cache_test_b.db');
1373+
}
1374+
});
1375+
test('instance cache - different config', async () => {
1376+
const cache = new DuckDBInstanceCache();
1377+
const instance1 = await cache.getOrCreateInstance();
1378+
const connection1 = await instance1.connect();
1379+
await connection1.run(`attach ':memory:' as mem1`);
1380+
try {
1381+
await cache.getOrCreateInstance(undefined, { accces_mode: 'READ_ONLY' });
1382+
assert.fail('should throw');
1383+
} catch (err) {
1384+
assert.deepEqual(
1385+
err,
1386+
new Error(
1387+
`Connection Error: Can't open a connection to same database file with a different configuration than existing connections`
1388+
)
1389+
);
1390+
}
1391+
});
1392+
test('instance cache - singleton', async () => {
1393+
const instance = await DuckDBInstance.fromCache();
1394+
const connection = await instance.connect();
1395+
await connection.run('select 1');
1396+
});
1397+
test('create connection using instance cache', async () => {
1398+
const connection = await DuckDBConnection.create();
1399+
await connection.run('select 1');
1400+
});
13361401
test('write integer vector', () => {
13371402
const chunk = DuckDBDataChunk.create([INTEGER], 3);
13381403
const vector = chunk.getColumnVector(0) as DuckDBIntegerVector;

0 commit comments

Comments
 (0)