dirsql 0.3.25 → 0.3.26

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/docs/api/index.md CHANGED
@@ -188,11 +188,12 @@ import { Table } from 'dirsql';
188
188
  ::: code-group
189
189
 
190
190
  ```python [Python]
191
- Table(*, ddl: str, glob: str, extract: Callable[[str], list[dict]])
191
+ Table(*, ddl: str, glob: str, extract: Callable[[str], list[dict]], strict: bool = False)
192
192
  ```
193
193
 
194
194
  ```rust [Rust]
195
- Table::new(ddl: &str, glob: &str, extract: fn(&str) -> Vec<Value>)
195
+ // Row = HashMap<String, Value>
196
+ Table::new(ddl: &str, glob: &str, extract: fn(&str) -> Vec<HashMap<String, Value>>)
196
197
  ```
197
198
 
198
199
  ```typescript [TypeScript]
@@ -207,7 +208,8 @@ Defines a mapping from files to SQLite table rows.
207
208
 
208
209
  - `ddl` -- A `CREATE TABLE` statement. The table name is parsed from this DDL.
209
210
  - `glob` -- A glob pattern matched against file paths relative to the root directory.
210
- - `extract` -- A callable `(path) -> list[dict]`. Receives the absolute filesystem path of the matched file. `dirsql` does not read file contents; a callback that needs the file body reads `path` itself. Returns a list of dicts/maps mapping column names to values. Return an empty list to skip a file.
211
+ - `extract` -- A callable `(path) -> list[dict]`. Receives the path of the matched file -- relative to the scan root, or absolute when `root` is absolute. `dirsql` does not read file contents; a callback that needs the file body reads `path` itself. Returns a list of dicts/maps mapping column names to values. Return an empty list to skip a file.
212
+ - `strict` -- Optional (default `False`). Controls row/schema validation. In the default relaxed mode, extra row keys are dropped and missing columns become `NULL`. When `True`, every row key must be a valid column identifier and any extra or missing key raises an error. Surfaced in [serialization](#serialization) above as part of each table's `{ ddl, glob, strict }`.
211
213
 
212
214
  **Attributes:**
213
215
 
@@ -231,7 +233,7 @@ use dirsql::RowEvent;
231
233
  ```
232
234
 
233
235
  ```typescript [TypeScript]
234
- import { RowEvent } from 'dirsql';
236
+ import type { RowEvent } from 'dirsql';
235
237
  ```
236
238
 
237
239
  :::
