@graphscope-neug/osx-arm64 0.1.3-alpha.1 → 0.2.0
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/CMakeLists.txt +14 -14
- package/README.md +63 -12
- package/build/Release/osx_arm64/.dylibs/libcrypto.3.dylib +0 -0
- package/build/Release/osx_arm64/.dylibs/libmimalloc.2.0.dylib +0 -0
- package/build/Release/osx_arm64/.dylibs/libssl.3.dylib +0 -0
- package/build/Release/osx_arm64/libneug.dylib +0 -0
- package/build/Release/osx_arm64/neug_node_bind.node +0 -0
- package/lib/connection.js +78 -6
- package/lib/database.js +2 -69
- package/lib/error-codes.js +3 -1
- package/lib/index.js +0 -2
- package/lib/proto/error_pb.js +2 -0
- package/lib/query-result.js +12 -0
- package/package.json +4 -3
- package/src/node_connection.cc +63 -3
- package/src/node_connection.h +5 -1
- package/src/node_database.cc +12 -96
- package/src/node_database.h +0 -8
- package/src/node_query_request.cc +3 -4
- package/src/node_query_result.cc +50 -5
- package/src/node_query_result.h +1 -0
- package/lib/session.js +0 -275
package/CMakeLists.txt
CHANGED
|
@@ -60,9 +60,6 @@ target_include_directories(neug_node_bind PRIVATE
|
|
|
60
60
|
add_dependencies(neug_node_bind neug_proto)
|
|
61
61
|
|
|
62
62
|
set(NEUG_LIBRARIES neug)
|
|
63
|
-
if (BUILD_HTTP_SERVER)
|
|
64
|
-
set(NEUG_LIBRARIES ${NEUG_LIBRARIES})
|
|
65
|
-
endif()
|
|
66
63
|
|
|
67
64
|
find_package(OpenSSL REQUIRED)
|
|
68
65
|
target_link_libraries(neug_node_bind PRIVATE
|
|
@@ -96,16 +93,19 @@ if (APPLE)
|
|
|
96
93
|
endif()
|
|
97
94
|
|
|
98
95
|
# Set RPATH so neug_node_bind.node can find libneug.so / libneug.dylib
|
|
99
|
-
# in the same directory (
|
|
100
|
-
#
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
)
|
|
96
|
+
# in the same directory (build/Release/<platform>/). Package builds retain
|
|
97
|
+
# CMake's build-tree RPATHs until dependencies are bundled. Those paths identify
|
|
98
|
+
# the exact libraries selected by the linker; the platform bundle step copies them
|
|
99
|
+
# into the package and replaces non-portable RPATHs before npm pack runs.
|
|
100
|
+
if(APPLE)
|
|
101
|
+
set(_neug_node_rpath "@loader_path")
|
|
106
102
|
else()
|
|
107
|
-
|
|
108
|
-
INSTALL_RPATH "$ORIGIN"
|
|
109
|
-
BUILD_WITH_INSTALL_RPATH TRUE
|
|
110
|
-
)
|
|
103
|
+
set(_neug_node_rpath "$ORIGIN")
|
|
111
104
|
endif()
|
|
105
|
+
|
|
106
|
+
set_target_properties(neug_node_bind PROPERTIES
|
|
107
|
+
BUILD_RPATH "${_neug_node_rpath}"
|
|
108
|
+
INSTALL_RPATH "${_neug_node_rpath}"
|
|
109
|
+
BUILD_WITH_INSTALL_RPATH FALSE
|
|
110
|
+
)
|
|
111
|
+
set_property(TARGET neug APPEND PROPERTY BUILD_RPATH "${_neug_node_rpath}")
|
package/README.md
CHANGED
|
@@ -4,11 +4,26 @@
|
|
|
4
4
|
|
|
5
5
|
Same system dependencies as NeuG (CMake >= 3.16, C++20 compiler), plus:
|
|
6
6
|
|
|
7
|
-
- Node.js >=
|
|
7
|
+
- Node.js >= 20.0.0
|
|
8
|
+
- macOS package builds only: `delocate==0.13.0`
|
|
9
|
+
- Linux package builds only: `ldd` and `patchelf==0.19.0`
|
|
10
|
+
|
|
11
|
+
Install the macOS packaging dependency with:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
python3 -m pip install delocate==0.13.0
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
On Linux, install `patchelf==0.19.0` from the system package manager or the
|
|
18
|
+
[official releases](https://github.com/NixOS/patchelf/releases):
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
patchelf --version
|
|
22
|
+
```
|
|
8
23
|
|
|
9
24
|
### Installing Node.js
|
|
10
25
|
|
|
11
|
-
NeuG Node.js bindings require **Node.js >=
|
|
26
|
+
NeuG Node.js bindings require **Node.js >= 20.0.0** (N-API v8). Install via nvm:
|
|
12
27
|
|
|
13
28
|
```bash
|
|
14
29
|
# Install nvm
|
|
@@ -21,11 +36,11 @@ source ~/.zshrc
|
|
|
21
36
|
|
|
22
37
|
# Install Node.js LTS (v22)
|
|
23
38
|
nvm install --lts && nvm use --lts
|
|
24
|
-
# Or install a specific version (>=
|
|
25
|
-
nvm install
|
|
39
|
+
# Or install a specific version (>= 20)
|
|
40
|
+
nvm install 20 && nvm use 20
|
|
26
41
|
|
|
27
42
|
# Verify
|
|
28
|
-
node -v # should be >=
|
|
43
|
+
node -v # should be >= 20.0.0
|
|
29
44
|
npm -v
|
|
30
45
|
```
|
|
31
46
|
|
|
@@ -43,7 +58,6 @@ This will:
|
|
|
43
58
|
2. Build the native addon via the main NeuG CMake project (`-DBUILD_NODEJS=ON`)
|
|
44
59
|
3. Copy the resulting `neug_node_bind.node` to `build/Release/`
|
|
45
60
|
|
|
46
|
-
|
|
47
61
|
### Pack
|
|
48
62
|
|
|
49
63
|
```bash
|
|
@@ -53,16 +67,26 @@ make pack
|
|
|
53
67
|
Create a self-contained, distributable npm package tarball (`.tgz`). This will:
|
|
54
68
|
|
|
55
69
|
1. Build the native addon (same as `make build`)
|
|
56
|
-
2. Copy
|
|
70
|
+
2. Copy NeuG's first-party binaries into `build/Release/<platform>/`:
|
|
57
71
|
- `neug_node_bind.node` — the native addon
|
|
58
|
-
- `libneug.so` — core shared library
|
|
59
|
-
|
|
60
|
-
|
|
72
|
+
- `libneug.so` / `libneug.dylib` — core shared library
|
|
73
|
+
3. Collect the third-party libraries actually selected at link time:
|
|
74
|
+
- macOS uses `delocate-path` to copy them into `.dylibs/` and rewrite their
|
|
75
|
+
install names.
|
|
76
|
+
- Linux uses `ldd` to resolve the dependency closure, copies non-system
|
|
77
|
+
libraries into `.libs/`, and uses `patchelf` to set package-relative
|
|
78
|
+
`$ORIGIN` RPATHs.
|
|
79
|
+
4. Print the bundled native dependencies for inspection.
|
|
80
|
+
5. Run `npm pack` to produce `neug-<version>.tgz`
|
|
81
|
+
|
|
82
|
+
Npm package builds always force `NEUG_PACKAGE_BUILD=ON` and
|
|
83
|
+
`NEUG_NATIVE_ARCH=OFF`.
|
|
84
|
+
For local source builds, use `NEUG_NATIVE_ARCH=ON make dev`.
|
|
61
85
|
|
|
62
86
|
The resulting tarball can be installed without a C++ build environment:
|
|
63
87
|
|
|
64
88
|
```bash
|
|
65
|
-
npm install ./neug-0.
|
|
89
|
+
npm install ./neug-0.2.0.tgz
|
|
66
90
|
```
|
|
67
91
|
|
|
68
92
|
|
|
@@ -83,7 +107,7 @@ node example.js
|
|
|
83
107
|
|
|
84
108
|
|
|
85
109
|
```js
|
|
86
|
-
const { Database } = require('
|
|
110
|
+
const { Database } = require('neug');
|
|
87
111
|
|
|
88
112
|
// Open an in-memory database
|
|
89
113
|
const db = new Database({ databasePath: '', mode: 'w' });
|
|
@@ -114,3 +138,30 @@ for (const row of result) {
|
|
|
114
138
|
conn.close();
|
|
115
139
|
db.close();
|
|
116
140
|
```
|
|
141
|
+
|
|
142
|
+
## Explicit Transactions
|
|
143
|
+
|
|
144
|
+
Since v0.2, the embedded Node.js `Connection` API supports explicit AP
|
|
145
|
+
transactions. Auto-commit remains the default; begin a transaction only when
|
|
146
|
+
multiple ordinary queries must share one private view and one final commit.
|
|
147
|
+
|
|
148
|
+
```js
|
|
149
|
+
const conn = db.connect();
|
|
150
|
+
|
|
151
|
+
conn.beginTransaction();
|
|
152
|
+
conn.execute("CREATE (p:person {id: 3, name: 'Carol'});");
|
|
153
|
+
// Reads on this connection see the new vertex before publication.
|
|
154
|
+
conn.execute('MATCH (p:person) RETURN p.name;');
|
|
155
|
+
conn.commit();
|
|
156
|
+
|
|
157
|
+
conn.beginTransaction({ readOnly: true });
|
|
158
|
+
// This transaction pins one read view and rejects writes.
|
|
159
|
+
conn.execute('MATCH (p:person) RETURN p.name;');
|
|
160
|
+
conn.rollback();
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
`hasActiveTransaction` remains true after a failed statement because the
|
|
164
|
+
connection is rollback-only. Call `rollback()` before issuing another query.
|
|
165
|
+
Nested transactions, read-to-write upgrades, Cypher `BEGIN`/`COMMIT`/`ROLLBACK`,
|
|
166
|
+
and explicit-transaction COPY, batch, index, checkpoint, procedure, and
|
|
167
|
+
temporary-schema operations are not supported.
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/lib/connection.js
CHANGED
|
@@ -59,6 +59,64 @@ class Connection {
|
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Whether this connection has an unfinished explicit transaction.
|
|
64
|
+
*
|
|
65
|
+
* The property remains true while a failed transaction is rollback-only.
|
|
66
|
+
* Call {@link rollback} before issuing another query.
|
|
67
|
+
*
|
|
68
|
+
* @returns {boolean}
|
|
69
|
+
*/
|
|
70
|
+
get hasActiveTransaction() {
|
|
71
|
+
return this._isOpen && this._conn.hasActiveTransaction();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Begin an explicit embedded AP transaction.
|
|
76
|
+
*
|
|
77
|
+
* @param {{readOnly?: boolean}} [options={}] Transaction options.
|
|
78
|
+
* @param {boolean} [options.readOnly=false] Pin one read view and reject
|
|
79
|
+
* writes. By default, the transaction uses a private COW write view.
|
|
80
|
+
* @throws {Error} If the connection is closed or already has an active
|
|
81
|
+
* transaction.
|
|
82
|
+
*/
|
|
83
|
+
beginTransaction(options = {}) {
|
|
84
|
+
this._ensureOpen('beginning a transaction');
|
|
85
|
+
if (
|
|
86
|
+
typeof options !== 'object' ||
|
|
87
|
+
options === null ||
|
|
88
|
+
Array.isArray(options)
|
|
89
|
+
) {
|
|
90
|
+
throw new TypeError('Transaction options must be an object.');
|
|
91
|
+
}
|
|
92
|
+
const { readOnly = false } = options;
|
|
93
|
+
if (typeof readOnly !== 'boolean') {
|
|
94
|
+
throw new TypeError('options.readOnly must be a boolean.');
|
|
95
|
+
}
|
|
96
|
+
this._checkTransactionStatus(
|
|
97
|
+
'begin transaction',
|
|
98
|
+
this._conn.beginTransaction(readOnly)
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Commit the active explicit transaction.
|
|
104
|
+
*
|
|
105
|
+
* A rollback-only transaction must be rolled back instead.
|
|
106
|
+
*/
|
|
107
|
+
commit() {
|
|
108
|
+
this._ensureOpen('committing a transaction');
|
|
109
|
+
this._checkTransactionStatus('commit transaction', this._conn.commit());
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Roll back the active explicit transaction and return to auto-commit mode.
|
|
114
|
+
*/
|
|
115
|
+
rollback() {
|
|
116
|
+
this._ensureOpen('rolling back a transaction');
|
|
117
|
+
this._checkTransactionStatus('roll back transaction', this._conn.rollback());
|
|
118
|
+
}
|
|
119
|
+
|
|
62
120
|
/**
|
|
63
121
|
* Execute a cypher query on the database.
|
|
64
122
|
*
|
|
@@ -81,12 +139,7 @@ class Connection {
|
|
|
81
139
|
* );
|
|
82
140
|
*/
|
|
83
141
|
execute(query, accessMode = '', parameters = null) {
|
|
84
|
-
|
|
85
|
-
throw new Error(
|
|
86
|
-
`Connection is closed. Please open the connection before executing queries. ` +
|
|
87
|
-
`Error code: ${ERR_CONNECTION_CLOSED}`
|
|
88
|
-
);
|
|
89
|
-
}
|
|
142
|
+
this._ensureOpen('executing queries');
|
|
90
143
|
|
|
91
144
|
if (accessMode !== '' && !isAccessModeValid(accessMode.toLowerCase())) {
|
|
92
145
|
throw new Error(
|
|
@@ -120,8 +173,27 @@ class Connection {
|
|
|
120
173
|
* @returns {string} The schema as a string.
|
|
121
174
|
*/
|
|
122
175
|
getSchema() {
|
|
176
|
+
this._ensureOpen('getting the schema');
|
|
123
177
|
return this._conn.getSchema();
|
|
124
178
|
}
|
|
179
|
+
|
|
180
|
+
_ensureOpen(action) {
|
|
181
|
+
if (!this._isOpen) {
|
|
182
|
+
throw new Error(
|
|
183
|
+
`Connection is closed. Please open the connection before ${action}. ` +
|
|
184
|
+
`Error code: ${ERR_CONNECTION_CLOSED}`
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
_checkTransactionStatus(action, status) {
|
|
190
|
+
if (status.code !== OK) {
|
|
191
|
+
throw new Error(
|
|
192
|
+
`Failed to ${action}. Error code: ${status.code}, Error Message: ` +
|
|
193
|
+
`${codeName(status.code)}: ${status.message}`
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
125
197
|
}
|
|
126
198
|
|
|
127
199
|
module.exports = { Connection };
|
package/lib/database.js
CHANGED
|
@@ -91,7 +91,6 @@ class Database {
|
|
|
91
91
|
|
|
92
92
|
this._connections = [];
|
|
93
93
|
this._asyncConnections = [];
|
|
94
|
-
this._serving = false;
|
|
95
94
|
|
|
96
95
|
// Validate path
|
|
97
96
|
const dbPath = databasePath !== null ? databasePath : '';
|
|
@@ -187,17 +186,12 @@ class Database {
|
|
|
187
186
|
* Connect to the database and return a Connection object.
|
|
188
187
|
*
|
|
189
188
|
* @returns {Connection} A connection to the database.
|
|
190
|
-
* @throws {Error} If the database is closed
|
|
189
|
+
* @throws {Error} If the database is closed.
|
|
191
190
|
*/
|
|
192
191
|
connect() {
|
|
193
192
|
if (!this._database) {
|
|
194
193
|
throw new Error('Database is closed.');
|
|
195
194
|
}
|
|
196
|
-
if (this._serving) {
|
|
197
|
-
throw new Error(
|
|
198
|
-
'Cannot create connection while the database server is running.'
|
|
199
|
-
);
|
|
200
|
-
}
|
|
201
195
|
const nativeConn = this._database.connect();
|
|
202
196
|
const conn = new Connection(nativeConn);
|
|
203
197
|
this._connections.push(conn);
|
|
@@ -208,79 +202,18 @@ class Database {
|
|
|
208
202
|
* Connect to the database asynchronously.
|
|
209
203
|
*
|
|
210
204
|
* @returns {AsyncConnection} An async connection to the database.
|
|
211
|
-
* @throws {Error} If the database is closed
|
|
205
|
+
* @throws {Error} If the database is closed.
|
|
212
206
|
*/
|
|
213
207
|
asyncConnect() {
|
|
214
208
|
if (!this._database) {
|
|
215
209
|
throw new Error('Database is closed.');
|
|
216
210
|
}
|
|
217
|
-
if (this._serving) {
|
|
218
|
-
throw new Error(
|
|
219
|
-
'Cannot create async connection while the database server is running.'
|
|
220
|
-
);
|
|
221
|
-
}
|
|
222
211
|
const nativeConn = this._database.connect();
|
|
223
212
|
const asyncConn = new AsyncConnection(nativeConn);
|
|
224
213
|
this._asyncConnections.push(asyncConn);
|
|
225
214
|
return asyncConn;
|
|
226
215
|
}
|
|
227
216
|
|
|
228
|
-
/**
|
|
229
|
-
* Start the database server for handling remote connections (TP mode).
|
|
230
|
-
*
|
|
231
|
-
* @param {Object} [options] - Server options.
|
|
232
|
-
* @param {number} [options.port=10000] - The port to listen on.
|
|
233
|
-
* @param {string} [options.host='localhost'] - The host to bind to.
|
|
234
|
-
* @param {boolean} [options.blocking=true] - Whether to block until server stops.
|
|
235
|
-
* @returns {string} The URI of the server.
|
|
236
|
-
*/
|
|
237
|
-
serve(options = {}) {
|
|
238
|
-
const { port = 10000, host = 'localhost', blocking = true } = options;
|
|
239
|
-
|
|
240
|
-
// Check all connections are closed
|
|
241
|
-
for (const conn of this._connections) {
|
|
242
|
-
if (conn && conn.isOpen) {
|
|
243
|
-
throw new Error(
|
|
244
|
-
'Cannot start the server while there are open connections to the local database.'
|
|
245
|
-
);
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
for (const asyncConn of this._asyncConnections) {
|
|
249
|
-
if (asyncConn && asyncConn.isOpen) {
|
|
250
|
-
throw new Error(
|
|
251
|
-
'Cannot start the server while there are open async connections to the local database.'
|
|
252
|
-
);
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
if (this._serving) {
|
|
257
|
-
console.log('[neug] Database server is already running.');
|
|
258
|
-
return;
|
|
259
|
-
}
|
|
260
|
-
this._serving = true;
|
|
261
|
-
console.log(`[neug] Starting database server on ${host}:${port}.`);
|
|
262
|
-
|
|
263
|
-
try {
|
|
264
|
-
return this._database.serve(port, host, this._maxThreadNum, blocking);
|
|
265
|
-
} catch (e) {
|
|
266
|
-
this._serving = false;
|
|
267
|
-
throw e;
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
/**
|
|
272
|
-
* Stop the database server.
|
|
273
|
-
* @throws {Error} If the server is not running.
|
|
274
|
-
*/
|
|
275
|
-
stopServing() {
|
|
276
|
-
if (!this._serving) {
|
|
277
|
-
throw new Error('Database server is not running.');
|
|
278
|
-
}
|
|
279
|
-
console.log('[neug] Stopping database server.');
|
|
280
|
-
this._database.stopServing();
|
|
281
|
-
this._serving = false;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
217
|
/**
|
|
285
218
|
* Close the database connection and release all resources.
|
|
286
219
|
*/
|
package/lib/error-codes.js
CHANGED
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
const { Code } = require('./proto/error_pb');
|
|
30
30
|
|
|
31
31
|
// ---- Numeric error code constants (from error.proto Code enum) ----
|
|
32
|
-
// General errors (1001-
|
|
32
|
+
// General errors (1001-1018)
|
|
33
33
|
const OK = Code.OK;
|
|
34
34
|
const ERR_PERMISSION = Code.ERR_PERMISSION;
|
|
35
35
|
const ERR_VERSION_MISMATCHED = Code.ERR_VERSION_MISMATCHED;
|
|
@@ -48,6 +48,7 @@ const ERR_IO_ERROR = Code.ERR_IO_ERROR;
|
|
|
48
48
|
const ERR_BAD_ENCODING = Code.ERR_BAD_ENCODING;
|
|
49
49
|
const ERR_INVALID_FILE = Code.ERR_INVALID_FILE;
|
|
50
50
|
const ERR_EXTENSION = Code.ERR_EXTENSION;
|
|
51
|
+
const ERR_NO_CHECKPOINT = Code.ERR_NO_CHECKPOINT;
|
|
51
52
|
|
|
52
53
|
// Network errors (2001-2007)
|
|
53
54
|
const ERR_NETWORK = Code.ERR_NETWORK;
|
|
@@ -122,6 +123,7 @@ module.exports = {
|
|
|
122
123
|
ERR_BAD_ENCODING,
|
|
123
124
|
ERR_INVALID_FILE,
|
|
124
125
|
ERR_EXTENSION,
|
|
126
|
+
ERR_NO_CHECKPOINT,
|
|
125
127
|
// Network errors
|
|
126
128
|
ERR_NETWORK,
|
|
127
129
|
ERR_SESSION_CLOSED,
|
package/lib/index.js
CHANGED
|
@@ -46,7 +46,6 @@ const { Database } = require('./database');
|
|
|
46
46
|
const { Connection } = require('./connection');
|
|
47
47
|
const { AsyncConnection } = require('./async-connection');
|
|
48
48
|
const { QueryResult } = require('./query-result');
|
|
49
|
-
const { Session } = require('./session');
|
|
50
49
|
const { parseAndFormatResults, printResultsAsTable, parseEntry } = require('./format');
|
|
51
50
|
const { version } = require('./version');
|
|
52
51
|
const errorCodes = require('./error-codes');
|
|
@@ -56,7 +55,6 @@ module.exports = {
|
|
|
56
55
|
Connection,
|
|
57
56
|
AsyncConnection,
|
|
58
57
|
QueryResult,
|
|
59
|
-
Session,
|
|
60
58
|
parseAndFormatResults,
|
|
61
59
|
printResultsAsTable,
|
|
62
60
|
parseEntry,
|
package/lib/proto/error_pb.js
CHANGED
|
@@ -39,6 +39,7 @@ const Code = Object.create(
|
|
|
39
39
|
[1015, 'ERR_BAD_ENCODING'],
|
|
40
40
|
[1016, 'ERR_INVALID_FILE'],
|
|
41
41
|
[1017, 'ERR_EXTENSION'],
|
|
42
|
+
[1018, 'ERR_NO_CHECKPOINT'],
|
|
42
43
|
[2001, 'ERR_NETWORK'],
|
|
43
44
|
[2002, 'ERR_SESSION_CLOSED'],
|
|
44
45
|
[2003, 'ERR_CONNECTION_CLOSED'],
|
|
@@ -90,6 +91,7 @@ Code.ERR_IO_ERROR = 1014;
|
|
|
90
91
|
Code.ERR_BAD_ENCODING = 1015;
|
|
91
92
|
Code.ERR_INVALID_FILE = 1016;
|
|
92
93
|
Code.ERR_EXTENSION = 1017;
|
|
94
|
+
Code.ERR_NO_CHECKPOINT = 1018;
|
|
93
95
|
Code.ERR_NETWORK = 2001;
|
|
94
96
|
Code.ERR_SESSION_CLOSED = 2002;
|
|
95
97
|
Code.ERR_CONNECTION_CLOSED = 2003;
|
package/lib/query-result.js
CHANGED
|
@@ -106,6 +106,18 @@ class QueryResult {
|
|
|
106
106
|
this._result.close();
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
/**
|
|
110
|
+
* Get the profile/explain result as a structured metrics object.
|
|
111
|
+
* @returns {{ total_elapsed_ms: number, total_output_rows: number, operators: Array }} Metrics object.
|
|
112
|
+
*/
|
|
113
|
+
getProfileMetrics() {
|
|
114
|
+
const metrics = this._result.getProfileMetrics();
|
|
115
|
+
if (!metrics.operators) {
|
|
116
|
+
metrics.operators = [];
|
|
117
|
+
}
|
|
118
|
+
return metrics;
|
|
119
|
+
}
|
|
120
|
+
|
|
109
121
|
/**
|
|
110
122
|
* Make QueryResult iterable with for...of loops.
|
|
111
123
|
* @returns {Iterator} An iterator over the result rows.
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@graphscope-neug/osx-arm64",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "NeuG Graph Database Node.js bindings - high performance embedded graph database with Cypher query support",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
|
+
"exports": "./lib/index.js",
|
|
6
7
|
"scripts": {
|
|
7
|
-
"test": "
|
|
8
|
+
"test": "node tests/test-shim.js"
|
|
8
9
|
},
|
|
9
10
|
"keywords": [
|
|
10
11
|
"graph",
|
|
@@ -23,7 +24,7 @@
|
|
|
23
24
|
"url": "git+https://github.com/alibaba/neug.git"
|
|
24
25
|
},
|
|
25
26
|
"engines": {
|
|
26
|
-
"node": ">=
|
|
27
|
+
"node": ">=20.0.0"
|
|
27
28
|
},
|
|
28
29
|
"dependencies": {
|
|
29
30
|
"node-addon-api": "^7.0.0"
|
package/src/node_connection.cc
CHANGED
|
@@ -29,6 +29,21 @@
|
|
|
29
29
|
|
|
30
30
|
namespace neug {
|
|
31
31
|
|
|
32
|
+
namespace {
|
|
33
|
+
|
|
34
|
+
Napi::Object StatusToJs(Napi::Env env, const Status& status) {
|
|
35
|
+
auto result = Napi::Object::New(env);
|
|
36
|
+
result.Set("code", Napi::Number::New(env, status.error_code()));
|
|
37
|
+
result.Set("message", Napi::String::New(env, status.error_message()));
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
Status ClosedConnectionStatus() {
|
|
42
|
+
return Status(StatusCode::ERR_CONNECTION_CLOSED, "Connection is closed.");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
} // namespace
|
|
46
|
+
|
|
32
47
|
Napi::FunctionReference NodeConnection::constructor;
|
|
33
48
|
|
|
34
49
|
Napi::Object NodeConnection::Init(Napi::Env env, Napi::Object exports) {
|
|
@@ -36,6 +51,11 @@ Napi::Object NodeConnection::Init(Napi::Env env, Napi::Object exports) {
|
|
|
36
51
|
env, "NodeConnection",
|
|
37
52
|
{
|
|
38
53
|
InstanceMethod("execute", &NodeConnection::Execute),
|
|
54
|
+
InstanceMethod("beginTransaction", &NodeConnection::BeginTransaction),
|
|
55
|
+
InstanceMethod("commit", &NodeConnection::Commit),
|
|
56
|
+
InstanceMethod("rollback", &NodeConnection::Rollback),
|
|
57
|
+
InstanceMethod("hasActiveTransaction",
|
|
58
|
+
&NodeConnection::HasActiveTransaction),
|
|
39
59
|
InstanceMethod("getSchema", &NodeConnection::GetSchema),
|
|
40
60
|
InstanceMethod("close", &NodeConnection::Close),
|
|
41
61
|
});
|
|
@@ -81,7 +101,7 @@ Napi::Value NodeConnection::Execute(const Napi::CallbackInfo& info) {
|
|
|
81
101
|
}
|
|
82
102
|
|
|
83
103
|
// Parse parameters from JavaScript object
|
|
84
|
-
rapidjson::Document params_json;
|
|
104
|
+
rapidjson::Document params_json(rapidjson::kObjectType);
|
|
85
105
|
if (info.Length() >= 3 && info[2].IsObject()) {
|
|
86
106
|
Napi::Object params_obj = info[2].As<Napi::Object>();
|
|
87
107
|
Napi::Array keys = params_obj.GetPropertyNames();
|
|
@@ -99,13 +119,53 @@ Napi::Value NodeConnection::Execute(const Napi::CallbackInfo& info) {
|
|
|
99
119
|
return NodeQueryResult::NewInstance(env, std::move(query_result.value()));
|
|
100
120
|
}
|
|
101
121
|
|
|
122
|
+
Napi::Value NodeConnection::BeginTransaction(const Napi::CallbackInfo& info) {
|
|
123
|
+
Napi::Env env = info.Env();
|
|
124
|
+
if (info.Length() > 1 || (info.Length() == 1 && !info[0].IsBoolean())) {
|
|
125
|
+
Napi::TypeError::New(env, "readOnly must be a boolean")
|
|
126
|
+
.ThrowAsJavaScriptException();
|
|
127
|
+
return env.Null();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const bool read_only =
|
|
131
|
+
info.Length() == 1 && info[0].As<Napi::Boolean>().Value();
|
|
132
|
+
const auto status =
|
|
133
|
+
conn_ ? conn_->BeginTransaction(read_only ? TransactionMode::kReadOnly
|
|
134
|
+
: TransactionMode::kReadWrite)
|
|
135
|
+
: ClosedConnectionStatus();
|
|
136
|
+
return StatusToJs(env, status);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
Napi::Value NodeConnection::Commit(const Napi::CallbackInfo& info) {
|
|
140
|
+
const auto status = conn_ ? conn_->Commit() : ClosedConnectionStatus();
|
|
141
|
+
return StatusToJs(info.Env(), status);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
Napi::Value NodeConnection::Rollback(const Napi::CallbackInfo& info) {
|
|
145
|
+
const auto status = conn_ ? conn_->Rollback() : ClosedConnectionStatus();
|
|
146
|
+
return StatusToJs(info.Env(), status);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
Napi::Value NodeConnection::HasActiveTransaction(
|
|
150
|
+
const Napi::CallbackInfo& info) {
|
|
151
|
+
return Napi::Boolean::New(info.Env(), conn_ && conn_->HasActiveTransaction());
|
|
152
|
+
}
|
|
153
|
+
|
|
102
154
|
Napi::Value NodeConnection::GetSchema(const Napi::CallbackInfo& info) {
|
|
103
|
-
|
|
155
|
+
Napi::Env env = info.Env();
|
|
156
|
+
try {
|
|
157
|
+
return Napi::String::New(env, conn_->GetSchema());
|
|
158
|
+
} catch (const neug::exception::Exception& e) {
|
|
159
|
+
Napi::Error::New(env, e.what()).ThrowAsJavaScriptException();
|
|
160
|
+
} catch (const std::exception& e) {
|
|
161
|
+
Napi::Error::New(env, e.what()).ThrowAsJavaScriptException();
|
|
162
|
+
}
|
|
163
|
+
return env.Null();
|
|
104
164
|
}
|
|
105
165
|
|
|
106
166
|
Napi::Value NodeConnection::Close(const Napi::CallbackInfo& info) {
|
|
107
167
|
if (conn_) {
|
|
108
|
-
|
|
168
|
+
conn_->Close();
|
|
109
169
|
conn_.reset();
|
|
110
170
|
}
|
|
111
171
|
return info.Env().Undefined();
|
package/src/node_connection.h
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
#include <memory>
|
|
22
22
|
#include <string>
|
|
23
23
|
|
|
24
|
-
#include "neug/
|
|
24
|
+
#include "neug/common/types/value.h"
|
|
25
25
|
#include "neug/main/connection.h"
|
|
26
26
|
#include "neug/main/neug_db.h"
|
|
27
27
|
#include "node_query_result.h"
|
|
@@ -43,6 +43,10 @@ class NodeConnection : public Napi::ObjectWrap<NodeConnection> {
|
|
|
43
43
|
static Napi::FunctionReference constructor;
|
|
44
44
|
|
|
45
45
|
Napi::Value Execute(const Napi::CallbackInfo& info);
|
|
46
|
+
Napi::Value BeginTransaction(const Napi::CallbackInfo& info);
|
|
47
|
+
Napi::Value Commit(const Napi::CallbackInfo& info);
|
|
48
|
+
Napi::Value Rollback(const Napi::CallbackInfo& info);
|
|
49
|
+
Napi::Value HasActiveTransaction(const Napi::CallbackInfo& info);
|
|
46
50
|
Napi::Value GetSchema(const Napi::CallbackInfo& info);
|
|
47
51
|
Napi::Value Close(const Napi::CallbackInfo& info);
|
|
48
52
|
|
package/src/node_database.cc
CHANGED
|
@@ -27,15 +27,13 @@ namespace neug {
|
|
|
27
27
|
Napi::FunctionReference NodeDatabase::constructor;
|
|
28
28
|
|
|
29
29
|
Napi::Object NodeDatabase::Init(Napi::Env env, Napi::Object exports) {
|
|
30
|
-
Napi::Function func =
|
|
31
|
-
env, "NodeDatabase",
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
StaticMethod("cpuCount", &NodeDatabase::GetCpuCount),
|
|
38
|
-
});
|
|
30
|
+
Napi::Function func =
|
|
31
|
+
DefineClass(env, "NodeDatabase",
|
|
32
|
+
{
|
|
33
|
+
InstanceMethod("connect", &NodeDatabase::Connect),
|
|
34
|
+
InstanceMethod("close", &NodeDatabase::Close),
|
|
35
|
+
StaticMethod("cpuCount", &NodeDatabase::GetCpuCount),
|
|
36
|
+
});
|
|
39
37
|
constructor = Napi::Persistent(func);
|
|
40
38
|
constructor.SuppressDestruct();
|
|
41
39
|
exports.Set("NodeDatabase", func);
|
|
@@ -76,8 +74,10 @@ NodeDatabase::NodeDatabase(const Napi::CallbackInfo& info)
|
|
|
76
74
|
}
|
|
77
75
|
|
|
78
76
|
bool checkpoint_on_close = true;
|
|
79
|
-
if (opts.Has("checkpointOnClose") &&
|
|
80
|
-
|
|
77
|
+
if (opts.Has("checkpointOnClose") &&
|
|
78
|
+
opts.Get("checkpointOnClose").IsBoolean()) {
|
|
79
|
+
checkpoint_on_close =
|
|
80
|
+
opts.Get("checkpointOnClose").As<Napi::Boolean>().Value();
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
std::string buffer_strategy = "M_FULL";
|
|
@@ -95,8 +95,7 @@ NodeDatabase::NodeDatabase(const Napi::CallbackInfo& info)
|
|
|
95
95
|
mode == "read-write") {
|
|
96
96
|
mode_ = DBMode::READ_WRITE;
|
|
97
97
|
} else {
|
|
98
|
-
Napi::Error::New(env, "Invalid mode: " + mode)
|
|
99
|
-
.ThrowAsJavaScriptException();
|
|
98
|
+
Napi::Error::New(env, "Invalid mode: " + mode).ThrowAsJavaScriptException();
|
|
100
99
|
return;
|
|
101
100
|
}
|
|
102
101
|
|
|
@@ -122,12 +121,6 @@ NodeDatabase::NodeDatabase(const Napi::CallbackInfo& info)
|
|
|
122
121
|
|
|
123
122
|
NodeDatabase::~NodeDatabase() {
|
|
124
123
|
std::lock_guard<std::recursive_mutex> lock(mtx_);
|
|
125
|
-
#ifdef BUILD_HTTP_SERVER
|
|
126
|
-
if (service_) {
|
|
127
|
-
service_->Stop();
|
|
128
|
-
service_.reset();
|
|
129
|
-
}
|
|
130
|
-
#endif
|
|
131
124
|
if (database) {
|
|
132
125
|
database->Close();
|
|
133
126
|
database.reset();
|
|
@@ -155,12 +148,6 @@ Napi::Value NodeDatabase::Connect(const Napi::CallbackInfo& info) {
|
|
|
155
148
|
|
|
156
149
|
Napi::Value NodeDatabase::Close(const Napi::CallbackInfo& info) {
|
|
157
150
|
std::lock_guard<std::recursive_mutex> lock(mtx_);
|
|
158
|
-
#ifdef BUILD_HTTP_SERVER
|
|
159
|
-
if (service_) {
|
|
160
|
-
service_->Stop();
|
|
161
|
-
service_.reset();
|
|
162
|
-
}
|
|
163
|
-
#endif
|
|
164
151
|
if (database) {
|
|
165
152
|
database->Close();
|
|
166
153
|
database.reset();
|
|
@@ -168,77 +155,6 @@ Napi::Value NodeDatabase::Close(const Napi::CallbackInfo& info) {
|
|
|
168
155
|
return info.Env().Undefined();
|
|
169
156
|
}
|
|
170
157
|
|
|
171
|
-
Napi::Value NodeDatabase::Serve(const Napi::CallbackInfo& info) {
|
|
172
|
-
Napi::Env env = info.Env();
|
|
173
|
-
#ifdef BUILD_HTTP_SERVER
|
|
174
|
-
if (!database) {
|
|
175
|
-
Napi::Error::New(env, "Database is not initialized.")
|
|
176
|
-
.ThrowAsJavaScriptException();
|
|
177
|
-
return env.Null();
|
|
178
|
-
}
|
|
179
|
-
if (service_) {
|
|
180
|
-
Napi::Error::New(env, "Server is already running.")
|
|
181
|
-
.ThrowAsJavaScriptException();
|
|
182
|
-
return env.Null();
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
int port = 10000;
|
|
186
|
-
std::string host = "localhost";
|
|
187
|
-
int32_t num_thread = 0;
|
|
188
|
-
bool blocking = false;
|
|
189
|
-
|
|
190
|
-
if (info.Length() >= 1 && info[0].IsNumber()) {
|
|
191
|
-
port = info[0].As<Napi::Number>().Int32Value();
|
|
192
|
-
}
|
|
193
|
-
if (info.Length() >= 2 && info[1].IsString()) {
|
|
194
|
-
host = info[1].As<Napi::String>().Utf8Value();
|
|
195
|
-
}
|
|
196
|
-
if (info.Length() >= 3 && info[2].IsNumber()) {
|
|
197
|
-
num_thread = info[2].As<Napi::Number>().Int32Value();
|
|
198
|
-
}
|
|
199
|
-
if (info.Length() >= 4 && info[3].IsBoolean()) {
|
|
200
|
-
blocking = info[3].As<Napi::Boolean>().Value();
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
std::lock_guard<std::recursive_mutex> lock(mtx_);
|
|
204
|
-
database->Close();
|
|
205
|
-
database->Open(database->config());
|
|
206
|
-
|
|
207
|
-
neug::ServiceConfig config;
|
|
208
|
-
config.query_port = port;
|
|
209
|
-
config.host_str = host;
|
|
210
|
-
config.shard_num =
|
|
211
|
-
(num_thread == 0) ? std::thread::hardware_concurrency() : num_thread;
|
|
212
|
-
#ifdef __APPLE__
|
|
213
|
-
if (host == "localhost") {
|
|
214
|
-
config.host_str = "127.0.0.1";
|
|
215
|
-
}
|
|
216
|
-
#endif
|
|
217
|
-
|
|
218
|
-
service_ = std::make_unique<neug::NeugDBService>(*database, config);
|
|
219
|
-
if (blocking) {
|
|
220
|
-
service_->run_and_wait_for_exit();
|
|
221
|
-
return Napi::String::New(env, "");
|
|
222
|
-
}
|
|
223
|
-
return Napi::String::New(env, service_->Start());
|
|
224
|
-
#else
|
|
225
|
-
Napi::Error::New(env, "HTTP server is not enabled in this build.")
|
|
226
|
-
.ThrowAsJavaScriptException();
|
|
227
|
-
return env.Null();
|
|
228
|
-
#endif
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
Napi::Value NodeDatabase::StopServing(const Napi::CallbackInfo& info) {
|
|
232
|
-
std::lock_guard<std::recursive_mutex> lock(mtx_);
|
|
233
|
-
#ifdef BUILD_HTTP_SERVER
|
|
234
|
-
if (service_) {
|
|
235
|
-
service_->Stop();
|
|
236
|
-
service_.reset();
|
|
237
|
-
}
|
|
238
|
-
#endif
|
|
239
|
-
return info.Env().Undefined();
|
|
240
|
-
}
|
|
241
|
-
|
|
242
158
|
Napi::Value NodeDatabase::GetCpuCount(const Napi::CallbackInfo& info) {
|
|
243
159
|
long n = sysconf(_SC_NPROCESSORS_ONLN);
|
|
244
160
|
if (n < 1) {
|
package/src/node_database.h
CHANGED
|
@@ -23,9 +23,6 @@
|
|
|
23
23
|
#include <string>
|
|
24
24
|
|
|
25
25
|
#include "neug/main/neug_db.h"
|
|
26
|
-
#ifdef BUILD_HTTP_SERVER
|
|
27
|
-
#include "neug/server/neug_db_service.h"
|
|
28
|
-
#endif
|
|
29
26
|
#include "node_connection.h"
|
|
30
27
|
|
|
31
28
|
namespace neug {
|
|
@@ -40,8 +37,6 @@ class NodeDatabase : public Napi::ObjectWrap<NodeDatabase> {
|
|
|
40
37
|
|
|
41
38
|
Napi::Value Connect(const Napi::CallbackInfo& info);
|
|
42
39
|
Napi::Value Close(const Napi::CallbackInfo& info);
|
|
43
|
-
Napi::Value Serve(const Napi::CallbackInfo& info);
|
|
44
|
-
Napi::Value StopServing(const Napi::CallbackInfo& info);
|
|
45
40
|
|
|
46
41
|
private:
|
|
47
42
|
static Napi::FunctionReference constructor;
|
|
@@ -51,9 +46,6 @@ class NodeDatabase : public Napi::ObjectWrap<NodeDatabase> {
|
|
|
51
46
|
std::recursive_mutex mtx_;
|
|
52
47
|
std::string db_dir_;
|
|
53
48
|
std::unique_ptr<NeugDB> database;
|
|
54
|
-
#ifdef BUILD_HTTP_SERVER
|
|
55
|
-
std::unique_ptr<NeugDBService> service_;
|
|
56
|
-
#endif
|
|
57
49
|
};
|
|
58
50
|
|
|
59
51
|
} // namespace neug
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
#include <sstream>
|
|
19
19
|
#include <string>
|
|
20
20
|
|
|
21
|
-
#include "neug/
|
|
21
|
+
#include "neug/common/types/value.h"
|
|
22
22
|
#include "neug/main/query_request.h"
|
|
23
23
|
#include "neug/utils/access_mode.h"
|
|
24
24
|
#include "neug/utils/exception/exception.h"
|
|
@@ -64,8 +64,7 @@ rapidjson::Document napi_value_to_rapidjson_document(
|
|
|
64
64
|
} else if (val.IsDate()) {
|
|
65
65
|
// Convert Date to ISO string
|
|
66
66
|
Napi::Object date_obj = val.As<Napi::Object>();
|
|
67
|
-
Napi::Function to_iso =
|
|
68
|
-
date_obj.Get("toISOString").As<Napi::Function>();
|
|
67
|
+
Napi::Function to_iso = date_obj.Get("toISOString").As<Napi::Function>();
|
|
69
68
|
std::string iso_str =
|
|
70
69
|
to_iso.Call(date_obj, {}).As<Napi::String>().Utf8Value();
|
|
71
70
|
doc.SetString(iso_str.c_str(), iso_str.length(), allocator);
|
|
@@ -141,7 +140,7 @@ Napi::Value NodeQueryRequest::SerializeRequest(const Napi::CallbackInfo& info) {
|
|
|
141
140
|
rapidjson::Value(access_mode.c_str(), allocator),
|
|
142
141
|
allocator);
|
|
143
142
|
|
|
144
|
-
rapidjson::Document params_doc;
|
|
143
|
+
rapidjson::Document params_doc(rapidjson::kObjectType);
|
|
145
144
|
if (info.Length() >= 3 && info[2].IsObject()) {
|
|
146
145
|
Napi::Object params_obj = info[2].As<Napi::Object>();
|
|
147
146
|
Napi::Array keys = params_obj.GetPropertyNames();
|
package/src/node_query_result.cc
CHANGED
|
@@ -41,6 +41,8 @@ Napi::Object NodeQueryResult::Init(Napi::Env env, Napi::Object exports) {
|
|
|
41
41
|
InstanceMethod("statusMessage", &NodeQueryResult::StatusMessage),
|
|
42
42
|
InstanceMethod("getBoltResponse", &NodeQueryResult::GetBoltResponse),
|
|
43
43
|
InstanceMethod("close", &NodeQueryResult::Close),
|
|
44
|
+
InstanceMethod("getProfileMetrics",
|
|
45
|
+
&NodeQueryResult::GetProfileMetrics),
|
|
44
46
|
StaticMethod("fromString", &NodeQueryResult::FromString),
|
|
45
47
|
});
|
|
46
48
|
constructor = Napi::Persistent(func);
|
|
@@ -216,8 +218,7 @@ Napi::Value FetchValueFromColumn(Napi::Env env, const neug::Array& column,
|
|
|
216
218
|
size_t offset = col.offsets(index);
|
|
217
219
|
Napi::Array arr = Napi::Array::New(env, list_size);
|
|
218
220
|
for (uint32_t i = 0; i < list_size; ++i) {
|
|
219
|
-
arr.Set(i,
|
|
220
|
-
FetchValueFromColumn(env, col.elements(), offset + i));
|
|
221
|
+
arr.Set(i, FetchValueFromColumn(env, col.elements(), offset + i));
|
|
221
222
|
}
|
|
222
223
|
return arr;
|
|
223
224
|
} else {
|
|
@@ -332,9 +333,8 @@ Napi::Value NodeQueryResult::StatusMessage(const Napi::CallbackInfo& info) {
|
|
|
332
333
|
|
|
333
334
|
Napi::Value NodeQueryResult::GetBoltResponse(const Napi::CallbackInfo& info) {
|
|
334
335
|
auto names = column_names();
|
|
335
|
-
return Napi::String::New(
|
|
336
|
-
|
|
337
|
-
names));
|
|
336
|
+
return Napi::String::New(
|
|
337
|
+
info.Env(), results_to_bolt_response(query_result_.response(), names));
|
|
338
338
|
}
|
|
339
339
|
|
|
340
340
|
Napi::Value NodeQueryResult::Close(const Napi::CallbackInfo& info) {
|
|
@@ -344,6 +344,51 @@ Napi::Value NodeQueryResult::Close(const Napi::CallbackInfo& info) {
|
|
|
344
344
|
return info.Env().Undefined();
|
|
345
345
|
}
|
|
346
346
|
|
|
347
|
+
Napi::Value NodeQueryResult::GetProfileMetrics(const Napi::CallbackInfo& info) {
|
|
348
|
+
Napi::Env env = info.Env();
|
|
349
|
+
Napi::Object result = Napi::Object::New(env);
|
|
350
|
+
|
|
351
|
+
if (!query_result_.response().has_profile_result()) {
|
|
352
|
+
return result;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const auto& profile_result = query_result_.response().profile_result();
|
|
356
|
+
|
|
357
|
+
// Add aggregate metrics
|
|
358
|
+
result.Set("total_elapsed_ms",
|
|
359
|
+
Napi::Number::New(env, profile_result.total_elapsed_ms()));
|
|
360
|
+
result.Set("total_output_rows",
|
|
361
|
+
Napi::Number::New(
|
|
362
|
+
env, static_cast<double>(profile_result.total_output_rows())));
|
|
363
|
+
|
|
364
|
+
// Convert operators to array of objects
|
|
365
|
+
Napi::Array operators_array =
|
|
366
|
+
Napi::Array::New(env, profile_result.operators_size());
|
|
367
|
+
for (int i = 0; i < profile_result.operators_size(); ++i) {
|
|
368
|
+
const auto& op = profile_result.operators(i);
|
|
369
|
+
Napi::Object op_obj = Napi::Object::New(env);
|
|
370
|
+
|
|
371
|
+
op_obj.Set("operator_id", Napi::Number::New(env, op.operator_id()));
|
|
372
|
+
op_obj.Set("parent_id", Napi::Number::New(env, op.parent_id()));
|
|
373
|
+
op_obj.Set("operator_name", Napi::String::New(env, op.operator_name()));
|
|
374
|
+
op_obj.Set("elapsed_ms", Napi::Number::New(env, op.elapsed_ms()));
|
|
375
|
+
op_obj.Set("output_rows",
|
|
376
|
+
Napi::Number::New(env, static_cast<double>(op.output_rows())));
|
|
377
|
+
|
|
378
|
+
// Convert child_ids repeated field to array
|
|
379
|
+
Napi::Array child_ids_array = Napi::Array::New(env, op.child_ids_size());
|
|
380
|
+
for (int j = 0; j < op.child_ids_size(); ++j) {
|
|
381
|
+
child_ids_array.Set(j, Napi::Number::New(env, op.child_ids(j)));
|
|
382
|
+
}
|
|
383
|
+
op_obj.Set("child_ids", child_ids_array);
|
|
384
|
+
|
|
385
|
+
operators_array.Set(i, op_obj);
|
|
386
|
+
}
|
|
387
|
+
result.Set("operators", operators_array);
|
|
388
|
+
|
|
389
|
+
return result;
|
|
390
|
+
}
|
|
391
|
+
|
|
347
392
|
Napi::Value NodeQueryResult::FromString(const Napi::CallbackInfo& info) {
|
|
348
393
|
Napi::Env env = info.Env();
|
|
349
394
|
if (info.Length() < 1) {
|
package/src/node_query_result.h
CHANGED
|
@@ -60,6 +60,7 @@ class NodeQueryResult : public Napi::ObjectWrap<NodeQueryResult> {
|
|
|
60
60
|
Napi::Value StatusMessage(const Napi::CallbackInfo& info);
|
|
61
61
|
Napi::Value GetBoltResponse(const Napi::CallbackInfo& info);
|
|
62
62
|
Napi::Value Close(const Napi::CallbackInfo& info);
|
|
63
|
+
Napi::Value GetProfileMetrics(const Napi::CallbackInfo& info);
|
|
63
64
|
static Napi::Value FromString(const Napi::CallbackInfo& info);
|
|
64
65
|
|
|
65
66
|
size_t index_{0};
|
package/lib/session.js
DELETED
|
@@ -1,275 +0,0 @@
|
|
|
1
|
-
/** Copyright 2020 Alibaba Group Holding Limited.
|
|
2
|
-
*
|
|
3
|
-
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
-
* you may not use this file except in compliance with the License.
|
|
5
|
-
* You may obtain a copy of the License at
|
|
6
|
-
*
|
|
7
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
-
*
|
|
9
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
10
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
-
* See the License for the specific language governing permissions and
|
|
13
|
-
* limitations under the License.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
'use strict';
|
|
17
|
-
|
|
18
|
-
const http = require('http');
|
|
19
|
-
const https = require('https');
|
|
20
|
-
const { URL } = require('url');
|
|
21
|
-
const { QueryResult } = require('./query-result');
|
|
22
|
-
const { isAccessModeValid, validAccessModes } = require('./utils');
|
|
23
|
-
const { ERR_NETWORK, ERR_SESSION_CLOSED } = require('./error-codes');
|
|
24
|
-
|
|
25
|
-
// Load the native binding (unified loader handles prebuilds/ and build/)
|
|
26
|
-
const nativeBinding = require('./binding');
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Parse a timeout string to milliseconds.
|
|
30
|
-
* @param {string|number} timeout - Timeout value (e.g., '10s', '500ms', or number of seconds).
|
|
31
|
-
* @returns {number} Timeout in milliseconds.
|
|
32
|
-
*/
|
|
33
|
-
function parseTimeout(timeout) {
|
|
34
|
-
if (typeof timeout === 'number') {
|
|
35
|
-
return timeout * 1000;
|
|
36
|
-
}
|
|
37
|
-
if (typeof timeout === 'string') {
|
|
38
|
-
if (timeout.endsWith('ms')) {
|
|
39
|
-
return parseInt(timeout.slice(0, -2), 10);
|
|
40
|
-
} else if (timeout.endsWith('s')) {
|
|
41
|
-
return parseInt(timeout.slice(0, -1), 10) * 1000;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
return 10000; // default 10 seconds
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Make an HTTP request using Node.js built-in http/https modules.
|
|
49
|
-
* @param {string} url - The URL to request.
|
|
50
|
-
* @param {Object} options - Request options.
|
|
51
|
-
* @param {string} [options.method='GET'] - HTTP method.
|
|
52
|
-
* @param {string} [options.body] - Request body.
|
|
53
|
-
* @param {number} [options.timeout=10000] - Timeout in milliseconds.
|
|
54
|
-
* @returns {Promise<{statusCode: number, body: Buffer}>}
|
|
55
|
-
*/
|
|
56
|
-
function httpRequest(url, options = {}) {
|
|
57
|
-
return new Promise((resolve, reject) => {
|
|
58
|
-
const parsedUrl = new URL(url);
|
|
59
|
-
const isHttps = parsedUrl.protocol === 'https:';
|
|
60
|
-
const lib = isHttps ? https : http;
|
|
61
|
-
|
|
62
|
-
const reqOptions = {
|
|
63
|
-
hostname: parsedUrl.hostname,
|
|
64
|
-
port: parsedUrl.port || (isHttps ? 443 : 80),
|
|
65
|
-
path: parsedUrl.pathname + parsedUrl.search,
|
|
66
|
-
method: options.method || 'GET',
|
|
67
|
-
timeout: options.timeout || 10000,
|
|
68
|
-
headers: options.headers || {},
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
const req = lib.request(reqOptions, (res) => {
|
|
72
|
-
const chunks = [];
|
|
73
|
-
res.on('data', (chunk) => chunks.push(chunk));
|
|
74
|
-
res.on('end', () => {
|
|
75
|
-
resolve({
|
|
76
|
-
statusCode: res.statusCode,
|
|
77
|
-
body: Buffer.concat(chunks),
|
|
78
|
-
});
|
|
79
|
-
});
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
req.on('error', reject);
|
|
83
|
-
req.on('timeout', () => {
|
|
84
|
-
req.destroy();
|
|
85
|
-
reject(new Error('Request timed out'));
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
if (options.body) {
|
|
89
|
-
req.write(options.body);
|
|
90
|
-
}
|
|
91
|
-
req.end();
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Session connects to a NeuG server and provides the same query interface as Connection.
|
|
97
|
-
*
|
|
98
|
-
* @example
|
|
99
|
-
* // Start the server
|
|
100
|
-
* const db = new Database({ databasePath: '/tmp/test.db', mode: 'w' });
|
|
101
|
-
* db.serve({ port: 10000, host: 'localhost' });
|
|
102
|
-
*
|
|
103
|
-
* // In another process, connect via Session
|
|
104
|
-
* const session = new Session({ endpoint: 'http://localhost:10000' });
|
|
105
|
-
* const result = session.execute('MATCH (n) RETURN count(n)');
|
|
106
|
-
* for (const row of result) {
|
|
107
|
-
* console.log(row);
|
|
108
|
-
* }
|
|
109
|
-
* session.close();
|
|
110
|
-
*/
|
|
111
|
-
class Session {
|
|
112
|
-
/**
|
|
113
|
-
* @param {Object} [options] - Session options.
|
|
114
|
-
* @param {string} [options.endpoint='http://localhost:10000'] - The server endpoint URL.
|
|
115
|
-
* @param {string|number} [options.timeout='10s'] - Request timeout.
|
|
116
|
-
*/
|
|
117
|
-
constructor(options = {}) {
|
|
118
|
-
const {
|
|
119
|
-
endpoint = 'http://localhost:10000',
|
|
120
|
-
timeout = '10s',
|
|
121
|
-
} = options;
|
|
122
|
-
|
|
123
|
-
this._endpoint = endpoint.replace(/\/+$/, '');
|
|
124
|
-
this._queryEndpoint = `${this._endpoint}/cypher`;
|
|
125
|
-
this._statusEndpoint = `${this._endpoint}/service_status`;
|
|
126
|
-
this._schemaEndpoint = `${this._endpoint}/schema`;
|
|
127
|
-
this._timeout = parseTimeout(timeout);
|
|
128
|
-
this._closed = false;
|
|
129
|
-
|
|
130
|
-
// Quick connectivity check
|
|
131
|
-
// (We do this synchronously-ish via the constructor, but it's fire-and-forget)
|
|
132
|
-
// For a proper async init, use Session.open() instead.
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Open a session with connectivity check.
|
|
137
|
-
*
|
|
138
|
-
* @param {Object} [options] - Session options (same as constructor).
|
|
139
|
-
* @returns {Promise<Session>} A connected Session instance.
|
|
140
|
-
*/
|
|
141
|
-
static async open(options = {}) {
|
|
142
|
-
const session = new Session(options);
|
|
143
|
-
// Wait for the server to be ready, same purpose as Python's
|
|
144
|
-
// implicit readiness via slower HTTP client startup.
|
|
145
|
-
const maxRetries = 5;
|
|
146
|
-
const retryDelayMs = 200;
|
|
147
|
-
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
148
|
-
try {
|
|
149
|
-
await session._checkConnection();
|
|
150
|
-
return session;
|
|
151
|
-
} catch (_) {
|
|
152
|
-
if (attempt < maxRetries - 1) {
|
|
153
|
-
await new Promise((r) => setTimeout(r, retryDelayMs));
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
// Return anyway — let actual queries surface the real error
|
|
158
|
-
return session;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* Check connectivity to the server.
|
|
163
|
-
* @private
|
|
164
|
-
*/
|
|
165
|
-
async _checkConnection() {
|
|
166
|
-
await httpRequest(this._statusEndpoint, {
|
|
167
|
-
method: 'GET',
|
|
168
|
-
timeout: this._timeout,
|
|
169
|
-
});
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* Close the session.
|
|
174
|
-
*/
|
|
175
|
-
close() {
|
|
176
|
-
if (this._closed) {
|
|
177
|
-
console.warn('[neug] Session is already closed.');
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
|
-
this._closed = true;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* Execute a query on the NeuG server.
|
|
185
|
-
*
|
|
186
|
-
* @param {string} query - The cypher query string.
|
|
187
|
-
* @param {string} [accessMode=''] - The access mode.
|
|
188
|
-
* @param {Object} [parameters=null] - Query parameters.
|
|
189
|
-
* @returns {Promise<QueryResult>} The query result.
|
|
190
|
-
* @throws {Error} If the session is closed or the request fails.
|
|
191
|
-
*/
|
|
192
|
-
async execute(query, accessMode = '', parameters = null) {
|
|
193
|
-
if (this._closed) {
|
|
194
|
-
throw new Error(
|
|
195
|
-
`Session is closed. Cannot execute query. Error code: ${ERR_SESSION_CLOSED}`
|
|
196
|
-
);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
if (accessMode !== '' && !isAccessModeValid(accessMode.toLowerCase())) {
|
|
200
|
-
throw new Error(
|
|
201
|
-
`Invalid access_mode: ${accessMode}. Supported access modes are ` +
|
|
202
|
-
`[${validAccessModes.join(', ')}].`
|
|
203
|
-
);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// Build payload
|
|
207
|
-
const payload = JSON.stringify({
|
|
208
|
-
query,
|
|
209
|
-
access_mode: accessMode || 'update',
|
|
210
|
-
parameters: parameters || {},
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
let response;
|
|
214
|
-
try {
|
|
215
|
-
response = await httpRequest(this._queryEndpoint, {
|
|
216
|
-
method: 'POST',
|
|
217
|
-
body: payload,
|
|
218
|
-
timeout: this._timeout,
|
|
219
|
-
headers: {
|
|
220
|
-
'Content-Type': 'application/json',
|
|
221
|
-
'Content-Length': Buffer.byteLength(payload),
|
|
222
|
-
},
|
|
223
|
-
});
|
|
224
|
-
} catch (e) {
|
|
225
|
-
throw new Error(
|
|
226
|
-
`Could not execute query: ${query}. Error code: ${ERR_NETWORK}. ` +
|
|
227
|
-
`Details: ${e.message}`
|
|
228
|
-
);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
if (response.statusCode !== 200) {
|
|
232
|
-
throw new Error(
|
|
233
|
-
`Failed to execute query: ${query}. Http code: ${response.statusCode}, ` +
|
|
234
|
-
`Response: ${response.body.toString()}`
|
|
235
|
-
);
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// The response body is a protobuf-serialized QueryResponse.
|
|
239
|
-
// Use the native binding to deserialize it.
|
|
240
|
-
if (!nativeBinding || !nativeBinding.NodeQueryResult) {
|
|
241
|
-
throw new Error(
|
|
242
|
-
'Native binding is required for Session but was not loaded. ' +
|
|
243
|
-
'Please ensure the neug native addon is built and available.'
|
|
244
|
-
);
|
|
245
|
-
}
|
|
246
|
-
const nativeResult = nativeBinding.NodeQueryResult.fromString(response.body);
|
|
247
|
-
return new QueryResult(nativeResult);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
/**
|
|
251
|
-
* Get the service status of the NeuG server.
|
|
252
|
-
* @returns {Promise<Object>} The server status as a JSON object.
|
|
253
|
-
*/
|
|
254
|
-
async serviceStatus() {
|
|
255
|
-
const response = await httpRequest(this._statusEndpoint, {
|
|
256
|
-
method: 'GET',
|
|
257
|
-
timeout: this._timeout,
|
|
258
|
-
});
|
|
259
|
-
return JSON.parse(response.body.toString());
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
/**
|
|
263
|
-
* Get the schema of the NeuG database from the server.
|
|
264
|
-
* @returns {Promise<Object>} The schema as a JSON object.
|
|
265
|
-
*/
|
|
266
|
-
async getSchema() {
|
|
267
|
-
const response = await httpRequest(this._schemaEndpoint, {
|
|
268
|
-
method: 'GET',
|
|
269
|
-
timeout: this._timeout,
|
|
270
|
-
});
|
|
271
|
-
return JSON.parse(response.body.toString());
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
module.exports = { Session };
|