@graphscope-neug/osx-arm64 0.1.3 → 0.2.1

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 CHANGED
@@ -62,13 +62,34 @@ add_dependencies(neug_node_bind neug_proto)
62
62
  set(NEUG_LIBRARIES neug)
63
63
 
64
64
  find_package(OpenSSL REQUIRED)
65
- target_link_libraries(neug_node_bind PRIVATE
66
- ${NEUG_LIBRARIES}
67
- ${GLOG_LIBRARIES}
68
- ${Protobuf_LIBRARIES}
69
- OpenSSL::SSL
70
- OpenSSL::Crypto
71
- )
65
+ if (WIN32)
66
+ set(NODE_IMPORT_LIB "" CACHE FILEPATH
67
+ "Path to the Node.js import library (node.lib)")
68
+ if (NOT NODE_IMPORT_LIB OR NOT EXISTS "${NODE_IMPORT_LIB}")
69
+ message(FATAL_ERROR
70
+ "Cannot find the Node.js import library. Set NODE_IMPORT_LIB to node.lib.")
71
+ endif()
72
+
73
+ # Protobuf namespace-scope default instances are not exported from neug.dll.
74
+ # Link the generated objects into the addon, matching the Windows Python binding.
75
+ target_link_libraries(neug_node_bind PRIVATE
76
+ ${NEUG_LIBRARIES}
77
+ ${GLOG_LIBRARIES}
78
+ ${Protobuf_LIBRARIES}
79
+ $<TARGET_OBJECTS:neug_proto>
80
+ OpenSSL::SSL
81
+ OpenSSL::Crypto
82
+ "${NODE_IMPORT_LIB}"
83
+ )
84
+ else()
85
+ target_link_libraries(neug_node_bind PRIVATE
86
+ ${NEUG_LIBRARIES}
87
+ ${GLOG_LIBRARIES}
88
+ ${Protobuf_LIBRARIES}
89
+ OpenSSL::SSL
90
+ OpenSSL::Crypto
91
+ )
92
+ endif()
72
93
 
