minigraf 1.2.2 → 1.2.3
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/Cargo.lock +2 -2
- package/Cargo.toml +1 -1
- package/README.md +25 -0
- package/index.d.ts +37 -1
- package/package.json +1 -1
- package/packages/@minigraf/darwin-universal/package.json +1 -1
- package/packages/@minigraf/linux-arm64-gnu/package.json +1 -1
- package/packages/@minigraf/linux-x64-gnu/package.json +1 -1
- package/packages/@minigraf/win32-x64-msvc/package.json +1 -1
- package/src/lib.rs +63 -7
- package/test/basic.test.mjs +55 -0
package/Cargo.lock
CHANGED
|
@@ -280,9 +280,9 @@ checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
|
|
|
280
280
|
|
|
281
281
|
[[package]]
|
|
282
282
|
name = "minigraf"
|
|
283
|
-
version = "1.2.
|
|
283
|
+
version = "1.2.3"
|
|
284
284
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
285
|
-
checksum = "
|
|
285
|
+
checksum = "ee16db19bf63c1bc6c66fc75993a6e4b8cf7e253d4e5643b44923a939745a913"
|
|
286
286
|
dependencies = [
|
|
287
287
|
"anyhow",
|
|
288
288
|
"chrono",
|
package/Cargo.toml
CHANGED
package/README.md
CHANGED
|
@@ -34,8 +34,33 @@ console.log(result.results[0][0]) // "Alice"
|
|
|
34
34
|
const db2 = new MiniGrafDb('path/to/mydb.graph')
|
|
35
35
|
db2.execute('(transact [[:bob :name "Bob"]])')
|
|
36
36
|
db2.checkpoint()
|
|
37
|
+
db2.close()
|
|
37
38
|
```
|
|
38
39
|
|
|
40
|
+
## Closing a file-backed database
|
|
41
|
+
|
|
42
|
+
Only one handle per `.graph` file may be open in a process at a time — two
|
|
43
|
+
would each cache their own page table and corrupt the file — so call `close()`
|
|
44
|
+
when you are done with one:
|
|
45
|
+
|
|
46
|
+
```js
|
|
47
|
+
const db = new MiniGrafDb('mydb.graph')
|
|
48
|
+
db.execute('(transact [[:bob :name "Bob"]])')
|
|
49
|
+
db.checkpoint()
|
|
50
|
+
db.close()
|
|
51
|
+
|
|
52
|
+
const again = new MiniGrafDb('mydb.graph') // fine: the first handle is gone
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Letting the variable go out of scope is **not** enough. JavaScript has no
|
|
56
|
+
deterministic destructor: the object becomes unreachable, but the underlying
|
|
57
|
+
handle survives until V8 garbage-collects it, which may be arbitrarily later or
|
|
58
|
+
not at all before the process exits. Reopening before then throws
|
|
59
|
+
`Database is already open in this process`.
|
|
60
|
+
|
|
61
|
+
`close()` is idempotent; every other method throws once it has run. In-memory
|
|
62
|
+
databases hold no file lock, so closing them is optional.
|
|
63
|
+
|
|
39
64
|
## Building from source
|
|
40
65
|
|
|
41
66
|
Requires Rust stable toolchain and `@napi-rs/cli`.
|
package/index.d.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
/* auto-generated by NAPI-RS */
|
|
2
2
|
/* eslint-disable */
|
|
3
3
|
export declare class MiniGrafDb {
|
|
4
|
-
/**
|
|
4
|
+
/**
|
|
5
|
+
* Open a file-backed database. Throws on error.
|
|
6
|
+
*
|
|
7
|
+
* Only one handle per file may be open in a process at a time. Call
|
|
8
|
+
* `close()` before reopening the same path — see `close()`.
|
|
9
|
+
*/
|
|
5
10
|
constructor(path: string)
|
|
6
11
|
/** Open an in-memory database. Throws on error. */
|
|
7
12
|
static inMemory(): MiniGrafDb
|
|
@@ -9,4 +14,35 @@ export declare class MiniGrafDb {
|
|
|
9
14
|
execute(datalog: string): string
|
|
10
15
|
/** Flush the WAL to disk. Throws on error. */
|
|
11
16
|
checkpoint(): void
|
|
17
|
+
/**
|
|
18
|
+
* Close the database, releasing its file lock immediately.
|
|
19
|
+
*
|
|
20
|
+
* Letting a `MiniGrafDb` go out of scope is **not** enough to release the
|
|
21
|
+
* file. JavaScript has no deterministic destructor: the object merely
|
|
22
|
+
* becomes unreachable, and the underlying handle lives until V8 garbage
|
|
23
|
+
* collects it — arbitrarily later, or never before the process exits.
|
|
24
|
+
*
|
|
25
|
+
* That matters because minigraf permits only one live handle per file per
|
|
26
|
+
* process (two would each cache their own page table and corrupt each
|
|
27
|
+
* other), so opening the same path again throws until the first handle is
|
|
28
|
+
* really gone. `close()` is the only way to make that happen on demand.
|
|
29
|
+
* Every other binding has the same escape hatch: UniFFI (Python, Java,
|
|
30
|
+
* Swift) exposes `destroy()`, and the C API exposes `minigraf_close`.
|
|
31
|
+
*
|
|
32
|
+
* Call `checkpoint()` first if you need to know the WAL was compacted;
|
|
33
|
+
* closing does that too, but cannot report a failure.
|
|
34
|
+
*
|
|
35
|
+
* Idempotent — closing an already-closed database is a no-op. Every other
|
|
36
|
+
* method throws afterwards.
|
|
37
|
+
*
|
|
38
|
+
* ```js
|
|
39
|
+
* const db = new MiniGrafDb(path)
|
|
40
|
+
* db.execute('(transact [[:bob :name "Bob"]])')
|
|
41
|
+
* db.checkpoint()
|
|
42
|
+
* db.close()
|
|
43
|
+
*
|
|
44
|
+
* const db2 = new MiniGrafDb(path) // fine: the first handle is gone
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
close(): void
|
|
12
48
|
}
|
package/package.json
CHANGED
package/src/lib.rs
CHANGED
|
@@ -42,20 +42,29 @@ fn query_result_to_json(result: QueryResult) -> String {
|
|
|
42
42
|
|
|
43
43
|
// ─── MiniGrafDb ───────────────────────────────────────────────────────────────
|
|
44
44
|
|
|
45
|
+
/// Thrown by every method once `close()` has run.
|
|
46
|
+
const CLOSED_MSG: &str = "database is closed";
|
|
47
|
+
|
|
45
48
|
#[napi]
|
|
46
49
|
pub struct MiniGrafDb {
|
|
47
|
-
|
|
50
|
+
/// `None` once `close()` has run. JavaScript has no deterministic
|
|
51
|
+
/// destructor, so the handle needs an explicit way to be dropped — see
|
|
52
|
+
/// `close()`.
|
|
53
|
+
inner: Arc<Mutex<Option<minigraf::Minigraf>>>,
|
|
48
54
|
}
|
|
49
55
|
|
|
50
56
|
#[napi]
|
|
51
57
|
impl MiniGrafDb {
|
|
52
58
|
/// Open a file-backed database. Throws on error.
|
|
59
|
+
///
|
|
60
|
+
/// Only one handle per file may be open in a process at a time. Call
|
|
61
|
+
/// `close()` before reopening the same path — see `close()`.
|
|
53
62
|
#[napi(constructor)]
|
|
54
63
|
pub fn new(path: String) -> Result<Self> {
|
|
55
64
|
let db = minigraf::Minigraf::open(&path)
|
|
56
65
|
.map_err(|e| Error::new(Status::GenericFailure, e.to_string()))?;
|
|
57
66
|
Ok(Self {
|
|
58
|
-
inner: Arc::new(Mutex::new(db)),
|
|
67
|
+
inner: Arc::new(Mutex::new(Some(db))),
|
|
59
68
|
})
|
|
60
69
|
}
|
|
61
70
|
|
|
@@ -65,17 +74,20 @@ impl MiniGrafDb {
|
|
|
65
74
|
let db = minigraf::Minigraf::in_memory()
|
|
66
75
|
.map_err(|e| Error::new(Status::GenericFailure, e.to_string()))?;
|
|
67
76
|
Ok(Self {
|
|
68
|
-
inner: Arc::new(Mutex::new(db)),
|
|
77
|
+
inner: Arc::new(Mutex::new(Some(db))),
|
|
69
78
|
})
|
|
70
79
|
}
|
|
71
80
|
|
|
72
81
|
/// Execute a Datalog string. Returns a JSON string. Throws on error.
|
|
73
82
|
#[napi]
|
|
74
83
|
pub fn execute(&self, datalog: String) -> Result<String> {
|
|
75
|
-
let
|
|
84
|
+
let guard = self
|
|
76
85
|
.inner
|
|
77
86
|
.lock()
|
|
78
|
-
.map_err(|_| Error::new(Status::GenericFailure, "mutex poisoned"))
|
|
87
|
+
.map_err(|_| Error::new(Status::GenericFailure, "mutex poisoned"))?;
|
|
88
|
+
let result = guard
|
|
89
|
+
.as_ref()
|
|
90
|
+
.ok_or_else(|| Error::new(Status::GenericFailure, CLOSED_MSG))?
|
|
79
91
|
.execute(&datalog)
|
|
80
92
|
.map_err(|e| Error::new(Status::GenericFailure, e.to_string()))?;
|
|
81
93
|
Ok(query_result_to_json(result))
|
|
@@ -84,10 +96,54 @@ impl MiniGrafDb {
|
|
|
84
96
|
/// Flush the WAL to disk. Throws on error.
|
|
85
97
|
#[napi]
|
|
86
98
|
pub fn checkpoint(&self) -> Result<()> {
|
|
87
|
-
self
|
|
99
|
+
let guard = self
|
|
100
|
+
.inner
|
|
88
101
|
.lock()
|
|
89
|
-
.map_err(|_| Error::new(Status::GenericFailure, "mutex poisoned"))
|
|
102
|
+
.map_err(|_| Error::new(Status::GenericFailure, "mutex poisoned"))?;
|
|
103
|
+
guard
|
|
104
|
+
.as_ref()
|
|
105
|
+
.ok_or_else(|| Error::new(Status::GenericFailure, CLOSED_MSG))?
|
|
90
106
|
.checkpoint()
|
|
91
107
|
.map_err(|e| Error::new(Status::GenericFailure, e.to_string()))
|
|
92
108
|
}
|
|
109
|
+
|
|
110
|
+
/// Close the database, releasing its file lock immediately.
|
|
111
|
+
///
|
|
112
|
+
/// Letting a `MiniGrafDb` go out of scope is **not** enough to release the
|
|
113
|
+
/// file. JavaScript has no deterministic destructor: the object merely
|
|
114
|
+
/// becomes unreachable, and the underlying handle lives until V8 garbage
|
|
115
|
+
/// collects it — arbitrarily later, or never before the process exits.
|
|
116
|
+
///
|
|
117
|
+
/// That matters because minigraf permits only one live handle per file per
|
|
118
|
+
/// process (two would each cache their own page table and corrupt each
|
|
119
|
+
/// other), so opening the same path again throws until the first handle is
|
|
120
|
+
/// really gone. `close()` is the only way to make that happen on demand.
|
|
121
|
+
/// Every other binding has the same escape hatch: UniFFI (Python, Java,
|
|
122
|
+
/// Swift) exposes `destroy()`, and the C API exposes `minigraf_close`.
|
|
123
|
+
///
|
|
124
|
+
/// Call `checkpoint()` first if you need to know the WAL was compacted;
|
|
125
|
+
/// closing does that too, but cannot report a failure.
|
|
126
|
+
///
|
|
127
|
+
/// Idempotent — closing an already-closed database is a no-op. Every other
|
|
128
|
+
/// method throws afterwards.
|
|
129
|
+
///
|
|
130
|
+
/// ```js
|
|
131
|
+
/// const db = new MiniGrafDb(path)
|
|
132
|
+
/// db.execute('(transact [[:bob :name "Bob"]])')
|
|
133
|
+
/// db.checkpoint()
|
|
134
|
+
/// db.close()
|
|
135
|
+
///
|
|
136
|
+
/// const db2 = new MiniGrafDb(path) // fine: the first handle is gone
|
|
137
|
+
/// ```
|
|
138
|
+
#[napi]
|
|
139
|
+
pub fn close(&self) -> Result<()> {
|
|
140
|
+
let mut guard = self
|
|
141
|
+
.inner
|
|
142
|
+
.lock()
|
|
143
|
+
.map_err(|_| Error::new(Status::GenericFailure, "mutex poisoned"))?;
|
|
144
|
+
// Dropping the last Minigraf clone runs its Drop impl: a best-effort
|
|
145
|
+
// checkpoint, then release of the sidecar `.graph.lock`.
|
|
146
|
+
*guard = None;
|
|
147
|
+
Ok(())
|
|
148
|
+
}
|
|
93
149
|
}
|
package/test/basic.test.mjs
CHANGED
|
@@ -41,9 +41,64 @@ test('file-backed roundtrip', async (t) => {
|
|
|
41
41
|
const db = new MiniGrafDb(dbPath)
|
|
42
42
|
db.execute('(transact [[:bob :name "Bob"]])')
|
|
43
43
|
db.checkpoint()
|
|
44
|
+
// Required, not tidiness: leaving the block makes `db` unreachable but does
|
|
45
|
+
// not free the handle — V8 frees it whenever it next collects. minigraf
|
|
46
|
+
// allows one live handle per file per process, so without this the reopen
|
|
47
|
+
// below throws "Database is already open in this process".
|
|
48
|
+
db.close()
|
|
44
49
|
}
|
|
45
50
|
|
|
46
51
|
const db2 = new MiniGrafDb(dbPath)
|
|
47
52
|
const qr = JSON.parse(db2.execute('(query [:find ?n :where [?e :name ?n]])'))
|
|
48
53
|
assert.equal(qr.results[0][0], 'Bob')
|
|
54
|
+
db2.close()
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('reopening without close throws', async (t) => {
|
|
58
|
+
const os = await import('node:os')
|
|
59
|
+
const path = await import('node:path')
|
|
60
|
+
const fs = await import('node:fs')
|
|
61
|
+
|
|
62
|
+
const dbPath = path.default.join(
|
|
63
|
+
os.default.tmpdir(),
|
|
64
|
+
`minigraf_node_noclose_${Date.now()}.graph`,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
const db = new MiniGrafDb(dbPath)
|
|
68
|
+
t.after(() => {
|
|
69
|
+
try { db.close() } catch {}
|
|
70
|
+
try { fs.default.unlinkSync(dbPath) } catch {}
|
|
71
|
+
try { fs.default.unlinkSync(dbPath + '.wal') } catch {}
|
|
72
|
+
try { fs.default.unlinkSync(dbPath + '.lock') } catch {}
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
// Two live handles would each cache their own page table and corrupt the
|
|
76
|
+
// file, so this must fail loudly rather than succeed.
|
|
77
|
+
assert.throws(
|
|
78
|
+
() => new MiniGrafDb(dbPath),
|
|
79
|
+
/already open in this process/,
|
|
80
|
+
)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
test('close is idempotent and use-after-close throws', async (t) => {
|
|
84
|
+
const os = await import('node:os')
|
|
85
|
+
const path = await import('node:path')
|
|
86
|
+
const fs = await import('node:fs')
|
|
87
|
+
|
|
88
|
+
const dbPath = path.default.join(
|
|
89
|
+
os.default.tmpdir(),
|
|
90
|
+
`minigraf_node_closed_${Date.now()}.graph`,
|
|
91
|
+
)
|
|
92
|
+
t.after(() => {
|
|
93
|
+
try { fs.default.unlinkSync(dbPath) } catch {}
|
|
94
|
+
try { fs.default.unlinkSync(dbPath + '.wal') } catch {}
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
const db = new MiniGrafDb(dbPath)
|
|
98
|
+
db.execute('(transact [[:bob :name "Bob"]])')
|
|
99
|
+
db.close()
|
|
100
|
+
db.close() // idempotent
|
|
101
|
+
|
|
102
|
+
assert.throws(() => db.execute('(query [:find ?n :where [?e :name ?n]])'), /closed/)
|
|
103
|
+
assert.throws(() => db.checkpoint(), /closed/)
|
|
49
104
|
})
|