Skip to content

Commit 1bbe697

Browse files
committed
feat: implement unregisterFile method
Add queryToJSON method to DuckDB ConnectionHelper class Bump Versions Signed-off-by: Gordon Smith <GordonJSmith@gmail.com>
1 parent 11395f5 commit 1bbe697

File tree

16 files changed

+1549
-1390
lines changed

16 files changed

+1549
-1390
lines changed

package-lock.json

Lines changed: 1017 additions & 1361 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -87,31 +87,31 @@
8787
"devDependencies": {
8888
"@eslint/js": "9.39.2",
8989
"@types/emscripten": "1.41.5",
90-
"@types/node": "24.10.4",
90+
"@types/node": "24.10.7",
9191
"@types/yargs": "17.0.35",
92-
"@typescript-eslint/parser": "8.49.0",
93-
"@vitest/browser": "4.0.15",
94-
"@vitest/browser-playwright": "4.0.15",
95-
"@vitest/coverage-istanbul": "4.0.15",
96-
"@vitest/coverage-v8": "4.0.15",
92+
"@typescript-eslint/parser": "8.53.0",
93+
"@vitest/browser": "4.0.17",
94+
"@vitest/browser-playwright": "4.0.17",
95+
"@vitest/coverage-istanbul": "4.0.17",
96+
"@vitest/coverage-v8": "4.0.17",
9797
"assemblyscript": "0.28.9",
9898
"chokidar-cli": "3.0.0",
9999
"eslint": "9.39.2",
100100
"globals": "16.5.0",
101-
"happy-dom": "20.0.11",
101+
"happy-dom": "20.1.0",
102102
"lerna": "9.0.3",
103103
"npm-run-all": "4.1.5",
104104
"playwright": "1.57.0",
105105
"release-please": "17.1.3",
106106
"rimraf": "6.1.2",
107107
"run-script-os": "1.1.6",
108108
"tslib": "2.8.1",
109-
"typedoc": "0.28.15",
109+
"typedoc": "0.28.16",
110110
"typedoc-plugin-markdown": "4.9.0",
111111
"typescript": "5.9.3",
112-
"typescript-eslint": "8.49.0",
112+
"typescript-eslint": "8.53.0",
113113
"vitepress": "1.6.4",
114-
"vitest": "4.0.15"
114+
"vitest": "4.0.17"
115115
},
116116
"c8": {
117117
"exclude-after-remap": []

packages/base91/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
},
4040
"dependencies": {},
4141
"devDependencies": {
42-
"@hpcc-js/esbuild-plugins": "1.7.0",
42+
"@hpcc-js/esbuild-plugins": "1.8.0",
4343
"@hpcc-js/wasm-util": "1.0.0"
4444
},
4545
"keywords": [

packages/duckdb/README.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,24 @@ const progress = connection.getQueryProgress();
133133
console.log(`Query is ${progress * 100}% complete`);
134134
```
135135

136+
##### `queryToJSON(sql: string): string`
137+
Executes a SQL query and returns the result directly as a JSON string. This is a convenience method that combines `query()` and `toJSON()` into a single call.
138+
139+
```typescript
140+
const json = connection.queryToJSON("SELECT * FROM users WHERE age > 18");
141+
// Returns: '[{"id":1,"name":"Alice","age":30},{"id":2,"name":"Bob","age":25}]'
142+
143+
const users = JSON.parse(json);
144+
console.log(users[0].name); // "Alice"
145+
```
146+
147+
**Features:**
148+
- No need to manually delete the result (automatic cleanup)
149+
- Returns an empty array `[]` for queries with no results
150+
- Handles all data types including NULL, numbers, strings, and booleans
151+
- Proper JSON escaping for special characters
152+
- NaN and Infinity are converted to `null`
153+
136154
#### Transaction Management
137155

138156
##### `beginTransaction(): void`
@@ -549,6 +567,34 @@ users.forEach(user => {
549567
result.delete();
550568
```
551569

570+
### Direct JSON Query (queryToJSON)
571+
572+
```typescript
573+
const connection = duckdb.connect();
574+
575+
// Convenience method: execute query and get JSON in one call
576+
const json = connection.queryToJSON("SELECT * FROM users ORDER BY age");
577+
578+
// No need to delete result - it's automatic!
579+
const users = JSON.parse(json);
580+
users.forEach(user => {
581+
console.log(`${user.name} is ${user.age} years old`);
582+
});
583+
584+
// Use with aggregations
585+
const stats = connection.queryToJSON(`
586+
SELECT
587+
department,
588+
COUNT(*) as count,
589+
AVG(salary) as avg_salary
590+
FROM employees
591+
GROUP BY department
592+
`);
593+
console.log(stats);
594+
595+
connection.delete();
596+
```
597+
552598
### Error Handling
553599

554600
```typescript

packages/duckdb/TOJSON.md

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,51 @@
1-
# toJSON Method for MaterializedQueryResult
1+
# JSON Methods for DuckDB Results
22

3-
The `toJSON()` method has been added to `MaterializedQueryResult` to convert query results directly to a JSON string.
3+
This package provides two methods for converting query results to JSON:
44

5-
## Usage Example
5+
1. `MaterializedQueryResult.toJSON()` - Convert an existing result to JSON
6+
2. `Connection.queryToJSON(sql)` - Execute a query and get JSON in one call (recommended)
7+
8+
## Usage Examples
9+
10+
### Using queryToJSON (Recommended)
611

712
```javascript
813
import { DuckDB } from '@hpcc-js/wasm-duckdb';
914

1015
const db = await DuckDB.load();
11-
const connection = await db.connect();
16+
const connection = db.connect();
17+
18+
// Execute query and get JSON in one call
19+
const jsonString = connection.queryToJSON(`
20+
SELECT
21+
id,
22+
name,
23+
age
24+
FROM users
25+
WHERE age > 18
26+
`);
27+
28+
console.log(jsonString);
29+
// Output: [{"id":1,"name":"Alice","age":25},{"id":2,"name":"Bob","age":30}]
30+
31+
// Parse back to JavaScript objects
32+
const data = JSON.parse(jsonString);
33+
console.log(data);
34+
// Output: [ { id: 1, name: 'Alice', age: 25 }, { id: 2, name: 'Bob', age: 30 } ]
35+
36+
// No need to call result.delete() - it's handled automatically!
37+
```
38+
39+
### Using toJSON on MaterializedQueryResult
40+
41+
```javascript
42+
import { DuckDB } from '@hpcc-js/wasm-duckdb';
43+
44+
const db = await DuckDB.load();
45+
const connection = db.connect();
1246

1347
// Execute a query
14-
const result = await connection.query(`
48+
const result = connection.query(`
1549
SELECT
1650
id,
1751
name,
@@ -25,15 +59,31 @@ const jsonString = result.toJSON();
2559
console.log(jsonString);
2660
// Output: [{"id":1,"name":"Alice","age":25},{"id":2,"name":"Bob","age":30}]
2761

62+
// Must manually clean up
63+
result.delete();
64+
2865
// Can be parsed back to JavaScript objects
2966
const data = JSON.parse(jsonString);
3067
console.log(data);
3168
// Output: [ { id: 1, name: 'Alice', age: 25 }, { id: 2, name: 'Bob', age: 30 } ]
3269
```
3370

71+
### When to Use Which Method
72+
73+
**Use `queryToJSON()`** when:
74+
- You only need JSON output (most common case)
75+
- You want cleaner code with automatic cleanup
76+
- You're building APIs or data pipelines
77+
78+
**Use `query()` + `toJSON()`** when:
79+
- You need to access individual values with `getValue()`
80+
- You need result metadata (row count, column types, etc.)
81+
- You need to perform multiple operations on the result
82+
3483
## Features
3584

3685
- **Efficient JSON Encoding**: Converts entire result sets to JSON in C++ for better performance
86+
- **Automatic Memory Management**: `queryToJSON()` handles cleanup automatically
3787
- **Proper Type Handling**:
3888
- Numbers (integers and floats)
3989
- Strings (with proper escaping)
@@ -42,6 +92,7 @@ console.log(data);
4292
- **Special Character Escaping**: Handles newlines, quotes, backslashes, tabs, and other special characters
4393
- **Multiple Rows**: Returns an array of objects, one per row
4494
- **Column Names**: Uses column names as JSON object keys
95+
- **Empty Results**: Returns `[]` for queries with no results
4596

4697
## Implementation Details
4798

packages/duckdb/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
},
4040
"dependencies": {},
4141
"devDependencies": {
42-
"@hpcc-js/esbuild-plugins": "1.7.0",
42+
"@hpcc-js/esbuild-plugins": "1.8.0",
4343
"@hpcc-js/wasm-util": "1.0.0"
4444
},
4545
"keywords": [

packages/duckdb/src-cpp/main.cpp

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -334,8 +334,17 @@ namespace ConnectionHelper
334334
{
335335
return obj.Query(query).release();
336336
}
337-
}
338337

338+
std::string queryToJSON(Connection &obj, const string &query)
339+
{
340+
auto result = obj.Query(query);
341+
if (!result->HasError())
342+
{
343+
return MaterializedQueryResultHelper::toJSON(*result);
344+
}
345+
return std::string();
346+
}
347+
}
339348
namespace DuckDBHelper
340349
{
341350
DuckDB *create()
@@ -476,6 +485,7 @@ EMSCRIPTEN_BINDINGS(duckdblib_bindings)
476485
class_<Connection>("Connection")
477486
.function("prepare", &ConnectionHelper::prepare, return_value_policy::take_ownership())
478487
.function("query", &ConnectionHelper::query, return_value_policy::take_ownership())
488+
.function("queryToJSON", &ConnectionHelper::queryToJSON)
479489
.function("interrupt", &Connection::Interrupt)
480490
.function("getQueryProgress", &Connection::GetQueryProgress)
481491
// .function("enableProfiling", &Connection::EnableProfiling)

packages/duckdb/src/duckdb.ts

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -229,13 +229,61 @@ export class DuckDB extends MainModuleEx<MainModule> {
229229
registerFile(path: string, content: Uint8Array): void {
230230
const normalizedPath = path.startsWith("/") ? path.slice(1) : path;
231231
const split = normalizedPath.lastIndexOf("/");
232-
const dir = split > 0 ? normalizedPath.substring(0, split) : "/";
233-
if (dir.length > 1 && this._module.FS_createPath) {
234-
this._module.FS_createPath(dir, true, true);
232+
if (split > 0) {
233+
const dir = normalizedPath.substring(0, split);
234+
const parts = dir.split("/");
235+
let currentPath = "/";
236+
for (const part of parts) {
237+
if (part && this._module.FS_createPath) {
238+
this._module.FS_createPath(currentPath, part, true, true);
239+
currentPath = currentPath === "/" ? "/" + part : currentPath + "/" + part;
240+
}
241+
}
235242
}
236243
this._module.FS_createDataFile(normalizedPath, undefined, content, true, true, true);
237244
}
238245

246+
/**
247+
* Unregisters and removes a file from the virtual file system.
248+
*
249+
* This removes a file that was previously registered using {@link registerFile} or
250+
* {@link registerFileString}. Once unregistered, the file will no longer be accessible
251+
* to DuckDB queries.
252+
*
253+
* The path is normalized to remove leading slashes before removal.
254+
*
255+
* @param path - The path of the file to remove (e.g., "data/users.csv")
256+
*
257+
* @example
258+
* ```ts
259+
* const duckdb = await DuckDB.load();
260+
*
261+
* // Register a file
262+
* duckdb.registerFileString("temp.csv", "id,name\n1,Alice");
263+
*
264+
* const connection = duckdb.connect();
265+
* let result = connection.query("SELECT * FROM read_csv_auto('temp.csv')");
266+
* console.log(result.rowCount()); // 1
267+
* result.delete();
268+
*
269+
* // Unregister the file
270+
* duckdb.unregisterFile("temp.csv");
271+
*
272+
* // File is no longer accessible
273+
* try {
274+
* result = connection.query("SELECT * FROM read_csv_auto('temp.csv')");
275+
* } catch (error) {
276+
* console.error("File not found"); // Error: file not accessible
277+
* }
278+
*
279+
* connection.delete();
280+
* ```
281+
*/
282+
unregisterFile(path: string): void {
283+
const normalizedPath = path.startsWith("/") ? path.slice(1) : path;
284+
this._module.FS_unlink(normalizedPath);
285+
}
286+
239287
/**
240288
* Registers a text file in the virtual file system.
241289
*

0 commit comments

Comments
 (0)