73
94
  target_compile_definitions(neug_node_bind PRIVATE
74
95
  NAPI_VERSION=8
@@ -93,16 +114,26 @@ if (APPLE)
93
114
  endif()
94
115
 
95
116
  # Set RPATH so neug_node_bind.node can find libneug.so / libneug.dylib
96
- # in the same directory (prebuilds/<platform>/). This is essential for
97
- # npm-distributed packages where the shared lib is bundled alongside the .node file.
98
- if (APPLE)
117
+ # in the same directory (build/Release/<platform>/). Package builds retain
118
+ # CMake's build-tree RPATHs until dependencies are bundled. Those paths identify
119
+ # the exact libraries selected by the linker; the platform bundle step copies them
120
+ # into the package and replaces non-portable RPATHs before npm pack runs.
121
+ if(APPLE)
122
+ set(_neug_node_rpath "@loader_path")
123
+ elseif(NOT WIN32)
124
+ set(_neug_node_rpath "$ORIGIN")
125
+ endif()
126
+
127
+ if(NOT WIN32)
99
128
  set_target_properties(neug_node_bind PROPERTIES
100
- INSTALL_RPATH "@loader_path"
101
- BUILD_WITH_INSTALL_RPATH TRUE
129
+ BUILD_RPATH "${_neug_node_rpath}"
130
+ INSTALL_RPATH "${_neug_node_rpath}"
131
+ BUILD_WITH_INSTALL_RPATH FALSE
102
132
  )
133
+ set_property(TARGET neug APPEND PROPERTY BUILD_RPATH "${_neug_node_rpath}")
103
134
  else()
104
- set_target_properties(neug_node_bind PROPERTIES
105
- INSTALL_RPATH "$ORIGIN"
106
- BUILD_WITH_INSTALL_RPATH TRUE
107
- )
135
+ # Windows has no RPATH. Keep neug.dll beside the addon so the loader can find it.
136
+ add_custom_command(TARGET neug_node_bind POST_BUILD
137
+ COMMAND ${CMAKE_COMMAND} -E copy_if_different
138
+ $<TARGET_FILE:neug> $<TARGET_FILE_DIR:neug_node_bind>)
108
139
  endif()
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 >= 18.0.0
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 >= 18.0.0** (N-API v8). Install via nvm:
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 (>= 18)
25
- nvm install 18 && nvm use 18
39
+ # Or install a specific version (>= 20)
40
+ nvm install 20 && nvm use 20
26
41
 
27
42
  # Verify
28
- node -v # should be >= 18.0.0
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 prebuilt binaries into `prebuilds/<platform>/`:
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
- - `libmimalloc.so.2` — memory allocator
60
- 3. Run `npm pack` to produce `neug-<version>.tgz`
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.1.3.tgz
89
+ npm install ./neug-0.2.1.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('./lib');
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,34 @@ 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 when
146
+ multiple 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
+ checkpoint/maintenance, and mutating procedure calls are not supported. Since
167
+ v0.2.1, embedded read-write transactions may group persistent `COPY FROM`
168
+ statements with ordinary DML and DDL into one checkpoint at `commit()`;
169
+ graph-read-only `LOAD FROM`/`COPY TO` and `COPY TEMP` may run in either
170
+ transaction mode, and `COPY TEMP` may be mixed with durable writes (only
171
+ persistent changes reach disk).
package/lib/binding.js CHANGED
@@ -16,14 +16,15 @@
16
16
  'use strict';
17
17
 
18
18
  // Native artifacts live in build/<Release|Debug>/<platform>/.
19
- // Platform tags: linux_x86_64, linux_arm64, osx_x86_64, osx_arm64
19
+ // Platform tags: linux_x86_64, linux_arm64, osx_x86_64, osx_arm64,
20
+ // windows_x86_64
20
21
  // Prefer Debug if present (dev intent); otherwise Release (the only
21
22
  // flavor shipped in npm tarballs).
22
23
 
23
24
  const path = require('path');
24
25
  const fs = require('fs');
25
26
 
26
- const OS_MAP = { linux: 'linux', darwin: 'osx' };
27
+ const OS_MAP = { linux: 'linux', darwin: 'osx', win32: 'windows' };
27
28
  const ARCH_MAP = { x64: 'x86_64', arm64: 'arm64' };
28
29
 
29
30
  const os = OS_MAP[process.platform];
package/lib/connection.js CHANGED
@@ -59,6 +59,68 @@ 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
+ * Since v0.2.1, persistent COPY FROM statements may be grouped with
81
+ * ordinary DML/DDL into one checkpoint at commit.
82
+ * @throws {Error} If the connection is closed or already has an active
83
+ * transaction.
84
+ */
85
+ beginTransaction(options = {}) {
86
+ this._ensureOpen('beginning a transaction');
87
+ if (
88
+ typeof options !== 'object' ||
89
+ options === null ||
90
+ Array.isArray(options)
91
+ ) {
92
+ throw new TypeError('Transaction options must be an object.');
93
+ }
94
+ const { readOnly = false } = options;
95
+ if (typeof readOnly !== 'boolean') {
96
+ throw new TypeError('options.readOnly must be a boolean.');
97
+ }
98
+ this._checkTransactionStatus(
99
+ 'begin transaction',
100
+ this._conn.beginTransaction(readOnly)
101
+ );
102
+ }
103
+
104
+ /**
105
+ * Commit the active explicit transaction.
106
+ *
107
+ * Persistent COPY FROM statements are published through one checkpoint;
108
+ * other writes use the ordinary logical-WAL commit path.
109
+ * A rollback-only transaction must be rolled back instead.
110
+ */
111
+ commit() {
112
+ this._ensureOpen('committing a transaction');
113
+ this._checkTransactionStatus('commit transaction', this._conn.commit());
114
+ }
115
+
116
+ /**
117
+ * Roll back the active explicit transaction and return to auto-commit mode.
118
+ */
119
+ rollback() {
120
+ this._ensureOpen('rolling back a transaction');
121
+ this._checkTransactionStatus('roll back transaction', this._conn.rollback());
122
+ }
123
+
62
124
  /**
63
125
  * Execute a cypher query on the database.
64
126
  *
@@ -81,12 +143,7 @@ class Connection {
81
143
  * );
82
144
  */
83
145
  execute(query, accessMode = '', parameters = null) {
84
- if (!this._isOpen) {
85
- throw new Error(
86
- `Connection is closed. Please open the connection before executing queries. ` +
87
- `Error code: ${ERR_CONNECTION_CLOSED}`
88
- );
89
- }
146
+ this._ensureOpen('executing queries');
90
147
 
91
148
  if (accessMode !== '' && !isAccessModeValid(accessMode.toLowerCase())) {
92
149
  throw new Error(
@@ -120,8 +177,27 @@ class Connection {
120
177
  * @returns {string} The schema as a string.
121
178
  */
122
179
  getSchema() {
180
+ this._ensureOpen('getting the schema');
123
181
  return this._conn.getSchema();
124
182
  }
183
+
184
+ _ensureOpen(action) {
185
+ if (!this._isOpen) {
186
+ throw new Error(
187
+ `Connection is closed. Please open the connection before ${action}. ` +
188
+ `Error code: ${ERR_CONNECTION_CLOSED}`
189
+ );
190
+ }
191
+ }
192
+
193
+ _checkTransactionStatus(action, status) {
194
+ if (status.code !== OK) {
195
+ throw new Error(
196
+ `Failed to ${action}. Error code: ${status.code}, Error Message: ` +
197
+ `${codeName(status.code)}: ${status.message}`
198
+ );
199
+ }
200
+ }
125
201
  }
126
202
 
127
203
  module.exports = { Connection };
package/lib/database.js CHANGED
@@ -28,7 +28,11 @@ const {
28
28
  // Load the native binding (unified loader handles prebuilds/ and build/)
29
29
  const nativeBinding = require('./binding');
30
30
 
31
- const ILLEGAL_CHARS = ['?', '*', '"', '<', '>', '|', ':', '\\'];
31
+ // On Windows, ':' (drive letter) and '\' (path separator) are valid, matching
32
+ // the Python binding's platform-aware validation.
33
+ const ILLEGAL_CHARS = process.platform === 'win32'
34
+ ? ['?', '*', '"', '<', '>', '|']
35
+ : ['?', '*', '"', '<', '>', '|', ':', '\\'];
32
36
  const PURE_MEMORY_PATHS = [':memory', ':memory:'];
33
37
  const VALID_MODES = [
34
38
  'r', 'read', 'w', 'rw', 'write',
@@ -58,9 +62,12 @@ const VALID_MODES = [
58
62
  */
59
63
  class Database {
60
64
  /**
61
- * Return the number of online CPUs, using the same system call as
62
- * Python's os.cpu_count() (sysconf(_SC_NPROCESSORS_ONLN)).
63
- * This is more reliable than os.cpus().length on arm64 Linux.
65
+ * Return the number of CPUs usable by the process, via the portable
66
+ * std::thread::hardware_concurrency() in the native binding
67
+ * (sysconf(_SC_NPROCESSORS_ONLN) on POSIX, GetSystemInfo on Windows).
68
+ * Typically matches Python's os.cpu_count() on mainstream platforms;
69
+ * returns 1 when the count is not detectable. This is more reliable
70
+ * than os.cpus().length on arm64 Linux.
64
71
  * @returns {number}
65
72
  */
66
73
  static cpuCount() {
@@ -121,8 +128,9 @@ class Database {
121
128
  );
122
129
  }
123
130
 
124
- // Use sysconf(_SC_NPROCESSORS_ONLN) via native binding to match
125
- // Python's os.cpu_count() behaviour (especially on arm64 Linux).
131
+ // Use std::thread::hardware_concurrency() via native binding (with a
132
+ // minimum of 1) to match Python's os.cpu_count() behaviour on
133
+ // mainstream platforms (especially on arm64 Linux).
126
134
  const cpuCount = Database.cpuCount();
127
135
  if (maxThreadNum > cpuCount) {
128
136
  throw new Error(
@@ -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-1017)
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,
@@ -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;
@@ -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.1.3",
3
+ "version": "0.2.1",
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": "for f in tests/test_*.js; do echo \"Running $f\"; node \"$f\" || exit 1; done"
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": ">=16.0.0"
27
+ "node": ">=20.0.0"
27
28
  },
28
29
  "dependencies": {
29
30
  "node-addon-api": "^7.0.0"
@@ -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
- return Napi::String::New(info.Env(), conn_->GetSchema());
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
- db_->RemoveConnection(conn_);
168
+ conn_->Close();
109
169
  conn_.reset();
110
170
  }
111
171
  return info.Env().Undefined();
@@ -21,7 +21,7 @@
21
21
  #include <memory>
22
22
  #include <string>
23
23
 
24
- #include "neug/execution/common/types/value.h"
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
 
@@ -15,7 +15,7 @@
15
15
 
16
16
  #include "node_database.h"
17
17
 
18
- #include <unistd.h>
18
+ #include <thread>
19
19
 
20
20
  #include <string>
21
21
 
@@ -156,7 +156,7 @@ Napi::Value NodeDatabase::Close(const Napi::CallbackInfo& info) {
156
156
  }
157
157
 
158
158
  Napi::Value NodeDatabase::GetCpuCount(const Napi::CallbackInfo& info) {
159
- long n = sysconf(_SC_NPROCESSORS_ONLN);
159
+ unsigned int n = std::thread::hardware_concurrency();
160
160
  if (n < 1) {
161
161
  n = 1;
162
162
  }
@@ -18,7 +18,7 @@
18
18
  #include <sstream>
19
19
  #include <string>
20
20
 
21
- #include "neug/execution/common/types/value.h"
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();
@@ -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(info.Env(),
336
- results_to_bolt_response(query_result_.response(),
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) {
@@ -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};