Skip to content

Commit 2d90889

Browse files
authored
Add Node.js close bindings for query results and connections (#3436)
* Add Node.js close bindings for query results and connections * Run clang-format --------- Co-authored-by: CI Bot <mewim@users.noreply.github.com>
1 parent 24c7c29 commit 2d90889

9 files changed

Lines changed: 206 additions & 68 deletions

File tree

src_cpp/include/node_connection.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ class NodeConnection : public Napi::ObjectWrap<NodeConnection> {
2929
void SetQueryTimeout(const Napi::CallbackInfo& info);
3030
Napi::Value ExecuteAsync(const Napi::CallbackInfo& info);
3131
Napi::Value QueryAsync(const Napi::CallbackInfo& info);
32+
void Close(const Napi::CallbackInfo& info);
3233

3334
private:
3435
std::shared_ptr<Database> database;

src_cpp/include/node_query_result.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ class NodeQueryResult : public Napi::ObjectWrap<NodeQueryResult> {
3030
Napi::Value GetNextAsync(const Napi::CallbackInfo& info);
3131
Napi::Value GetColumnDataTypesAsync(const Napi::CallbackInfo& info);
3232
Napi::Value GetColumnNamesAsync(const Napi::CallbackInfo& info);
33+
void Close(const Napi::CallbackInfo& info);
34+
void Close();
3335

3436
private:
3537
QueryResult* queryResult = nullptr;

src_cpp/node_connection.cpp

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,12 @@ Napi::Object NodeConnection::Init(Napi::Env env, Napi::Object exports) {
1111
Napi::HandleScope scope(env);
1212

1313
Napi::Function t = DefineClass(env, "NodeConnection",
14-
{
15-
InstanceMethod("initAsync", &NodeConnection::InitAsync),
14+
{InstanceMethod("initAsync", &NodeConnection::InitAsync),
1615
InstanceMethod("executeAsync", &NodeConnection::ExecuteAsync),
1716
InstanceMethod("queryAsync", &NodeConnection::QueryAsync),
1817
InstanceMethod("setMaxNumThreadForExec", &NodeConnection::SetMaxNumThreadForExec),
1918
InstanceMethod("setQueryTimeout", &NodeConnection::SetQueryTimeout),
20-
});
19+
InstanceMethod("close", &NodeConnection::Close)});
2120

2221
exports.Set("NodeConnection", t);
2322
return exports;
@@ -68,6 +67,12 @@ void NodeConnection::SetQueryTimeout(const Napi::CallbackInfo& info) {
6867
}
6968
}
7069

70+
void NodeConnection::Close(const Napi::CallbackInfo& info) {
71+
Napi::Env env = info.Env();
72+
Napi::HandleScope scope(env);
73+
this->connection.reset();
74+
}
75+
7176
Napi::Value NodeConnection::ExecuteAsync(const Napi::CallbackInfo& info) {
7277
Napi::Env env = info.Env();
7378
Napi::HandleScope scope(env);

src_cpp/node_query_result.cpp

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,15 @@ Napi::Object NodeQueryResult::Init(Napi::Env env, Napi::Object exports) {
1111
Napi::HandleScope scope(env);
1212

1313
Napi::Function t = DefineClass(env, "NodeQueryResult",
14-
{
15-
InstanceMethod("resetIterator", &NodeQueryResult::ResetIterator),
14+
{InstanceMethod("resetIterator", &NodeQueryResult::ResetIterator),
1615
InstanceMethod("hasNext", &NodeQueryResult::HasNext),
1716
InstanceMethod("hasNextQueryResult", &NodeQueryResult::HasNextQueryResult),
1817
InstanceMethod("getNextQueryResultAsync", &NodeQueryResult::GetNextQueryResultAsync),
1918
InstanceMethod("getNumTuples", &NodeQueryResult::GetNumTuples),
2019
InstanceMethod("getNextAsync", &NodeQueryResult::GetNextAsync),
2120
InstanceMethod("getColumnDataTypesAsync", &NodeQueryResult::GetColumnDataTypesAsync),
2221
InstanceMethod("getColumnNamesAsync", &NodeQueryResult::GetColumnNamesAsync),
23-
});
22+
InstanceMethod("close", &NodeQueryResult::Close)});
2423

2524
exports.Set("NodeQueryResult", t);
2625
return exports;
@@ -30,10 +29,7 @@ NodeQueryResult::NodeQueryResult(const Napi::CallbackInfo& info)
3029
: Napi::ObjectWrap<NodeQueryResult>(info) {}
3130

3231
NodeQueryResult::~NodeQueryResult() {
33-
if (this->isOwned) {
34-
delete this->queryResult;
35-
this->queryResult = nullptr;
36-
}
32+
this->Close();
3733
}
3834

3935
void NodeQueryResult::SetQueryResult(QueryResult* queryResult, bool isOwned) {
@@ -123,3 +119,16 @@ Napi::Value NodeQueryResult::GetColumnNamesAsync(const Napi::CallbackInfo& info)
123119
asyncWorker->Queue();
124120
return info.Env().Undefined();
125121
}
122+
123+
void NodeQueryResult::Close(const Napi::CallbackInfo& info) {
124+
Napi::Env env = info.Env();
125+
Napi::HandleScope scope(env);
126+
this->Close();
127+
}
128+
129+
void NodeQueryResult::Close() {
130+
if (this->isOwned) {
131+
delete this->queryResult;
132+
this->queryResult = nullptr;
133+
}
134+
}

src_js/connection.js

Lines changed: 100 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ class Connection {
2525
this._connection = null;
2626
this._isInitialized = false;
2727
this._initPromise = null;
28+
this._isClosed = false;
2829
numThreads = parseInt(numThreads);
2930
if (numThreads && numThreads > 0) {
3031
this._numThreads = numThreads;
@@ -36,6 +37,9 @@ class Connection {
3637
* connection is initialized automatically when the first query is executed.
3738
*/
3839
async init() {
40+
if (this._isClosed) {
41+
throw new Error("Connection is closed.");
42+
}
3943
if (!this._connection) {
4044
const database = await this._database._getDatabase();
4145
this._connection = new KuzuNative.NodeConnection(database);
@@ -69,6 +73,9 @@ class Connection {
6973
* @returns {KuzuNative.NodeConnection} the underlying native connection.
7074
*/
7175
async _getConnection() {
76+
if (this._isClosed) {
77+
throw new Error("Connection is closed.");
78+
}
7279
await this.init();
7380
return this._connection;
7481
}
@@ -123,30 +130,34 @@ class Connection {
123130
);
124131
}
125132
}
126-
this._getConnection().then((connection) => {
127-
const nodeQueryResult = new KuzuNative.NodeQueryResult();
128-
try {
129-
connection.executeAsync(
130-
preparedStatement._preparedStatement,
131-
nodeQueryResult,
132-
paramArray,
133-
(err) => {
134-
if (err) {
135-
return reject(err);
136-
}
137-
this._unwrapMultipleQueryResults(nodeQueryResult)
138-
.then((queryResults) => {
139-
return resolve(queryResults);
140-
})
141-
.catch((err) => {
133+
this._getConnection()
134+
.then((connection) => {
135+
const nodeQueryResult = new KuzuNative.NodeQueryResult();
136+
try {
137+
connection.executeAsync(
138+
preparedStatement._preparedStatement,
139+
nodeQueryResult,
140+
paramArray,
141+
(err) => {
142+
if (err) {
142143
return reject(err);
143-
});
144-
}
145-
);
146-
} catch (e) {
147-
return reject(e);
148-
}
149-
});
144+
}
145+
this._unwrapMultipleQueryResults(nodeQueryResult)
146+
.then((queryResults) => {
147+
return resolve(queryResults);
148+
})
149+
.catch((err) => {
150+
return reject(err);
151+
});
152+
}
153+
);
154+
} catch (e) {
155+
return reject(e);
156+
}
157+
})
158+
.catch((err) => {
159+
return reject(err);
160+
});
150161
});
151162
}
152163

@@ -160,18 +171,22 @@ class Connection {
160171
if (typeof statement !== "string") {
161172
return reject(new Error("statement must be a string."));
162173
}
163-
this._getConnection().then((connection) => {
164-
const preparedStatement = new KuzuNative.NodePreparedStatement(
165-
connection,
166-
statement
167-
);
168-
preparedStatement.initAsync((err) => {
169-
if (err) {
170-
return reject(err);
171-
}
172-
return resolve(new PreparedStatement(this, preparedStatement));
174+
this._getConnection()
175+
.then((connection) => {
176+
const preparedStatement = new KuzuNative.NodePreparedStatement(
177+
connection,
178+
statement
179+
);
180+
preparedStatement.initAsync((err) => {
181+
if (err) {
182+
return reject(err);
183+
}
184+
return resolve(new PreparedStatement(this, preparedStatement));
185+
});
186+
})
187+
.catch((err) => {
188+
return reject(err);
173189
});
174-
});
175190
});
176191
}
177192

@@ -185,25 +200,29 @@ class Connection {
185200
if (typeof statement !== "string") {
186201
return reject(new Error("statement must be a string."));
187202
}
188-
this._getConnection().then((connection) => {
189-
const nodeQueryResult = new KuzuNative.NodeQueryResult();
190-
try {
191-
connection.queryAsync(statement, nodeQueryResult, (err) => {
192-
if (err) {
193-
return reject(err);
194-
}
195-
this._unwrapMultipleQueryResults(nodeQueryResult)
196-
.then((queryResults) => {
197-
return resolve(queryResults);
198-
})
199-
.catch((err) => {
203+
this._getConnection()
204+
.then((connection) => {
205+
const nodeQueryResult = new KuzuNative.NodeQueryResult();
206+
try {
207+
connection.queryAsync(statement, nodeQueryResult, (err) => {
208+
if (err) {
200209
return reject(err);
201-
});
202-
});
203-
} catch (e) {
204-
return reject(e);
205-
}
206-
});
210+
}
211+
this._unwrapMultipleQueryResults(nodeQueryResult)
212+
.then((queryResults) => {
213+
return resolve(queryResults);
214+
})
215+
.catch((err) => {
216+
return reject(err);
217+
});
218+
});
219+
} catch (e) {
220+
return reject(e);
221+
}
222+
})
223+
.catch((err) => {
224+
return reject(err);
225+
});
207226
});
208227
}
209228

@@ -238,8 +257,7 @@ class Connection {
238257
let currentQueryResult = nodeQueryResult;
239258
while (currentQueryResult.hasNextQueryResult()) {
240259
queryResults.push(await this._getNextQueryResult(currentQueryResult));
241-
currentQueryResult =
242-
queryResults[queryResults.length - 1]._queryResult;
260+
currentQueryResult = queryResults[queryResults.length - 1]._queryResult;
243261
}
244262
return queryResults;
245263
}
@@ -280,6 +298,34 @@ class Connection {
280298
this._queryTimeout = timeoutInMs;
281299
}
282300
}
301+
302+
/**
303+
* Close the connection.
304+
*
305+
* Note: Call to this method is optional. The connection will be closed
306+
* automatically when the object goes out of scope.
307+
*/
308+
async close() {
309+
if (this._isClosed) {
310+
return;
311+
}
312+
if (!this._isInitialized) {
313+
if (this._initPromise) {
314+
// Connection is initializing, wait for it to finish first.
315+
await this._initPromise;
316+
} else {
317+
// Connection is not initialized, simply mark it as closed and initialized.
318+
this._isInitialized = true;
319+
this._isClosed = true;
320+
delete this._connection;
321+
return;
322+
}
323+
}
324+
// Connection is initialized, close it.
325+
this._connection.close();
326+
delete this._connection;
327+
this._isClosed = true;
328+
}
283329
}
284330

285331
module.exports = Connection;

src_js/database.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,15 @@ class Database {
132132
}
133133

134134
/**
135-
* Close the database.
135+
* Close the database. Once the database is closed, the lock on the database
136+
* files is released and the database can be opened in another process.
137+
*
138+
* Note: Call to this method is not required.
139+
* The Node.js garbage collector will automatically close the database when no
140+
* references to the database object exist. It is recommended not to call this
141+
* method explicitly. If you decide to manually close the database, make sure
142+
* that all the QueryResult and Connection objects are closed before calling
143+
* this method.
136144
*/
137145
async close() {
138146
if (this._isClosed) {

0 commit comments

Comments
 (0)