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 CHANGED
@@ -280,9 +280,9 @@ checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
280
280
 
281
281
  [[package]]
282
282
  name = "minigraf"
283
- version = "1.2.2"
283
+ version = "1.2.3"
284
284
  source = "registry+https://github.com/rust-lang/crates.io-index"
285
- checksum = "1e62df0c250d941359d9610c25cdde804e4ade914694089e8b6e1c46b25251b0"
285
+ checksum = "ee16db19bf63c1bc6c66fc75993a6e4b8cf7e253d4e5643b44923a939745a913"
286
286
  dependencies = [
287
287
  "anyhow",
288
288
  "chrono",
package/Cargo.toml CHANGED
@@ -10,7 +10,7 @@ crate-type = ["cdylib"]
10
10
  [dependencies]
11
11
  napi = { version = "3.8", features = ["napi6"] }
12
12
  napi-derive = "3"
13
- minigraf = { version = "=1.2.2" }
13
+ minigraf = { version = "=1.2.3" }
14
14
  serde_json = "1.0"
15
15
 
16
16
  [build-dependencies]
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
- /** Open a file-backed database. Throws on error. */
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minigraf",
3
- "version": "1.2.2",
3
+ "version": "1.2.3",
4
4
  "description": "Zero-config, single-file, embedded graph database with bi-temporal Datalog queries",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@minigraf/darwin-universal",
3
- "version": "1.2.2",
3
+ "version": "1.2.3",
4
4
  "description": "Zero-config, single-file, embedded graph database with bi-temporal Datalog queries (macOS universal)",
5
5
  "os": [
6
6
  "darwin"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@minigraf/linux-arm64-gnu",
3
- "version": "1.2.2",
3
+ "version": "1.2.3",
4
4
  "description": "Zero-config, single-file, embedded graph database with bi-temporal Datalog queries (Linux arm64 GNU)",
5
5
  "os": [
6
6
  "linux"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@minigraf/linux-x64-gnu",
3
- "version": "1.2.2",
3
+ "version": "1.2.3",
4
4
  "description": "Zero-config, single-file, embedded graph database with bi-temporal Datalog queries (Linux x64 GNU)",
5
5
  "os": [
6
6
  "linux"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@minigraf/win32-x64-msvc",
3
- "version": "1.2.2",
3
+ "version": "1.2.3",
4
4
  "description": "Zero-config, single-file, embedded graph database with bi-temporal Datalog queries (Windows x64 MSVC)",
5
5
  "os": [
6
6
  "win32"
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
- inner: Arc<Mutex<minigraf::Minigraf>>,
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 result = self
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.inner
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
  }
@@ -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
  })