@@ -171,7 +171,7 @@ const results = await db.query(`
171
171
 
172
172
  1. `dirsql` walks the directory tree
173
173
  2. Files matching each table's glob pattern are identified
174
- 3. The `extract` function receives each matched file's absolute path and returns rows
174
+ 3. The `extract` function receives each matched file's path (relative to the scan root, or absolute when `root` is absolute) and returns rows
175
175
  4. Rows are inserted into an in-memory SQLite database
176
176
  5. SQL queries run against that database
177
177
 
@@ -28,6 +28,7 @@ async def main():
28
28
  ),
29
29
  ],
30
30
  )
31
+ await db.ready()
31
32
 
32
33
  # Query (runs in a thread, does not block the event loop)
33
34
  results = await db.query("SELECT * FROM items WHERE value > 10")
@@ -37,7 +38,31 @@ asyncio.run(main())
37
38
  ```
38
39
 
39
40
  ```rust [Rust]
40
- use dirsql::{DirSQL, Table};
41
+ use dirsql::{DirSQL, Table, Value};
42
+ use std::collections::HashMap;
43
+
44
+ // See `row_from_json` in getting-started.md for a reusable helper that
45
+ // turns a JSON object into a dirsql row (dirsql::Value is not Deserialize,
46
+ // so a row can't be produced by serde_json::from_str directly).
47
+ fn row_from_json(raw: &str) -> HashMap<String, Value> {
48
+ let v: serde_json::Value = serde_json::from_str(raw).unwrap();
49
+ let serde_json::Value::Object(obj) = v else { return HashMap::new() };
50
+ obj.into_iter()
51
+ .map(|(k, val)| {
52
+ let v = match val {
53
+ serde_json::Value::String(s) => Value::Text(s),
54
+ serde_json::Value::Number(n) => n
55
+ .as_i64()
56
+ .map(Value::Integer)
57
+ .unwrap_or_else(|| Value::Real(n.as_f64().unwrap_or(0.0))),
58
+ serde_json::Value::Bool(b) => Value::Integer(b as i64),
59
+ serde_json::Value::Null => Value::Null,
60
+ other => Value::Text(other.to_string()),
61
+ };
62
+ (k, v)
63
+ })
64
+ .collect()
65
+ }
41
66
 
42
67
  #[tokio::main]
43
68
  async fn main() -> Result<(), Box<dyn std::error::Error>> {
@@ -47,7 +72,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
47
72
  Table::new(
48
73
  "CREATE TABLE items (name TEXT, value INTEGER)",
49
74
  "data/*.json",
50
- |path| vec![serde_json::from_str(&std::fs::read_to_string(path).unwrap()).unwrap()],
75
+ |path| vec![row_from_json(&std::fs::read_to_string(path).unwrap())],
51
76
  ),
52
77
  ],
53
78
  )?;
@@ -83,7 +108,7 @@ console.log(results);
83
108
  ## Constructor
84
109
 
85
110
  ```python
86
- DirSQL(root=None, *, tables=None, ignore=None, config=None)
111
+ DirSQL(root=None, *, tables=None, ignore=None, config=None, persist=False, persist_path=None)
87
112
  ```
88
113
 
89
114
  The constructor immediately starts scanning in a background thread via `asyncio.ensure_future`. The constructor itself returns immediately without blocking.
@@ -132,15 +157,29 @@ async for event in db.watch():
132
157
  ```
133
158
 
134
159
  ```rust [Rust]
160
+ // `RowEvent` is an enum; match on the variant to destructure its fields.
161
+ // `StreamExt` (for `.next()`) comes from the `futures` crate, which is only a
162
+ // dirsql dependency under its `cli` feature -- add it to your own project:
163
+ //
164
+ // cargo add futures
165
+ use dirsql::RowEvent;
135
166
  use futures::StreamExt;
136
167
 
137
- let mut stream = db.watch();
168
+ let mut stream = db.watch()?; // watch() returns Result<WatchStream>
138
169
  while let Some(event) = stream.next().await {
139
- match event.action {
140
- Action::Insert => println!("New row in {}: {:?}", event.table, event.row),
141
- Action::Update => println!("Updated row in {}: {:?}", event.table, event.row),
142
- Action::Delete => println!("Deleted row from {}: {:?}", event.table, event.row),
143
- Action::Error => eprintln!("Error: {:?}", event.error),
170
+ match event {
171
+ RowEvent::Insert { table, row, file_path } => {
172
+ println!("New row in {table} ({file_path}): {row:?}")
173
+ }
174
+ RowEvent::Update { table, new_row, file_path, .. } => {
175
+ println!("Updated row in {table} ({file_path}): {new_row:?}")
176
+ }
177
+ RowEvent::Delete { table, row, file_path } => {
178
+ println!("Deleted row from {table} ({file_path}): {row:?}")
179
+ }
180
+ RowEvent::Error { file_path, error, .. } => {
181
+ eprintln!("Error on {file_path:?}: {error}")
182
+ }
144
183
  }
145
184
  }
146
185
  ```
@@ -188,16 +227,20 @@ async def main():
188
227
  ```
189
228
 
190
229
  ```rust [Rust]
191
- async fn watch_and_serve(db: &DirSQL) {
192
- let mut stream = db.watch();
230
+ // `.next()` needs `StreamExt` from the `futures` crate (`cargo add futures`).
231
+ use futures::StreamExt;
232
+
233
+ async fn watch_and_serve(db: &DirSQL) -> Result<(), Box<dyn std::error::Error>> {
234
+ let mut stream = db.watch()?; // watch() returns Result<WatchStream>
193
235
  while let Some(event) = stream.next().await {
194
236
  notify_clients(&event).await;
195
237
  }
238
+ Ok(())
196
239
  }
197
240
 
198
241
  #[tokio::main]
199
242
  async fn main() -> Result<(), Box<dyn std::error::Error>> {
200
- let db = DirSQL::new("./data", vec![...])?;
243
+ let db = DirSQL::new("./data", vec![/* tables */])?;
201
244
 
202
245
  tokio::join!(
203
246
  watch_and_serve(&db),
@@ -94,7 +94,7 @@ const tables: TableDef[] = [
94
94
  },
95
95
  ];
96
96
 
97
- const db = new DirSQL('./workspace', tables);
97
+ const db = new DirSQL({ root: './workspace', tables });
98
98
  ```
99
99
 
100
100
  :::
@@ -160,8 +160,9 @@ assert!(matches!(err, dirsql::DirSqlError::WriteForbidden));
160
160
  ```
161
161
 
162
162
  ```typescript [TypeScript]
163
- // Throws an Error whose message explains writes are not accepted.
164
- expect(() => db.query('DELETE FROM posts')).toThrow(/read-only/i);
163
+ // Rejects with an Error whose message explains writes are not accepted.
164
+ // `db.query` is async, so assert on the rejected promise.
165
+ await expect(db.query('DELETE FROM posts')).rejects.toThrow(/read-only/i);
165
166
  ```
166
167
 
167
168
  :::
@@ -91,7 +91,7 @@ Glob syntax follows standard Unix globbing rules. `**` matches any number of dir
91
91
 
92
92
  A callable `(path: str) -> list[dict]` that converts a file into rows.
93
93
 
94
- - `path` is the **absolute filesystem path** of the matched file
94
+ - `path` is the path of the matched file, **relative to the scan root** (or absolute when the `root` passed to `DirSQL` is absolute)
95
95
  - Return a list of dicts, where each dict maps column names to values
96
96
  - Return an empty list to skip a file
97
97
 
@@ -36,6 +36,10 @@ async for event in db.watch():
36
36
  ```
37
37
 
38
38
  ```rust [Rust]
39
+ // `StreamExt` (for `.next()`) comes from the `futures` crate. dirsql only
40
+ // depends on `futures` under its `cli` feature, so add it to your project:
41
+ //
42
+ // cargo add futures
39
43
  use dirsql::{DirSQL, RowEvent, Table, Value};
40
44
  use futures::StreamExt;
41
45
  use std::collections::HashMap;
@@ -84,8 +88,8 @@ while let Some(event) = stream.next().await {
84
88
  RowEvent::Delete { table, row, file_path } => {
85
89
  println!("delete on {table} ({file_path}): {row:?}")
86
90
  }
87
- RowEvent::Error { file_path, error } => {
88
- println!("error on {file_path:?}: {error}")
91
+ RowEvent::Error { table, file_path, error } => {
92
+ println!("error on {table:?} {file_path:?}: {error}")
89
93
  }
90
94
  }
91
95
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dirsql",
3
- "version": "0.3.25",
3
+ "version": "0.3.26",
4
4
  "description": "Ephemeral SQL index over a local directory",
5
5
  "license": "MIT",
6
6
  "repository": "https://github.com/thekevinscott/dirsql",
@@ -195,15 +195,15 @@
195
195
  "smol-toml": "^1.6.1"
196
196
  },
197
197
  "optionalDependencies": {
198
- "@dirsql/lib-linux-x64-gnu": "0.3.25",
199
- "@dirsql/lib-linux-arm64-gnu": "0.3.25",
200
- "@dirsql/lib-darwin-x64": "0.3.25",
201
- "@dirsql/lib-darwin-arm64": "0.3.25",
202
- "@dirsql/lib-win32-x64-msvc": "0.3.25",
203
- "@dirsql/cli-linux-x64-gnu": "0.3.25",
204
- "@dirsql/cli-linux-arm64-gnu": "0.3.25",
205
- "@dirsql/cli-darwin-x64": "0.3.25",
206
- "@dirsql/cli-darwin-arm64": "0.3.25",
207
- "@dirsql/cli-win32-x64-msvc": "0.3.25"
198
+ "@dirsql/lib-linux-x64-gnu": "0.3.26",
199
+ "@dirsql/lib-linux-arm64-gnu": "0.3.26",
200
+ "@dirsql/lib-darwin-x64": "0.3.26",
201
+ "@dirsql/lib-darwin-arm64": "0.3.26",
202
+ "@dirsql/lib-win32-x64-msvc": "0.3.26",
203
+ "@dirsql/cli-linux-x64-gnu": "0.3.26",
204
+ "@dirsql/cli-linux-arm64-gnu": "0.3.26",
205
+ "@dirsql/cli-darwin-x64": "0.3.26",
206
+ "@dirsql/cli-darwin-arm64": "0.3.26",
207
+ "@dirsql/cli-win32-x64-msvc": "0.3.26"
208
208
  }
209
209
  }