cypherlite 2.0.0 → 2.0.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/cypherlite.darwin-arm64.node +0 -0
- package/cypherlite.darwin-x64.node +0 -0
- package/cypherlite.linux-arm64-gnu.node +0 -0
- package/cypherlite.linux-x64-gnu.node +0 -0
- package/cypherlite.win32-x64-msvc.node +0 -0
- package/index.js +314 -14
- package/package.json +13 -4
- package/Cargo.toml +0 -28
- package/__test__/index.spec.mjs +0 -405
- package/build.rs +0 -5
- package/src/database.rs +0 -121
- package/src/error.rs +0 -18
- package/src/lib.rs +0 -44
- package/src/result.rs +0 -87
- package/src/transaction.rs +0 -85
- package/src/value.rs +0 -136
- package/vitest.config.mjs +0 -8
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/index.js
CHANGED
|
@@ -1,20 +1,320 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/* tslint:disable */
|
|
2
|
+
/* eslint-disable */
|
|
3
|
+
/* prettier-ignore */
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
const { join } = require('node:path');
|
|
5
|
+
/* auto-generated by NAPI-RS */
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
const
|
|
7
|
+
const { existsSync, readFileSync } = require('fs')
|
|
8
|
+
const { join } = require('path')
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
const { platform, arch } = process
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
12
|
+
let nativeBinding = null
|
|
13
|
+
let localFileExisted = false
|
|
14
|
+
let loadError = null
|
|
15
|
+
|
|
16
|
+
function isMusl() {
|
|
17
|
+
// For Node 10
|
|
18
|
+
if (!process.report || typeof process.report.getReport !== 'function') {
|
|
19
|
+
try {
|
|
20
|
+
const lddPath = require('child_process').execSync('which ldd').toString().trim()
|
|
21
|
+
return readFileSync(lddPath, 'utf8').includes('musl')
|
|
22
|
+
} catch (e) {
|
|
23
|
+
return true
|
|
24
|
+
}
|
|
25
|
+
} else {
|
|
26
|
+
const { glibcVersionRuntime } = process.report.getReport().header
|
|
27
|
+
return !glibcVersionRuntime
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
switch (platform) {
|
|
32
|
+
case 'android':
|
|
33
|
+
switch (arch) {
|
|
34
|
+
case 'arm64':
|
|
35
|
+
localFileExisted = existsSync(join(__dirname, 'cypherlite.android-arm64.node'))
|
|
36
|
+
try {
|
|
37
|
+
if (localFileExisted) {
|
|
38
|
+
nativeBinding = require('./cypherlite.android-arm64.node')
|
|
39
|
+
} else {
|
|
40
|
+
nativeBinding = require('cypherlite-android-arm64')
|
|
41
|
+
}
|
|
42
|
+
} catch (e) {
|
|
43
|
+
loadError = e
|
|
44
|
+
}
|
|
45
|
+
break
|
|
46
|
+
case 'arm':
|
|
47
|
+
localFileExisted = existsSync(join(__dirname, 'cypherlite.android-arm-eabi.node'))
|
|
48
|
+
try {
|
|
49
|
+
if (localFileExisted) {
|
|
50
|
+
nativeBinding = require('./cypherlite.android-arm-eabi.node')
|
|
51
|
+
} else {
|
|
52
|
+
nativeBinding = require('cypherlite-android-arm-eabi')
|
|
53
|
+
}
|
|
54
|
+
} catch (e) {
|
|
55
|
+
loadError = e
|
|
56
|
+
}
|
|
57
|
+
break
|
|
58
|
+
default:
|
|
59
|
+
throw new Error(`Unsupported architecture on Android ${arch}`)
|
|
60
|
+
}
|
|
61
|
+
break
|
|
62
|
+
case 'win32':
|
|
63
|
+
switch (arch) {
|
|
64
|
+
case 'x64':
|
|
65
|
+
localFileExisted = existsSync(
|
|
66
|
+
join(__dirname, 'cypherlite.win32-x64-msvc.node')
|
|
67
|
+
)
|
|
68
|
+
try {
|
|
69
|
+
if (localFileExisted) {
|
|
70
|
+
nativeBinding = require('./cypherlite.win32-x64-msvc.node')
|
|
71
|
+
} else {
|
|
72
|
+
nativeBinding = require('cypherlite-win32-x64-msvc')
|
|
73
|
+
}
|
|
74
|
+
} catch (e) {
|
|
75
|
+
loadError = e
|
|
76
|
+
}
|
|
77
|
+
break
|
|
78
|
+
case 'ia32':
|
|
79
|
+
localFileExisted = existsSync(
|
|
80
|
+
join(__dirname, 'cypherlite.win32-ia32-msvc.node')
|
|
81
|
+
)
|
|
82
|
+
try {
|
|
83
|
+
if (localFileExisted) {
|
|
84
|
+
nativeBinding = require('./cypherlite.win32-ia32-msvc.node')
|
|
85
|
+
} else {
|
|
86
|
+
nativeBinding = require('cypherlite-win32-ia32-msvc')
|
|
87
|
+
}
|
|
88
|
+
} catch (e) {
|
|
89
|
+
loadError = e
|
|
90
|
+
}
|
|
91
|
+
break
|
|
92
|
+
case 'arm64':
|
|
93
|
+
localFileExisted = existsSync(
|
|
94
|
+
join(__dirname, 'cypherlite.win32-arm64-msvc.node')
|
|
95
|
+
)
|
|
96
|
+
try {
|
|
97
|
+
if (localFileExisted) {
|
|
98
|
+
nativeBinding = require('./cypherlite.win32-arm64-msvc.node')
|
|
99
|
+
} else {
|
|
100
|
+
nativeBinding = require('cypherlite-win32-arm64-msvc')
|
|
101
|
+
}
|
|
102
|
+
} catch (e) {
|
|
103
|
+
loadError = e
|
|
104
|
+
}
|
|
105
|
+
break
|
|
106
|
+
default:
|
|
107
|
+
throw new Error(`Unsupported architecture on Windows: ${arch}`)
|
|
108
|
+
}
|
|
109
|
+
break
|
|
110
|
+
case 'darwin':
|
|
111
|
+
localFileExisted = existsSync(join(__dirname, 'cypherlite.darwin-universal.node'))
|
|
112
|
+
try {
|
|
113
|
+
if (localFileExisted) {
|
|
114
|
+
nativeBinding = require('./cypherlite.darwin-universal.node')
|
|
115
|
+
} else {
|
|
116
|
+
nativeBinding = require('cypherlite-darwin-universal')
|
|
117
|
+
}
|
|
118
|
+
break
|
|
119
|
+
} catch {}
|
|
120
|
+
switch (arch) {
|
|
121
|
+
case 'x64':
|
|
122
|
+
localFileExisted = existsSync(join(__dirname, 'cypherlite.darwin-x64.node'))
|
|
123
|
+
try {
|
|
124
|
+
if (localFileExisted) {
|
|
125
|
+
nativeBinding = require('./cypherlite.darwin-x64.node')
|
|
126
|
+
} else {
|
|
127
|
+
nativeBinding = require('cypherlite-darwin-x64')
|
|
128
|
+
}
|
|
129
|
+
} catch (e) {
|
|
130
|
+
loadError = e
|
|
131
|
+
}
|
|
132
|
+
break
|
|
133
|
+
case 'arm64':
|
|
134
|
+
localFileExisted = existsSync(
|
|
135
|
+
join(__dirname, 'cypherlite.darwin-arm64.node')
|
|
136
|
+
)
|
|
137
|
+
try {
|
|
138
|
+
if (localFileExisted) {
|
|
139
|
+
nativeBinding = require('./cypherlite.darwin-arm64.node')
|
|
140
|
+
} else {
|
|
141
|
+
nativeBinding = require('cypherlite-darwin-arm64')
|
|
142
|
+
}
|
|
143
|
+
} catch (e) {
|
|
144
|
+
loadError = e
|
|
145
|
+
}
|
|
146
|
+
break
|
|
147
|
+
default:
|
|
148
|
+
throw new Error(`Unsupported architecture on macOS: ${arch}`)
|
|
149
|
+
}
|
|
150
|
+
break
|
|
151
|
+
case 'freebsd':
|
|
152
|
+
if (arch !== 'x64') {
|
|
153
|
+
throw new Error(`Unsupported architecture on FreeBSD: ${arch}`)
|
|
154
|
+
}
|
|
155
|
+
localFileExisted = existsSync(join(__dirname, 'cypherlite.freebsd-x64.node'))
|
|
156
|
+
try {
|
|
157
|
+
if (localFileExisted) {
|
|
158
|
+
nativeBinding = require('./cypherlite.freebsd-x64.node')
|
|
159
|
+
} else {
|
|
160
|
+
nativeBinding = require('cypherlite-freebsd-x64')
|
|
161
|
+
}
|
|
162
|
+
} catch (e) {
|
|
163
|
+
loadError = e
|
|
164
|
+
}
|
|
165
|
+
break
|
|
166
|
+
case 'linux':
|
|
167
|
+
switch (arch) {
|
|
168
|
+
case 'x64':
|
|
169
|
+
if (isMusl()) {
|
|
170
|
+
localFileExisted = existsSync(
|
|
171
|
+
join(__dirname, 'cypherlite.linux-x64-musl.node')
|
|
172
|
+
)
|
|
173
|
+
try {
|
|
174
|
+
if (localFileExisted) {
|
|
175
|
+
nativeBinding = require('./cypherlite.linux-x64-musl.node')
|
|
176
|
+
} else {
|
|
177
|
+
nativeBinding = require('cypherlite-linux-x64-musl')
|
|
178
|
+
}
|
|
179
|
+
} catch (e) {
|
|
180
|
+
loadError = e
|
|
181
|
+
}
|
|
182
|
+
} else {
|
|
183
|
+
localFileExisted = existsSync(
|
|
184
|
+
join(__dirname, 'cypherlite.linux-x64-gnu.node')
|
|
185
|
+
)
|
|
186
|
+
try {
|
|
187
|
+
if (localFileExisted) {
|
|
188
|
+
nativeBinding = require('./cypherlite.linux-x64-gnu.node')
|
|
189
|
+
} else {
|
|
190
|
+
nativeBinding = require('cypherlite-linux-x64-gnu')
|
|
191
|
+
}
|
|
192
|
+
} catch (e) {
|
|
193
|
+
loadError = e
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
break
|
|
197
|
+
case 'arm64':
|
|
198
|
+
if (isMusl()) {
|
|
199
|
+
localFileExisted = existsSync(
|
|
200
|
+
join(__dirname, 'cypherlite.linux-arm64-musl.node')
|
|
201
|
+
)
|
|
202
|
+
try {
|
|
203
|
+
if (localFileExisted) {
|
|
204
|
+
nativeBinding = require('./cypherlite.linux-arm64-musl.node')
|
|
205
|
+
} else {
|
|
206
|
+
nativeBinding = require('cypherlite-linux-arm64-musl')
|
|
207
|
+
}
|
|
208
|
+
} catch (e) {
|
|
209
|
+
loadError = e
|
|
210
|
+
}
|
|
211
|
+
} else {
|
|
212
|
+
localFileExisted = existsSync(
|
|
213
|
+
join(__dirname, 'cypherlite.linux-arm64-gnu.node')
|
|
214
|
+
)
|
|
215
|
+
try {
|
|
216
|
+
if (localFileExisted) {
|
|
217
|
+
nativeBinding = require('./cypherlite.linux-arm64-gnu.node')
|
|
218
|
+
} else {
|
|
219
|
+
nativeBinding = require('cypherlite-linux-arm64-gnu')
|
|
220
|
+
}
|
|
221
|
+
} catch (e) {
|
|
222
|
+
loadError = e
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
break
|
|
226
|
+
case 'arm':
|
|
227
|
+
if (isMusl()) {
|
|
228
|
+
localFileExisted = existsSync(
|
|
229
|
+
join(__dirname, 'cypherlite.linux-arm-musleabihf.node')
|
|
230
|
+
)
|
|
231
|
+
try {
|
|
232
|
+
if (localFileExisted) {
|
|
233
|
+
nativeBinding = require('./cypherlite.linux-arm-musleabihf.node')
|
|
234
|
+
} else {
|
|
235
|
+
nativeBinding = require('cypherlite-linux-arm-musleabihf')
|
|
236
|
+
}
|
|
237
|
+
} catch (e) {
|
|
238
|
+
loadError = e
|
|
239
|
+
}
|
|
240
|
+
} else {
|
|
241
|
+
localFileExisted = existsSync(
|
|
242
|
+
join(__dirname, 'cypherlite.linux-arm-gnueabihf.node')
|
|
243
|
+
)
|
|
244
|
+
try {
|
|
245
|
+
if (localFileExisted) {
|
|
246
|
+
nativeBinding = require('./cypherlite.linux-arm-gnueabihf.node')
|
|
247
|
+
} else {
|
|
248
|
+
nativeBinding = require('cypherlite-linux-arm-gnueabihf')
|
|
249
|
+
}
|
|
250
|
+
} catch (e) {
|
|
251
|
+
loadError = e
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
break
|
|
255
|
+
case 'riscv64':
|
|
256
|
+
if (isMusl()) {
|
|
257
|
+
localFileExisted = existsSync(
|
|
258
|
+
join(__dirname, 'cypherlite.linux-riscv64-musl.node')
|
|
259
|
+
)
|
|
260
|
+
try {
|
|
261
|
+
if (localFileExisted) {
|
|
262
|
+
nativeBinding = require('./cypherlite.linux-riscv64-musl.node')
|
|
263
|
+
} else {
|
|
264
|
+
nativeBinding = require('cypherlite-linux-riscv64-musl')
|
|
265
|
+
}
|
|
266
|
+
} catch (e) {
|
|
267
|
+
loadError = e
|
|
268
|
+
}
|
|
269
|
+
} else {
|
|
270
|
+
localFileExisted = existsSync(
|
|
271
|
+
join(__dirname, 'cypherlite.linux-riscv64-gnu.node')
|
|
272
|
+
)
|
|
273
|
+
try {
|
|
274
|
+
if (localFileExisted) {
|
|
275
|
+
nativeBinding = require('./cypherlite.linux-riscv64-gnu.node')
|
|
276
|
+
} else {
|
|
277
|
+
nativeBinding = require('cypherlite-linux-riscv64-gnu')
|
|
278
|
+
}
|
|
279
|
+
} catch (e) {
|
|
280
|
+
loadError = e
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
break
|
|
284
|
+
case 's390x':
|
|
285
|
+
localFileExisted = existsSync(
|
|
286
|
+
join(__dirname, 'cypherlite.linux-s390x-gnu.node')
|
|
287
|
+
)
|
|
288
|
+
try {
|
|
289
|
+
if (localFileExisted) {
|
|
290
|
+
nativeBinding = require('./cypherlite.linux-s390x-gnu.node')
|
|
291
|
+
} else {
|
|
292
|
+
nativeBinding = require('cypherlite-linux-s390x-gnu')
|
|
293
|
+
}
|
|
294
|
+
} catch (e) {
|
|
295
|
+
loadError = e
|
|
296
|
+
}
|
|
297
|
+
break
|
|
298
|
+
default:
|
|
299
|
+
throw new Error(`Unsupported architecture on Linux: ${arch}`)
|
|
300
|
+
}
|
|
301
|
+
break
|
|
302
|
+
default:
|
|
303
|
+
throw new Error(`Unsupported OS: ${platform}, architecture: ${arch}`)
|
|
18
304
|
}
|
|
19
305
|
|
|
20
|
-
|
|
306
|
+
if (!nativeBinding) {
|
|
307
|
+
if (loadError) {
|
|
308
|
+
throw loadError
|
|
309
|
+
}
|
|
310
|
+
throw new Error(`Failed to load native binding`)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const { Database, open, CylResult, Transaction, version, features } = nativeBinding
|
|
314
|
+
|
|
315
|
+
module.exports.Database = Database
|
|
316
|
+
module.exports.open = open
|
|
317
|
+
module.exports.CylResult = CylResult
|
|
318
|
+
module.exports.Transaction = Transaction
|
|
319
|
+
module.exports.version = version
|
|
320
|
+
module.exports.features = features
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cypherlite",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"description": "Lightweight embedded graph database with Cypher query support",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -21,13 +21,22 @@
|
|
|
21
21
|
"graph-database",
|
|
22
22
|
"napi-rs"
|
|
23
23
|
],
|
|
24
|
+
"files": [
|
|
25
|
+
"index.js",
|
|
26
|
+
"index.d.ts",
|
|
27
|
+
"README.md",
|
|
28
|
+
"*.node"
|
|
29
|
+
],
|
|
24
30
|
"napi": {
|
|
25
31
|
"name": "cypherlite",
|
|
26
|
-
"triples": {
|
|
32
|
+
"triples": {
|
|
33
|
+
"defaults": true,
|
|
34
|
+
"additional": ["aarch64-unknown-linux-gnu"]
|
|
35
|
+
}
|
|
27
36
|
},
|
|
28
37
|
"scripts": {
|
|
29
|
-
"build": "napi build --release",
|
|
30
|
-
"build:debug": "napi build",
|
|
38
|
+
"build": "napi build --platform --release",
|
|
39
|
+
"build:debug": "napi build --platform",
|
|
31
40
|
"test": "vitest run"
|
|
32
41
|
},
|
|
33
42
|
"devDependencies": {
|
package/Cargo.toml
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
[package]
|
|
2
|
-
name = "cypherlite-node"
|
|
3
|
-
version = "2.0.0"
|
|
4
|
-
edition = "2021"
|
|
5
|
-
rust-version = "1.84"
|
|
6
|
-
description = "Node.js bindings for CypherLite embedded graph database via napi-rs"
|
|
7
|
-
publish = false
|
|
8
|
-
|
|
9
|
-
[lib]
|
|
10
|
-
crate-type = ["cdylib"]
|
|
11
|
-
|
|
12
|
-
[features]
|
|
13
|
-
default = ["temporal-core"]
|
|
14
|
-
temporal-core = ["cypherlite-query/temporal-core", "cypherlite-core/temporal-core"]
|
|
15
|
-
temporal-edge = ["temporal-core", "cypherlite-query/temporal-edge", "cypherlite-core/temporal-edge"]
|
|
16
|
-
subgraph = ["temporal-edge", "cypherlite-query/subgraph", "cypherlite-core/subgraph"]
|
|
17
|
-
hypergraph = ["subgraph", "cypherlite-query/hypergraph", "cypherlite-core/hypergraph"]
|
|
18
|
-
full-temporal = ["hypergraph", "cypherlite-query/full-temporal", "cypherlite-core/full-temporal"]
|
|
19
|
-
plugin = ["cypherlite-query/plugin", "cypherlite-core/plugin"]
|
|
20
|
-
|
|
21
|
-
[dependencies]
|
|
22
|
-
napi = { version = "2", features = ["napi9"] }
|
|
23
|
-
napi-derive = "2"
|
|
24
|
-
cypherlite-query = { version = "2.0.0", path = "../cypherlite-query" }
|
|
25
|
-
cypherlite-core = { version = "2.0.0", path = "../cypherlite-core" }
|
|
26
|
-
|
|
27
|
-
[build-dependencies]
|
|
28
|
-
napi-build = "2"
|
package/__test__/index.spec.mjs
DELETED
|
@@ -1,405 +0,0 @@
|
|
|
1
|
-
// CypherLite Node.js bindings specification tests.
|
|
2
|
-
//
|
|
3
|
-
// These tests define the expected behavior of the napi-rs bindings.
|
|
4
|
-
// They cover: version/features, database lifecycle, query execution,
|
|
5
|
-
// parameter binding, transactions, result access, value types, and errors.
|
|
6
|
-
|
|
7
|
-
import { describe, it, expect, afterEach } from 'vitest';
|
|
8
|
-
import { tmpdir } from 'node:os';
|
|
9
|
-
import { mkdtemp, rm } from 'node:fs/promises';
|
|
10
|
-
import { join } from 'node:path';
|
|
11
|
-
|
|
12
|
-
// The native module is loaded from the build output.
|
|
13
|
-
import {
|
|
14
|
-
version,
|
|
15
|
-
features,
|
|
16
|
-
open,
|
|
17
|
-
Database,
|
|
18
|
-
CylResult,
|
|
19
|
-
Transaction,
|
|
20
|
-
} from '../index.js';
|
|
21
|
-
|
|
22
|
-
// Helper: create a temporary directory for each test.
|
|
23
|
-
async function createTempDir() {
|
|
24
|
-
return mkdtemp(join(tmpdir(), 'cypherlite-node-test-'));
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// Track temp dirs for cleanup.
|
|
28
|
-
const tempDirs = [];
|
|
29
|
-
|
|
30
|
-
afterEach(async () => {
|
|
31
|
-
for (const dir of tempDirs) {
|
|
32
|
-
await rm(dir, { recursive: true, force: true }).catch(() => {});
|
|
33
|
-
}
|
|
34
|
-
tempDirs.length = 0;
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
async function openTempDb(options) {
|
|
38
|
-
const dir = await createTempDir();
|
|
39
|
-
tempDirs.push(dir);
|
|
40
|
-
const dbPath = join(dir, 'test.cyl');
|
|
41
|
-
return open(dbPath, options);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// ============================================================
|
|
45
|
-
// M1: Version and Features
|
|
46
|
-
// ============================================================
|
|
47
|
-
|
|
48
|
-
describe('version and features', () => {
|
|
49
|
-
it('should return a version string', () => {
|
|
50
|
-
const v = version();
|
|
51
|
-
expect(typeof v).toBe('string');
|
|
52
|
-
expect(v).toMatch(/^\d+\.\d+\.\d+$/);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it('should return a features string', () => {
|
|
56
|
-
const f = features();
|
|
57
|
-
expect(typeof f).toBe('string');
|
|
58
|
-
// At minimum, temporal-core should be present (default feature).
|
|
59
|
-
expect(f).toContain('temporal-core');
|
|
60
|
-
});
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
// ============================================================
|
|
64
|
-
// M2: Database Lifecycle
|
|
65
|
-
// ============================================================
|
|
66
|
-
|
|
67
|
-
describe('database lifecycle', () => {
|
|
68
|
-
it('should open a database', async () => {
|
|
69
|
-
const db = await openTempDb();
|
|
70
|
-
expect(db).toBeInstanceOf(Database);
|
|
71
|
-
db.close();
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it('should open with custom options', async () => {
|
|
75
|
-
const db = await openTempDb({ pageSize: 4096, cacheCapacity: 128 });
|
|
76
|
-
expect(db).toBeInstanceOf(Database);
|
|
77
|
-
db.close();
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it('should report isClosed correctly', async () => {
|
|
81
|
-
const db = await openTempDb();
|
|
82
|
-
expect(db.isClosed).toBe(false);
|
|
83
|
-
db.close();
|
|
84
|
-
expect(db.isClosed).toBe(true);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it('should allow close to be called multiple times', async () => {
|
|
88
|
-
const db = await openTempDb();
|
|
89
|
-
db.close();
|
|
90
|
-
expect(() => db.close()).not.toThrow();
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it('should throw on execute after close', async () => {
|
|
94
|
-
const db = await openTempDb();
|
|
95
|
-
db.close();
|
|
96
|
-
expect(() => db.execute("MATCH (n) RETURN n")).toThrow(/closed/i);
|
|
97
|
-
});
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
// ============================================================
|
|
101
|
-
// M3: Query Execution
|
|
102
|
-
// ============================================================
|
|
103
|
-
|
|
104
|
-
describe('query execution', () => {
|
|
105
|
-
it('should execute CREATE and MATCH', async () => {
|
|
106
|
-
const db = await openTempDb();
|
|
107
|
-
db.execute("CREATE (n:Person {name: 'Alice', age: 30})");
|
|
108
|
-
const result = db.execute("MATCH (n:Person) RETURN n.name, n.age");
|
|
109
|
-
expect(result).toBeInstanceOf(CylResult);
|
|
110
|
-
expect(result.length).toBe(1);
|
|
111
|
-
const row = result.row(0);
|
|
112
|
-
expect(row['n.name']).toBe('Alice');
|
|
113
|
-
expect(row['n.age']).toBe(30);
|
|
114
|
-
db.close();
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it('should return empty result for non-existent label', async () => {
|
|
118
|
-
const db = await openTempDb();
|
|
119
|
-
const result = db.execute("MATCH (n:Ghost) RETURN n");
|
|
120
|
-
expect(result.length).toBe(0);
|
|
121
|
-
db.close();
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it('should execute with parameters', async () => {
|
|
125
|
-
const db = await openTempDb();
|
|
126
|
-
db.execute("CREATE (n:Person {name: 'Alice'})");
|
|
127
|
-
const result = db.execute(
|
|
128
|
-
"MATCH (n:Person) WHERE n.name = $name RETURN n.name",
|
|
129
|
-
{ name: 'Alice' }
|
|
130
|
-
);
|
|
131
|
-
expect(result.length).toBe(1);
|
|
132
|
-
expect(result.row(0)['n.name']).toBe('Alice');
|
|
133
|
-
db.close();
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
it('should handle multiple rows', async () => {
|
|
137
|
-
const db = await openTempDb();
|
|
138
|
-
db.execute("CREATE (n:Person {name: 'Alice'})");
|
|
139
|
-
db.execute("CREATE (n:Person {name: 'Bob'})");
|
|
140
|
-
db.execute("CREATE (n:Person {name: 'Charlie'})");
|
|
141
|
-
const result = db.execute("MATCH (n:Person) RETURN n.name");
|
|
142
|
-
expect(result.length).toBe(3);
|
|
143
|
-
const names = result.toArray().map(r => r['n.name']).sort();
|
|
144
|
-
expect(names).toEqual(['Alice', 'Bob', 'Charlie']);
|
|
145
|
-
db.close();
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
it('should handle SET then MATCH', async () => {
|
|
149
|
-
const db = await openTempDb();
|
|
150
|
-
db.execute("CREATE (n:Person {name: 'Alice', age: 25})");
|
|
151
|
-
db.execute("MATCH (n:Person) SET n.age = 30");
|
|
152
|
-
const result = db.execute("MATCH (n:Person) RETURN n.age");
|
|
153
|
-
expect(result.row(0)['n.age']).toBe(30);
|
|
154
|
-
db.close();
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
it('should handle DETACH DELETE', async () => {
|
|
158
|
-
const db = await openTempDb();
|
|
159
|
-
db.execute("CREATE (a:Person {name: 'Alice'})-[:KNOWS]->(b:Person {name: 'Bob'})");
|
|
160
|
-
db.execute("MATCH (n:Person) DETACH DELETE n");
|
|
161
|
-
const result = db.execute("MATCH (n:Person) RETURN n");
|
|
162
|
-
expect(result.length).toBe(0);
|
|
163
|
-
db.close();
|
|
164
|
-
});
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
// ============================================================
|
|
168
|
-
// M4: Transaction Support
|
|
169
|
-
// ============================================================
|
|
170
|
-
|
|
171
|
-
describe('transactions', () => {
|
|
172
|
-
it('should begin and commit a transaction', async () => {
|
|
173
|
-
const db = await openTempDb();
|
|
174
|
-
const tx = db.begin();
|
|
175
|
-
expect(tx).toBeInstanceOf(Transaction);
|
|
176
|
-
tx.execute("CREATE (n:Person {name: 'Alice'})");
|
|
177
|
-
tx.commit();
|
|
178
|
-
const result = db.execute("MATCH (n:Person) RETURN n.name");
|
|
179
|
-
expect(result.length).toBe(1);
|
|
180
|
-
expect(result.row(0)['n.name']).toBe('Alice');
|
|
181
|
-
db.close();
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
it('should begin and rollback a transaction', async () => {
|
|
185
|
-
const db = await openTempDb();
|
|
186
|
-
const tx = db.begin();
|
|
187
|
-
tx.execute("CREATE (n:Person {name: 'Bob'})");
|
|
188
|
-
tx.rollback();
|
|
189
|
-
// After rollback (Phase 2 no-op), data may still exist.
|
|
190
|
-
// The important thing is no error is thrown.
|
|
191
|
-
db.close();
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
it('should throw on execute after commit', async () => {
|
|
195
|
-
const db = await openTempDb();
|
|
196
|
-
const tx = db.begin();
|
|
197
|
-
tx.commit();
|
|
198
|
-
expect(() => tx.execute("CREATE (n:Person {name: 'X'})")).toThrow(/finished/i);
|
|
199
|
-
db.close();
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
it('should throw on execute after rollback', async () => {
|
|
203
|
-
const db = await openTempDb();
|
|
204
|
-
const tx = db.begin();
|
|
205
|
-
tx.rollback();
|
|
206
|
-
expect(() => tx.execute("CREATE (n:Person {name: 'X'})")).toThrow(/finished/i);
|
|
207
|
-
db.close();
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
it('should throw on double commit', async () => {
|
|
211
|
-
const db = await openTempDb();
|
|
212
|
-
const tx = db.begin();
|
|
213
|
-
tx.commit();
|
|
214
|
-
expect(() => tx.commit()).toThrow(/finished/i);
|
|
215
|
-
db.close();
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
it('should execute with params in transaction', async () => {
|
|
219
|
-
const db = await openTempDb();
|
|
220
|
-
const tx = db.begin();
|
|
221
|
-
tx.execute("CREATE (n:Person {name: 'Alice'})");
|
|
222
|
-
const result = tx.execute(
|
|
223
|
-
"MATCH (n:Person) WHERE n.name = $name RETURN n.name",
|
|
224
|
-
{ name: 'Alice' }
|
|
225
|
-
);
|
|
226
|
-
expect(result.length).toBe(1);
|
|
227
|
-
tx.commit();
|
|
228
|
-
db.close();
|
|
229
|
-
});
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
// ============================================================
|
|
233
|
-
// M5: Result and Row Access
|
|
234
|
-
// ============================================================
|
|
235
|
-
|
|
236
|
-
describe('result access', () => {
|
|
237
|
-
it('should expose column names', async () => {
|
|
238
|
-
const db = await openTempDb();
|
|
239
|
-
db.execute("CREATE (n:Person {name: 'Alice', age: 30})");
|
|
240
|
-
const result = db.execute("MATCH (n:Person) RETURN n.name, n.age");
|
|
241
|
-
const cols = result.columns;
|
|
242
|
-
expect(Array.isArray(cols)).toBe(true);
|
|
243
|
-
expect(cols).toContain('n.name');
|
|
244
|
-
expect(cols).toContain('n.age');
|
|
245
|
-
db.close();
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
it('should expose row count via length', async () => {
|
|
249
|
-
const db = await openTempDb();
|
|
250
|
-
db.execute("CREATE (n:Person {name: 'Alice'})");
|
|
251
|
-
db.execute("CREATE (n:Person {name: 'Bob'})");
|
|
252
|
-
const result = db.execute("MATCH (n:Person) RETURN n.name");
|
|
253
|
-
expect(result.length).toBe(2);
|
|
254
|
-
db.close();
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
it('should access individual row by index', async () => {
|
|
258
|
-
const db = await openTempDb();
|
|
259
|
-
db.execute("CREATE (n:Person {name: 'Alice', age: 30})");
|
|
260
|
-
const result = db.execute("MATCH (n:Person) RETURN n.name, n.age");
|
|
261
|
-
const row = result.row(0);
|
|
262
|
-
expect(typeof row).toBe('object');
|
|
263
|
-
expect(row['n.name']).toBe('Alice');
|
|
264
|
-
expect(row['n.age']).toBe(30);
|
|
265
|
-
db.close();
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
it('should throw on out-of-range row index', async () => {
|
|
269
|
-
const db = await openTempDb();
|
|
270
|
-
const result = db.execute("MATCH (n:Ghost) RETURN n");
|
|
271
|
-
expect(() => result.row(0)).toThrow(/out of range/i);
|
|
272
|
-
db.close();
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
it('should convert all rows via toArray', async () => {
|
|
276
|
-
const db = await openTempDb();
|
|
277
|
-
db.execute("CREATE (n:Person {name: 'Alice'})");
|
|
278
|
-
db.execute("CREATE (n:Person {name: 'Bob'})");
|
|
279
|
-
const result = db.execute("MATCH (n:Person) RETURN n.name");
|
|
280
|
-
const rows = result.toArray();
|
|
281
|
-
expect(Array.isArray(rows)).toBe(true);
|
|
282
|
-
expect(rows.length).toBe(2);
|
|
283
|
-
const names = rows.map(r => r['n.name']).sort();
|
|
284
|
-
expect(names).toEqual(['Alice', 'Bob']);
|
|
285
|
-
db.close();
|
|
286
|
-
});
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
// ============================================================
|
|
290
|
-
// M6: Value Type Conversions
|
|
291
|
-
// ============================================================
|
|
292
|
-
|
|
293
|
-
describe('value types', () => {
|
|
294
|
-
it('should handle null values', async () => {
|
|
295
|
-
const db = await openTempDb();
|
|
296
|
-
db.execute("CREATE (n:Person {name: 'Alice'})");
|
|
297
|
-
const result = db.execute("MATCH (n:Person) RETURN n.name, n.email");
|
|
298
|
-
const row = result.row(0);
|
|
299
|
-
expect(row['n.name']).toBe('Alice');
|
|
300
|
-
// Missing property should be null.
|
|
301
|
-
expect(row['n.email']).toBeNull();
|
|
302
|
-
db.close();
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
it('should handle boolean values', async () => {
|
|
306
|
-
const db = await openTempDb();
|
|
307
|
-
db.execute("CREATE (n:Flag {active: true})");
|
|
308
|
-
const result = db.execute("MATCH (n:Flag) RETURN n.active");
|
|
309
|
-
expect(result.row(0)['n.active']).toBe(true);
|
|
310
|
-
db.close();
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
it('should handle integer values', async () => {
|
|
314
|
-
const db = await openTempDb();
|
|
315
|
-
db.execute("CREATE (n:Num {val: 42})");
|
|
316
|
-
const result = db.execute("MATCH (n:Num) RETURN n.val");
|
|
317
|
-
expect(result.row(0)['n.val']).toBe(42);
|
|
318
|
-
db.close();
|
|
319
|
-
});
|
|
320
|
-
|
|
321
|
-
it('should handle float values', async () => {
|
|
322
|
-
const db = await openTempDb();
|
|
323
|
-
db.execute("CREATE (n:Num {val: 3.14})");
|
|
324
|
-
const result = db.execute("MATCH (n:Num) RETURN n.val");
|
|
325
|
-
expect(result.row(0)['n.val']).toBeCloseTo(3.14);
|
|
326
|
-
db.close();
|
|
327
|
-
});
|
|
328
|
-
|
|
329
|
-
it('should handle string values', async () => {
|
|
330
|
-
const db = await openTempDb();
|
|
331
|
-
db.execute("CREATE (n:Text {val: 'hello world'})");
|
|
332
|
-
const result = db.execute("MATCH (n:Text) RETURN n.val");
|
|
333
|
-
expect(result.row(0)['n.val']).toBe('hello world');
|
|
334
|
-
db.close();
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
it('should handle node ID as BigInt', async () => {
|
|
338
|
-
const db = await openTempDb();
|
|
339
|
-
db.execute("CREATE (n:Person {name: 'Alice'})");
|
|
340
|
-
const result = db.execute("MATCH (n:Person) RETURN n");
|
|
341
|
-
const row = result.row(0);
|
|
342
|
-
// Node value should be a BigInt (node ID).
|
|
343
|
-
expect(typeof row['n']).toBe('bigint');
|
|
344
|
-
db.close();
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
it('should handle edge ID as BigInt', async () => {
|
|
348
|
-
const db = await openTempDb();
|
|
349
|
-
db.execute("CREATE (a:Person {name: 'A'})-[:KNOWS]->(b:Person {name: 'B'})");
|
|
350
|
-
const result = db.execute("MATCH ()-[r:KNOWS]->() RETURN r");
|
|
351
|
-
const row = result.row(0);
|
|
352
|
-
// Edge value should be a BigInt (edge ID).
|
|
353
|
-
expect(typeof row['r']).toBe('bigint');
|
|
354
|
-
db.close();
|
|
355
|
-
});
|
|
356
|
-
|
|
357
|
-
it('should handle parameter types (string, int, float, bool, null)', async () => {
|
|
358
|
-
const db = await openTempDb();
|
|
359
|
-
db.execute("CREATE (n:Item {name: 'test'})");
|
|
360
|
-
// String param
|
|
361
|
-
let result = db.execute(
|
|
362
|
-
"MATCH (n:Item) WHERE n.name = $v RETURN n.name",
|
|
363
|
-
{ v: 'test' }
|
|
364
|
-
);
|
|
365
|
-
expect(result.length).toBe(1);
|
|
366
|
-
// Int param
|
|
367
|
-
db.execute("CREATE (n:Num {val: 42})");
|
|
368
|
-
result = db.execute(
|
|
369
|
-
"MATCH (n:Num) WHERE n.val = $v RETURN n.val",
|
|
370
|
-
{ v: 42 }
|
|
371
|
-
);
|
|
372
|
-
expect(result.length).toBe(1);
|
|
373
|
-
db.close();
|
|
374
|
-
});
|
|
375
|
-
});
|
|
376
|
-
|
|
377
|
-
// ============================================================
|
|
378
|
-
// Error Handling
|
|
379
|
-
// ============================================================
|
|
380
|
-
|
|
381
|
-
describe('error handling', () => {
|
|
382
|
-
it('should throw on invalid Cypher syntax', async () => {
|
|
383
|
-
const db = await openTempDb();
|
|
384
|
-
expect(() => db.execute("INVALID QUERY @#$")).toThrow();
|
|
385
|
-
db.close();
|
|
386
|
-
});
|
|
387
|
-
|
|
388
|
-
it('should throw on semantic error (undefined variable)', async () => {
|
|
389
|
-
const db = await openTempDb();
|
|
390
|
-
expect(() => db.execute("MATCH (n:Person) RETURN m.name")).toThrow();
|
|
391
|
-
db.close();
|
|
392
|
-
});
|
|
393
|
-
|
|
394
|
-
it('should include error message in thrown error', async () => {
|
|
395
|
-
const db = await openTempDb();
|
|
396
|
-
try {
|
|
397
|
-
db.execute("MATCH (n:Person RETURN n");
|
|
398
|
-
expect.unreachable('should have thrown');
|
|
399
|
-
} catch (e) {
|
|
400
|
-
expect(e.message).toBeTruthy();
|
|
401
|
-
expect(typeof e.message).toBe('string');
|
|
402
|
-
}
|
|
403
|
-
db.close();
|
|
404
|
-
});
|
|
405
|
-
});
|
package/build.rs
DELETED
package/src/database.rs
DELETED
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
// Database lifecycle and query execution for Node.js.
|
|
2
|
-
|
|
3
|
-
use std::sync::atomic::{AtomicBool, Ordering};
|
|
4
|
-
use std::sync::{Arc, Mutex};
|
|
5
|
-
|
|
6
|
-
use napi::Env;
|
|
7
|
-
|
|
8
|
-
use cypherlite_core::DatabaseConfig;
|
|
9
|
-
use cypherlite_query::api::CypherLite;
|
|
10
|
-
|
|
11
|
-
use crate::error::{db_closed, mutex_poisoned, to_napi_error};
|
|
12
|
-
use crate::result::CylResult;
|
|
13
|
-
use crate::transaction::Transaction;
|
|
14
|
-
use crate::value::convert_params;
|
|
15
|
-
|
|
16
|
-
/// Options for opening a database.
|
|
17
|
-
#[napi(object)]
|
|
18
|
-
pub struct OpenOptions {
|
|
19
|
-
/// Page size in bytes (default: 4096).
|
|
20
|
-
pub page_size: Option<u32>,
|
|
21
|
-
/// Number of pages in the buffer pool cache (default: 256).
|
|
22
|
-
pub cache_capacity: Option<u32>,
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/// The main CypherLite database handle for Node.js.
|
|
26
|
-
#[napi]
|
|
27
|
-
pub struct Database {
|
|
28
|
-
inner: Arc<Mutex<Option<CypherLite>>>,
|
|
29
|
-
in_transaction: Arc<AtomicBool>,
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/// Open a CypherLite database at the given path.
|
|
33
|
-
#[napi]
|
|
34
|
-
pub fn open(path: String, options: Option<OpenOptions>) -> napi::Result<Database> {
|
|
35
|
-
let config = DatabaseConfig {
|
|
36
|
-
path: std::path::PathBuf::from(&path),
|
|
37
|
-
page_size: options.as_ref().and_then(|o| o.page_size).unwrap_or(4096),
|
|
38
|
-
cache_capacity: options
|
|
39
|
-
.as_ref()
|
|
40
|
-
.and_then(|o| o.cache_capacity)
|
|
41
|
-
.unwrap_or(256) as usize,
|
|
42
|
-
..Default::default()
|
|
43
|
-
};
|
|
44
|
-
let db = CypherLite::open(config).map_err(to_napi_error)?;
|
|
45
|
-
Ok(Database {
|
|
46
|
-
inner: Arc::new(Mutex::new(Some(db))),
|
|
47
|
-
in_transaction: Arc::new(AtomicBool::new(false)),
|
|
48
|
-
})
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
#[napi]
|
|
52
|
-
impl Database {
|
|
53
|
-
/// Execute a Cypher query string.
|
|
54
|
-
///
|
|
55
|
-
/// Optional second argument provides named parameters as a plain object.
|
|
56
|
-
#[napi]
|
|
57
|
-
pub fn execute(
|
|
58
|
-
&self,
|
|
59
|
-
env: Env,
|
|
60
|
-
query: String,
|
|
61
|
-
params: Option<napi::JsObject>,
|
|
62
|
-
) -> napi::Result<CylResult> {
|
|
63
|
-
if self.in_transaction.load(Ordering::SeqCst) {
|
|
64
|
-
return Err(napi::Error::from_reason(
|
|
65
|
-
"cannot execute on database while a transaction is active",
|
|
66
|
-
));
|
|
67
|
-
}
|
|
68
|
-
let rust_params = convert_params(&env, params)?;
|
|
69
|
-
let mut guard = self.inner.lock().map_err(|_| mutex_poisoned())?;
|
|
70
|
-
let db = guard.as_mut().ok_or_else(db_closed)?;
|
|
71
|
-
let qr = if rust_params.is_empty() {
|
|
72
|
-
db.execute(&query).map_err(to_napi_error)?
|
|
73
|
-
} else {
|
|
74
|
-
db.execute_with_params(&query, rust_params)
|
|
75
|
-
.map_err(to_napi_error)?
|
|
76
|
-
};
|
|
77
|
-
Ok(CylResult::from_query_result(qr))
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/// Close the database. Safe to call multiple times.
|
|
81
|
-
#[napi]
|
|
82
|
-
pub fn close(&self) -> napi::Result<()> {
|
|
83
|
-
let mut guard = self.inner.lock().map_err(|_| mutex_poisoned())?;
|
|
84
|
-
let _ = guard.take();
|
|
85
|
-
Ok(())
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/// Check if the database is closed.
|
|
89
|
-
#[napi(getter, js_name = "isClosed")]
|
|
90
|
-
pub fn is_closed(&self) -> napi::Result<bool> {
|
|
91
|
-
let guard = self.inner.lock().map_err(|_| mutex_poisoned())?;
|
|
92
|
-
Ok(guard.is_none())
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/// Begin a new transaction.
|
|
96
|
-
#[napi]
|
|
97
|
-
pub fn begin(&self) -> napi::Result<Transaction> {
|
|
98
|
-
// Check database is open.
|
|
99
|
-
{
|
|
100
|
-
let guard = self.inner.lock().map_err(|_| mutex_poisoned())?;
|
|
101
|
-
if guard.is_none() {
|
|
102
|
-
return Err(db_closed());
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// Check no transaction is already active.
|
|
107
|
-
if self
|
|
108
|
-
.in_transaction
|
|
109
|
-
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
|
|
110
|
-
.is_err()
|
|
111
|
-
{
|
|
112
|
-
return Err(napi::Error::from_reason("a transaction is already active"));
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
Ok(Transaction {
|
|
116
|
-
inner: Arc::clone(&self.inner),
|
|
117
|
-
in_transaction: Arc::clone(&self.in_transaction),
|
|
118
|
-
finished: false,
|
|
119
|
-
})
|
|
120
|
-
}
|
|
121
|
-
}
|
package/src/error.rs
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
// Error conversion from CypherLiteError to napi::Error.
|
|
2
|
-
|
|
3
|
-
use cypherlite_core::CypherLiteError;
|
|
4
|
-
|
|
5
|
-
/// Convert a CypherLiteError into a napi::Error suitable for throwing in JS.
|
|
6
|
-
pub fn to_napi_error(e: CypherLiteError) -> napi::Error {
|
|
7
|
-
napi::Error::from_reason(e.to_string())
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
/// Create an error for a poisoned mutex.
|
|
11
|
-
pub fn mutex_poisoned() -> napi::Error {
|
|
12
|
-
napi::Error::from_reason("internal error: mutex poisoned")
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/// Create an error for a closed database.
|
|
16
|
-
pub fn db_closed() -> napi::Error {
|
|
17
|
-
napi::Error::from_reason("database is closed")
|
|
18
|
-
}
|
package/src/lib.rs
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
// CypherLite Node.js Bindings via napi-rs.
|
|
2
|
-
//
|
|
3
|
-
// This crate exposes the CypherLite embedded graph database as a native
|
|
4
|
-
// Node.js addon built with @napi-rs/cli.
|
|
5
|
-
|
|
6
|
-
#[macro_use]
|
|
7
|
-
extern crate napi_derive;
|
|
8
|
-
|
|
9
|
-
pub mod database;
|
|
10
|
-
pub mod error;
|
|
11
|
-
pub mod result;
|
|
12
|
-
pub mod transaction;
|
|
13
|
-
pub mod value;
|
|
14
|
-
|
|
15
|
-
/// Return the library version string.
|
|
16
|
-
#[napi]
|
|
17
|
-
pub fn version() -> String {
|
|
18
|
-
env!("CARGO_PKG_VERSION").to_string()
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/// Return a comma-separated string of compiled feature flags.
|
|
22
|
-
#[napi]
|
|
23
|
-
pub fn features() -> String {
|
|
24
|
-
let mut flags = Vec::new();
|
|
25
|
-
if cfg!(feature = "temporal-core") {
|
|
26
|
-
flags.push("temporal-core");
|
|
27
|
-
}
|
|
28
|
-
if cfg!(feature = "temporal-edge") {
|
|
29
|
-
flags.push("temporal-edge");
|
|
30
|
-
}
|
|
31
|
-
if cfg!(feature = "subgraph") {
|
|
32
|
-
flags.push("subgraph");
|
|
33
|
-
}
|
|
34
|
-
if cfg!(feature = "hypergraph") {
|
|
35
|
-
flags.push("hypergraph");
|
|
36
|
-
}
|
|
37
|
-
if cfg!(feature = "full-temporal") {
|
|
38
|
-
flags.push("full-temporal");
|
|
39
|
-
}
|
|
40
|
-
if cfg!(feature = "plugin") {
|
|
41
|
-
flags.push("plugin");
|
|
42
|
-
}
|
|
43
|
-
flags.join(",")
|
|
44
|
-
}
|
package/src/result.rs
DELETED
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
// Query result wrapper for Node.js.
|
|
2
|
-
|
|
3
|
-
use cypherlite_query::api::QueryResult;
|
|
4
|
-
use cypherlite_query::executor::Value;
|
|
5
|
-
use napi::Env;
|
|
6
|
-
|
|
7
|
-
use crate::value::rust_to_js;
|
|
8
|
-
|
|
9
|
-
/// A query result containing columns and rows.
|
|
10
|
-
///
|
|
11
|
-
/// Row data is stored as Rust types and converted to JS on access.
|
|
12
|
-
#[napi]
|
|
13
|
-
pub struct CylResult {
|
|
14
|
-
columns: Vec<String>,
|
|
15
|
-
/// Each row is a Vec of Values in column order.
|
|
16
|
-
rows: Vec<Vec<Value>>,
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
impl CylResult {
|
|
20
|
-
/// Create from a Rust QueryResult.
|
|
21
|
-
pub fn from_query_result(qr: QueryResult) -> Self {
|
|
22
|
-
let columns = qr.columns;
|
|
23
|
-
let rows: Vec<Vec<Value>> = qr
|
|
24
|
-
.rows
|
|
25
|
-
.iter()
|
|
26
|
-
.map(|row| {
|
|
27
|
-
columns
|
|
28
|
-
.iter()
|
|
29
|
-
.map(|col| row.get(col).cloned().unwrap_or(Value::Null))
|
|
30
|
-
.collect()
|
|
31
|
-
})
|
|
32
|
-
.collect();
|
|
33
|
-
Self { columns, rows }
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
#[napi]
|
|
38
|
-
impl CylResult {
|
|
39
|
-
/// Column names as an array of strings.
|
|
40
|
-
#[napi(getter)]
|
|
41
|
-
pub fn columns(&self) -> Vec<String> {
|
|
42
|
-
self.columns.clone()
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/// Number of rows in the result.
|
|
46
|
-
#[napi(getter)]
|
|
47
|
-
pub fn length(&self) -> u32 {
|
|
48
|
-
self.rows.len() as u32
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/// Get a single row by index as a plain JS object.
|
|
52
|
-
#[napi]
|
|
53
|
-
pub fn row(&self, env: Env, index: u32) -> napi::Result<napi::JsObject> {
|
|
54
|
-
let idx = index as usize;
|
|
55
|
-
if idx >= self.rows.len() {
|
|
56
|
-
return Err(napi::Error::from_reason(format!(
|
|
57
|
-
"row index out of range: {} >= {}",
|
|
58
|
-
idx,
|
|
59
|
-
self.rows.len()
|
|
60
|
-
)));
|
|
61
|
-
}
|
|
62
|
-
self.row_to_object(&env, idx)
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/// Convert all rows to an array of plain JS objects.
|
|
66
|
-
#[napi(js_name = "toArray")]
|
|
67
|
-
pub fn to_array(&self, env: Env) -> napi::Result<Vec<napi::JsObject>> {
|
|
68
|
-
let mut result = Vec::with_capacity(self.rows.len());
|
|
69
|
-
for i in 0..self.rows.len() {
|
|
70
|
-
result.push(self.row_to_object(&env, i)?);
|
|
71
|
-
}
|
|
72
|
-
Ok(result)
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
impl CylResult {
|
|
77
|
-
/// Build a JS object for a single row.
|
|
78
|
-
fn row_to_object(&self, env: &Env, row_idx: usize) -> napi::Result<napi::JsObject> {
|
|
79
|
-
let mut obj = env.create_object()?;
|
|
80
|
-
let row = &self.rows[row_idx];
|
|
81
|
-
for (i, col) in self.columns.iter().enumerate() {
|
|
82
|
-
let js_val = rust_to_js(env, &row[i])?;
|
|
83
|
-
obj.set_named_property(col, js_val)?;
|
|
84
|
-
}
|
|
85
|
-
Ok(obj)
|
|
86
|
-
}
|
|
87
|
-
}
|
package/src/transaction.rs
DELETED
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
// Transaction wrapper for Node.js.
|
|
2
|
-
|
|
3
|
-
use std::sync::atomic::{AtomicBool, Ordering};
|
|
4
|
-
use std::sync::{Arc, Mutex};
|
|
5
|
-
|
|
6
|
-
use napi::Env;
|
|
7
|
-
|
|
8
|
-
use cypherlite_query::api::CypherLite;
|
|
9
|
-
|
|
10
|
-
use crate::error::{db_closed, mutex_poisoned, to_napi_error};
|
|
11
|
-
use crate::result::CylResult;
|
|
12
|
-
use crate::value::convert_params;
|
|
13
|
-
|
|
14
|
-
/// A transaction wrapping CypherLite execute calls.
|
|
15
|
-
///
|
|
16
|
-
/// Shares the database Mutex with the parent Database object.
|
|
17
|
-
#[napi]
|
|
18
|
-
pub struct Transaction {
|
|
19
|
-
pub(crate) inner: Arc<Mutex<Option<CypherLite>>>,
|
|
20
|
-
pub(crate) in_transaction: Arc<AtomicBool>,
|
|
21
|
-
pub(crate) finished: bool,
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
impl Drop for Transaction {
|
|
25
|
-
fn drop(&mut self) {
|
|
26
|
-
// Auto-rollback: clear the transaction flag.
|
|
27
|
-
self.finish();
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
impl Transaction {
|
|
32
|
-
/// Mark this transaction as finished and clear the in_transaction flag.
|
|
33
|
-
fn finish(&mut self) {
|
|
34
|
-
if !self.finished {
|
|
35
|
-
self.finished = true;
|
|
36
|
-
self.in_transaction.store(false, Ordering::SeqCst);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
#[napi]
|
|
42
|
-
impl Transaction {
|
|
43
|
-
/// Execute a Cypher query within this transaction.
|
|
44
|
-
#[napi]
|
|
45
|
-
pub fn execute(
|
|
46
|
-
&mut self,
|
|
47
|
-
env: Env,
|
|
48
|
-
query: String,
|
|
49
|
-
params: Option<napi::JsObject>,
|
|
50
|
-
) -> napi::Result<CylResult> {
|
|
51
|
-
if self.finished {
|
|
52
|
-
return Err(napi::Error::from_reason("transaction is already finished"));
|
|
53
|
-
}
|
|
54
|
-
let rust_params = convert_params(&env, params)?;
|
|
55
|
-
let mut guard = self.inner.lock().map_err(|_| mutex_poisoned())?;
|
|
56
|
-
let db = guard.as_mut().ok_or_else(db_closed)?;
|
|
57
|
-
let qr = if rust_params.is_empty() {
|
|
58
|
-
db.execute(&query).map_err(to_napi_error)?
|
|
59
|
-
} else {
|
|
60
|
-
db.execute_with_params(&query, rust_params)
|
|
61
|
-
.map_err(to_napi_error)?
|
|
62
|
-
};
|
|
63
|
-
Ok(CylResult::from_query_result(qr))
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/// Commit the transaction.
|
|
67
|
-
#[napi]
|
|
68
|
-
pub fn commit(&mut self) -> napi::Result<()> {
|
|
69
|
-
if self.finished {
|
|
70
|
-
return Err(napi::Error::from_reason("transaction is already finished"));
|
|
71
|
-
}
|
|
72
|
-
self.finish();
|
|
73
|
-
Ok(())
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/// Rollback the transaction (Phase 2: no-op at storage level).
|
|
77
|
-
#[napi]
|
|
78
|
-
pub fn rollback(&mut self) -> napi::Result<()> {
|
|
79
|
-
if self.finished {
|
|
80
|
-
return Err(napi::Error::from_reason("transaction is already finished"));
|
|
81
|
-
}
|
|
82
|
-
self.finish();
|
|
83
|
-
Ok(())
|
|
84
|
-
}
|
|
85
|
-
}
|
package/src/value.rs
DELETED
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
// Value conversion between Rust executor::Value and JavaScript values.
|
|
2
|
-
|
|
3
|
-
use cypherlite_query::executor::Value;
|
|
4
|
-
use napi::{Env, JsObject, JsUnknown, ValueType};
|
|
5
|
-
|
|
6
|
-
/// Create a JS BigInt from a u64 entity ID.
|
|
7
|
-
fn u64_to_bigint(env: &Env, id: u64) -> napi::Result<JsUnknown> {
|
|
8
|
-
// Node IDs are unsigned; create_bigint_from_words takes (sign, words).
|
|
9
|
-
let (sign, words) = if (id as i64) < 0 {
|
|
10
|
-
(true, vec![id])
|
|
11
|
-
} else {
|
|
12
|
-
(false, vec![id])
|
|
13
|
-
};
|
|
14
|
-
env.create_bigint_from_words(sign, words)?.into_unknown()
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/// Convert a Rust Value to a JavaScript value.
|
|
18
|
-
pub fn rust_to_js(env: &Env, val: &Value) -> napi::Result<JsUnknown> {
|
|
19
|
-
match val {
|
|
20
|
-
Value::Null => Ok(env.get_null()?.into_unknown()),
|
|
21
|
-
Value::Bool(b) => Ok(env.get_boolean(*b)?.into_unknown()),
|
|
22
|
-
Value::Int64(i) => Ok(env.create_int64(*i)?.into_unknown()),
|
|
23
|
-
Value::Float64(f) => Ok(env.create_double(*f)?.into_unknown()),
|
|
24
|
-
Value::String(s) => Ok(env.create_string(s)?.into_unknown()),
|
|
25
|
-
Value::Bytes(b) => {
|
|
26
|
-
let buf = env.create_buffer_with_data(b.clone())?;
|
|
27
|
-
Ok(buf.into_unknown())
|
|
28
|
-
}
|
|
29
|
-
Value::List(items) => {
|
|
30
|
-
let mut arr = env.create_array_with_length(items.len())?;
|
|
31
|
-
for (i, item) in items.iter().enumerate() {
|
|
32
|
-
let js_val = rust_to_js(env, item)?;
|
|
33
|
-
arr.set_element(i as u32, js_val)?;
|
|
34
|
-
}
|
|
35
|
-
Ok(arr.into_unknown())
|
|
36
|
-
}
|
|
37
|
-
Value::Node(id) => u64_to_bigint(env, id.0),
|
|
38
|
-
Value::Edge(id) => u64_to_bigint(env, id.0),
|
|
39
|
-
Value::DateTime(ms) => Ok(env.create_int64(*ms)?.into_unknown()),
|
|
40
|
-
#[cfg(feature = "subgraph")]
|
|
41
|
-
Value::Subgraph(id) => u64_to_bigint(env, id.0),
|
|
42
|
-
#[cfg(feature = "hypergraph")]
|
|
43
|
-
Value::Hyperedge(id) => u64_to_bigint(env, id.0),
|
|
44
|
-
#[cfg(feature = "hypergraph")]
|
|
45
|
-
Value::TemporalNode(id, ms) => {
|
|
46
|
-
// Return as a plain object with nodeId and timestamp fields.
|
|
47
|
-
let mut obj = env.create_object()?;
|
|
48
|
-
let bigint = u64_to_bigint(env, id.0)?;
|
|
49
|
-
obj.set_named_property("nodeId", bigint)?;
|
|
50
|
-
obj.set_named_property("timestamp", env.create_int64(*ms)?)?;
|
|
51
|
-
Ok(obj.into_unknown())
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/// Convert a JavaScript value to a Rust Value.
|
|
57
|
-
#[allow(clippy::only_used_in_recursion)]
|
|
58
|
-
pub fn js_to_rust(env: &Env, val: JsUnknown) -> napi::Result<Value> {
|
|
59
|
-
match val.get_type()? {
|
|
60
|
-
ValueType::Null | ValueType::Undefined => Ok(Value::Null),
|
|
61
|
-
ValueType::Boolean => {
|
|
62
|
-
let b = val.coerce_to_bool()?.get_value()?;
|
|
63
|
-
Ok(Value::Bool(b))
|
|
64
|
-
}
|
|
65
|
-
ValueType::Number => {
|
|
66
|
-
let n = val.coerce_to_number()?;
|
|
67
|
-
let f = n.get_double()?;
|
|
68
|
-
// If the number is an integer (no fractional part and within i64 range),
|
|
69
|
-
// store as Int64 for better compatibility with Cypher integer semantics.
|
|
70
|
-
if f.fract() == 0.0 && f >= i64::MIN as f64 && f <= i64::MAX as f64 {
|
|
71
|
-
Ok(Value::Int64(f as i64))
|
|
72
|
-
} else {
|
|
73
|
-
Ok(Value::Float64(f))
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
ValueType::String => {
|
|
77
|
-
let s = val.coerce_to_string()?.into_utf8()?;
|
|
78
|
-
Ok(Value::String(s.as_str()?.to_string()))
|
|
79
|
-
}
|
|
80
|
-
ValueType::BigInt => {
|
|
81
|
-
let mut bigint = unsafe { val.cast::<napi::JsBigInt>() };
|
|
82
|
-
let (signed, value) = bigint.get_words()?;
|
|
83
|
-
// Simple conversion: take the first word as u64, apply sign.
|
|
84
|
-
let raw = value.first().copied().unwrap_or(0);
|
|
85
|
-
let result = if signed { -(raw as i64) } else { raw as i64 };
|
|
86
|
-
Ok(Value::Int64(result))
|
|
87
|
-
}
|
|
88
|
-
ValueType::Object => {
|
|
89
|
-
// Check if it is an array.
|
|
90
|
-
let obj: JsObject = unsafe { val.cast() };
|
|
91
|
-
if obj.is_array()? {
|
|
92
|
-
let len = obj.get_array_length()?;
|
|
93
|
-
let mut items = Vec::with_capacity(len as usize);
|
|
94
|
-
for i in 0..len {
|
|
95
|
-
let elem: JsUnknown = obj.get_element(i)?;
|
|
96
|
-
items.push(js_to_rust(env, elem)?);
|
|
97
|
-
}
|
|
98
|
-
return Ok(Value::List(items));
|
|
99
|
-
}
|
|
100
|
-
// Check if it is a Buffer.
|
|
101
|
-
if obj.is_buffer()? {
|
|
102
|
-
let unknown = obj.into_unknown();
|
|
103
|
-
let buf = napi::JsBuffer::try_from(unknown)?;
|
|
104
|
-
let data = buf.into_value()?;
|
|
105
|
-
return Ok(Value::Bytes(data.to_vec()));
|
|
106
|
-
}
|
|
107
|
-
Err(napi::Error::from_reason(
|
|
108
|
-
"cannot convert JS object to CypherLite value",
|
|
109
|
-
))
|
|
110
|
-
}
|
|
111
|
-
_ => Err(napi::Error::from_reason(format!(
|
|
112
|
-
"cannot convert JS type {:?} to CypherLite value",
|
|
113
|
-
val.get_type()?
|
|
114
|
-
))),
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/// Convert a JS object of params ({ key: value, ... }) to a HashMap.
|
|
119
|
-
pub fn convert_params(
|
|
120
|
-
env: &Env,
|
|
121
|
-
params: Option<JsObject>,
|
|
122
|
-
) -> napi::Result<std::collections::HashMap<String, Value>> {
|
|
123
|
-
let Some(obj) = params else {
|
|
124
|
-
return Ok(std::collections::HashMap::new());
|
|
125
|
-
};
|
|
126
|
-
let keys = obj.get_property_names()?;
|
|
127
|
-
let len = keys.get_array_length()?;
|
|
128
|
-
let mut map = std::collections::HashMap::with_capacity(len as usize);
|
|
129
|
-
for i in 0..len {
|
|
130
|
-
let key: napi::JsString = keys.get_element(i)?;
|
|
131
|
-
let key_str = key.into_utf8()?.as_str()?.to_string();
|
|
132
|
-
let val: JsUnknown = obj.get_named_property(&key_str)?;
|
|
133
|
-
map.insert(key_str, js_to_rust(env, val)?);
|
|
134
|
-
}
|
|
135
|
-
Ok(map)
|
|
136
|
-
}
|