dirsql 0.1.3__tar.gz → 0.1.5__tar.gz

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.
Files changed (39) hide show
  1. {dirsql-0.1.3 → dirsql-0.1.5}/PKG-INFO +1 -1
  2. {dirsql-0.1.3 → dirsql-0.1.5}/packages/python/src/lib.rs +14 -6
  3. {dirsql-0.1.3 → dirsql-0.1.5}/packages/python/tests/integration/test_async_dirsql.py +4 -0
  4. {dirsql-0.1.3 → dirsql-0.1.5}/packages/rust/src/db.rs +69 -2
  5. {dirsql-0.1.3 → dirsql-0.1.5}/packages/rust/src/differ.rs +4 -0
  6. {dirsql-0.1.3 → dirsql-0.1.5}/packages/rust/src/lib.rs +13 -11
  7. {dirsql-0.1.3 → dirsql-0.1.5}/packages/rust/tests/sdk.rs +11 -1
  8. {dirsql-0.1.3 → dirsql-0.1.5}/pyproject.toml +1 -1
  9. {dirsql-0.1.3 → dirsql-0.1.5}/Cargo.lock +0 -0
  10. {dirsql-0.1.3 → dirsql-0.1.5}/Cargo.toml +0 -0
  11. {dirsql-0.1.3 → dirsql-0.1.5}/README.md +0 -0
  12. {dirsql-0.1.3 → dirsql-0.1.5}/packages/python/Cargo.toml +0 -0
  13. {dirsql-0.1.3 → dirsql-0.1.5}/packages/python/README.md +0 -0
  14. {dirsql-0.1.3 → dirsql-0.1.5}/packages/python/tests/__init__.py +0 -0
  15. {dirsql-0.1.3 → dirsql-0.1.5}/packages/python/tests/conftest.py +0 -0
  16. {dirsql-0.1.3 → dirsql-0.1.5}/packages/python/tests/integration/__init__.py +0 -0
  17. {dirsql-0.1.3 → dirsql-0.1.5}/packages/python/tests/integration/test_binding.py +0 -0
  18. {dirsql-0.1.3 → dirsql-0.1.5}/packages/python/tests/integration/test_dirsql.py +0 -0
  19. {dirsql-0.1.3 → dirsql-0.1.5}/packages/python/tests/integration/test_docs_examples.py +0 -0
  20. {dirsql-0.1.3 → dirsql-0.1.5}/packages/python/tests/integration/test_docs_gaps.py +0 -0
  21. {dirsql-0.1.3 → dirsql-0.1.5}/packages/python/tests/integration/test_from_config.py +0 -0
  22. {dirsql-0.1.3 → dirsql-0.1.5}/packages/rust/Cargo.toml +0 -0
  23. {dirsql-0.1.3 → dirsql-0.1.5}/packages/rust/README.md +0 -0
  24. {dirsql-0.1.3 → dirsql-0.1.5}/packages/rust/benches/db_bench.rs +0 -0
  25. {dirsql-0.1.3 → dirsql-0.1.5}/packages/rust/benches/differ_bench.rs +0 -0
  26. {dirsql-0.1.3 → dirsql-0.1.5}/packages/rust/benches/matcher_bench.rs +0 -0
  27. {dirsql-0.1.3 → dirsql-0.1.5}/packages/rust/benches/scanner_bench.rs +0 -0
  28. {dirsql-0.1.3 → dirsql-0.1.5}/packages/rust/src/config.rs +0 -0
  29. {dirsql-0.1.3 → dirsql-0.1.5}/packages/rust/src/matcher.rs +0 -0
  30. {dirsql-0.1.3 → dirsql-0.1.5}/packages/rust/src/parser.rs +0 -0
  31. {dirsql-0.1.3 → dirsql-0.1.5}/packages/rust/src/scanner.rs +0 -0
  32. {dirsql-0.1.3 → dirsql-0.1.5}/packages/rust/src/watcher.rs +0 -0
  33. {dirsql-0.1.3 → dirsql-0.1.5}/packages/rust/tests/async_sdk.rs +0 -0
  34. {dirsql-0.1.3 → dirsql-0.1.5}/packages/rust/tests/docs_examples.rs +0 -0
  35. {dirsql-0.1.3 → dirsql-0.1.5}/packages/rust/tests/docs_gaps.rs +0 -0
  36. {dirsql-0.1.3 → dirsql-0.1.5}/packages/rust/tests/from_config.rs +0 -0
  37. {dirsql-0.1.3 → dirsql-0.1.5}/python/dirsql/__init__.py +0 -0
  38. {dirsql-0.1.3 → dirsql-0.1.5}/python/dirsql/_async.py +0 -0
  39. {dirsql-0.1.3 → dirsql-0.1.5}/python/dirsql/test_async.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dirsql
3
- Version: 0.1.3
3
+ Version: 0.1.5
4
4
  Requires-Dist: pytest>=8 ; extra == 'dev'
5
5
  Requires-Dist: pytest-describe>=2 ; extra == 'dev'
6
6
  Requires-Dist: pytest-asyncio>=0.23 ; extra == 'dev'
@@ -49,10 +49,14 @@ mod python {
49
49
  }
50
50
 
51
51
  /// A row event produced by the watch loop.
52
+ ///
53
+ /// `table` is `Optional[str]` because error events may occur before a
54
+ /// file has been attributed to any table (e.g. a watch-channel failure).
55
+ /// For insert / update / delete events it is always set.
52
56
  #[pyclass(name = "RowEvent", frozen)]
53
57
  struct PyRowEvent {
54
58
  #[pyo3(get)]
55
- table: String,
59
+ table: Option<String>,
56
60
  #[pyo3(get)]
57
61
  action: String,
58
62
  #[pyo3(get)]
@@ -199,7 +203,7 @@ mod python {
199
203
  row,
200
204
  file_path,
201
205
  } => PyRowEvent {
202
- table: table.clone(),
206
+ table: Some(table.clone()),
203
207
  action: "insert".to_string(),
204
208
  row: Some(value_row_to_py_dict(py, row)?),
205
209
  old_row: None,
@@ -212,7 +216,7 @@ mod python {
212
216
  new_row,
213
217
  file_path,
214
218
  } => PyRowEvent {
215
- table: table.clone(),
219
+ table: Some(table.clone()),
216
220
  action: "update".to_string(),
217
221
  row: Some(value_row_to_py_dict(py, new_row)?),
218
222
  old_row: Some(value_row_to_py_dict(py, old_row)?),
@@ -224,15 +228,19 @@ mod python {
224
228
  row,
225
229
  file_path,
226
230
  } => PyRowEvent {
227
- table: table.clone(),
231
+ table: Some(table.clone()),
228
232
  action: "delete".to_string(),
229
233
  row: Some(value_row_to_py_dict(py, row)?),
230
234
  old_row: None,
231
235
  error: None,
232
236
  file_path: Some(file_path.clone()),
233
237
  },
234
- RowEvent::Error { file_path, error } => PyRowEvent {
235
- table: String::new(),
238
+ RowEvent::Error {
239
+ table,
240
+ file_path,
241
+ error,
242
+ } => PyRowEvent {
243
+ table: table.clone(),
236
244
  action: "error".to_string(),
237
245
  row: None,
238
246
  old_row: None,
@@ -341,6 +341,10 @@ def describe_DirSQL_async():
341
341
  assert len(events) >= 1
342
342
  assert events[0].action == "error"
343
343
  assert events[0].error is not None
344
+ # The failing file matched the `items` table's glob; the error
345
+ # event must carry that attribution so multi-table consumers can
346
+ # route the error to the right handler.
347
+ assert events[0].table == "items"
344
348
 
345
349
  @pytest.mark.asyncio
346
350
  async def it_updates_db_on_file_changes(tmp_dir):
@@ -133,7 +133,11 @@ impl Db {
133
133
  }
134
134
 
135
135
  /// Query the database, returning rows as a list of column-name -> value maps.
136
- /// Internal tracking columns (_dirsql_*) are excluded from results.
136
+ ///
137
+ /// Internal tracking columns (`_dirsql_*`) are excluded from `SELECT *`
138
+ /// results so they don't leak. But if the user names one explicitly in the
139
+ /// projection (e.g. `SELECT _dirsql_file_path FROM t`), it's returned —
140
+ /// users opt into the tracking surface by typing the column name.
137
141
  pub fn query(&self, sql: &str) -> Result<Vec<HashMap<String, Value>>> {
138
142
  let mut stmt = self.conn.prepare(sql)?;
139
143
  let column_names: Vec<String> = stmt.column_names().iter().map(|s| s.to_string()).collect();
@@ -141,7 +145,7 @@ impl Db {
141
145
  let rows = stmt.query_map([], |row| {
142
146
  let mut map = HashMap::new();
143
147
  for (i, name) in column_names.iter().enumerate() {
144
- if name.starts_with("_dirsql_") {
148
+ if name.starts_with("_dirsql_") && !sql.contains(name) {
145
149
  continue;
146
150
  }
147
151
  let val: rusqlite::types::Value = row.get(i)?;
@@ -635,6 +639,69 @@ mod tests {
635
639
  assert!(results[0].contains_key("id"));
636
640
  }
637
641
 
642
+ #[test]
643
+ fn query_honors_explicit_dirsql_file_path() {
644
+ let db = Db::new().unwrap();
645
+ db.create_table("CREATE TABLE t (id TEXT)").unwrap();
646
+ let row = HashMap::from([("id".into(), Value::Text("1".into()))]);
647
+ db.insert_row("t", &row, "file.json", 0).unwrap();
648
+
649
+ let results = db.query("SELECT _dirsql_file_path FROM t").unwrap();
650
+ assert_eq!(results.len(), 1);
651
+ assert_eq!(
652
+ results[0].get("_dirsql_file_path"),
653
+ Some(&Value::Text("file.json".into())),
654
+ );
655
+ }
656
+
657
+ #[test]
658
+ fn query_honors_explicit_dirsql_alongside_user_columns() {
659
+ let db = Db::new().unwrap();
660
+ db.create_table("CREATE TABLE posts (title TEXT)").unwrap();
661
+ let row = HashMap::from([("title".into(), Value::Text("Hello".into()))]);
662
+ db.insert_row("posts", &row, "posts/hello.json", 0).unwrap();
663
+
664
+ let results = db
665
+ .query("SELECT title, _dirsql_file_path FROM posts")
666
+ .unwrap();
667
+ assert_eq!(results.len(), 1);
668
+ assert_eq!(results[0]["title"], Value::Text("Hello".into()));
669
+ assert_eq!(
670
+ results[0]["_dirsql_file_path"],
671
+ Value::Text("posts/hello.json".into()),
672
+ );
673
+ }
674
+
675
+ #[test]
676
+ fn query_honors_explicit_dirsql_row_index() {
677
+ let db = Db::new().unwrap();
678
+ db.create_table("CREATE TABLE t (id TEXT)").unwrap();
679
+ let row = HashMap::from([("id".into(), Value::Text("a".into()))]);
680
+ db.insert_row("t", &row, "f.jsonl", 7).unwrap();
681
+
682
+ let results = db.query("SELECT _dirsql_row_index FROM t").unwrap();
683
+ assert_eq!(results.len(), 1);
684
+ assert_eq!(results[0]["_dirsql_row_index"], Value::Integer(7));
685
+ }
686
+
687
+ #[test]
688
+ fn query_keeps_dirsql_when_filter_references_it_with_star_projection() {
689
+ // Naming `_dirsql_file_path` anywhere in the SQL is treated as
690
+ // "the user is aware of this tracking column", so `SELECT *` with
691
+ // a `_dirsql_*` reference in WHERE returns it.
692
+ let db = Db::new().unwrap();
693
+ db.create_table("CREATE TABLE t (id TEXT)").unwrap();
694
+ let row = HashMap::from([("id".into(), Value::Text("1".into()))]);
695
+ db.insert_row("t", &row, "file.json", 0).unwrap();
696
+
697
+ let results = db
698
+ .query("SELECT * FROM t WHERE _dirsql_file_path = 'file.json'")
699
+ .unwrap();
700
+ assert_eq!(results.len(), 1);
701
+ assert!(results[0].contains_key("id"));
702
+ assert!(results[0].contains_key("_dirsql_file_path"));
703
+ }
704
+
638
705
  // --- Error path: query with invalid SQL ---
639
706
 
640
707
  #[test]
@@ -26,6 +26,10 @@ pub enum RowEvent {
26
26
  file_path: String,
27
27
  },
28
28
  Error {
29
+ /// The table whose glob matched the failing file, when known.
30
+ /// `None` for errors that aren't tied to a specific table (e.g.
31
+ /// a watch-channel failure before file attribution).
32
+ table: Option<String>,
29
33
  file_path: PathBuf,
30
34
  error: String,
31
35
  },
@@ -341,18 +341,18 @@ impl DirSQL {
341
341
  fn handle_delete(&self, table: &str, rel_path: &str) -> Vec<RowEvent> {
342
342
  let old_rows = match self.inner.file_rows.lock() {
343
343
  Ok(mut file_rows) => file_rows.remove(rel_path).map(|(_, r)| r),
344
- Err(e) => return vec![error_event(rel_path, e.to_string())],
344
+ Err(e) => return vec![error_event(Some(table), rel_path, e.to_string())],
345
345
  };
346
346
 
347
347
  let row_events = differ::diff(table, old_rows.as_deref(), None, rel_path);
348
348
 
349
349
  let delete_result = match self.inner.db.lock() {
350
350
  Ok(db) => db.delete_rows_by_file(table, rel_path),
351
- Err(e) => return vec![error_event(rel_path, e.to_string())],
351
+ Err(e) => return vec![error_event(Some(table), rel_path, e.to_string())],
352
352
  };
353
353
 
354
354
  if let Err(e) = delete_result {
355
- return vec![error_event(rel_path, e.to_string())];
355
+ return vec![error_event(Some(table), rel_path, e.to_string())];
356
356
  }
357
357
 
358
358
  row_events
@@ -362,7 +362,7 @@ impl DirSQL {
362
362
  let content = match std::fs::read_to_string(abs_path) {
363
363
  Ok(c) => c,
364
364
  Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Vec::new(),
365
- Err(e) => return vec![error_event(rel_path, e.to_string())],
365
+ Err(e) => return vec![error_event(Some(table), rel_path, e.to_string())],
366
366
  };
367
367
 
368
368
  let extract = match self.inner.extract_map.get(table) {
@@ -372,7 +372,7 @@ impl DirSQL {
372
372
 
373
373
  let raw_rows = match extract(rel_path, &content) {
374
374
  Ok(r) => r,
375
- Err(e) => return vec![error_event(rel_path, e.to_string())],
375
+ Err(e) => return vec![error_event(Some(table), rel_path, e.to_string())],
376
376
  };
377
377
 
378
378
  let strict = *self.inner.strict_map.get(table).unwrap_or(&false);
@@ -380,13 +380,13 @@ impl DirSQL {
380
380
  let new_rows = {
381
381
  let db = match self.inner.db.lock() {
382
382
  Ok(g) => g,
383
- Err(e) => return vec![error_event(rel_path, e.to_string())],
383
+ Err(e) => return vec![error_event(Some(table), rel_path, e.to_string())],
384
384
  };
385
385
  let mut normalized = Vec::with_capacity(raw_rows.len());
386
386
  for raw in &raw_rows {
387
387
  match db.normalize_row(table, raw, strict) {
388
388
  Ok(row) => normalized.push(row),
389
- Err(e) => return vec![error_event(rel_path, e.to_string())],
389
+ Err(e) => return vec![error_event(Some(table), rel_path, e.to_string())],
390
390
  }
391
391
  }
392
392
  normalized
@@ -394,7 +394,7 @@ impl DirSQL {
394
394
 
395
395
  let old_rows = match self.inner.file_rows.lock() {
396
396
  Ok(guard) => guard.get(rel_path).map(|(_, r)| r.clone()),
397
- Err(e) => return vec![error_event(rel_path, e.to_string())],
397
+ Err(e) => return vec![error_event(Some(table), rel_path, e.to_string())],
398
398
  };
399
399
 
400
400
  let row_events = differ::diff(table, old_rows.as_deref(), Some(&new_rows), rel_path);
@@ -406,11 +406,11 @@ impl DirSQL {
406
406
  }
407
407
  Ok(())
408
408
  }),
409
- Err(e) => return vec![error_event(rel_path, e.to_string())],
409
+ Err(e) => return vec![error_event(Some(table), rel_path, e.to_string())],
410
410
  };
411
411
 
412
412
  if let Err(e) = db_result {
413
- return vec![error_event(rel_path, e.to_string())];
413
+ return vec![error_event(Some(table), rel_path, e.to_string())];
414
414
  }
415
415
 
416
416
  if let Ok(mut guard) = self.inner.file_rows.lock() {
@@ -491,8 +491,9 @@ impl DirSQL {
491
491
  }
492
492
  }
493
493
 
494
- fn error_event(rel_path: &str, error: String) -> RowEvent {
494
+ fn error_event(table: Option<&str>, rel_path: &str, error: String) -> RowEvent {
495
495
  RowEvent::Error {
496
+ table: table.map(str::to_string),
496
497
  file_path: PathBuf::from(rel_path),
497
498
  error,
498
499
  }
@@ -510,6 +511,7 @@ fn run_channel_loop(db: DirSQL, tx: UnboundedSender<RowEvent>) {
510
511
  }
511
512
  Err(e) => {
512
513
  let _ = tx.unbounded_send(RowEvent::Error {
514
+ table: None,
513
515
  file_path: db.inner.root.clone(),
514
516
  error: e.to_string(),
515
517
  });
@@ -382,8 +382,18 @@ fn it_streams_watch_error_events() {
382
382
 
383
383
  let event = block_on(stream.next()).expect("watch event");
384
384
  match event {
385
- dirsql::RowEvent::Error { error, .. } => {
385
+ dirsql::RowEvent::Error {
386
+ table,
387
+ error,
388
+ file_path,
389
+ } => {
386
390
  assert!(error.contains("intentional parse failure"));
391
+ assert_eq!(
392
+ table.as_deref(),
393
+ Some("items"),
394
+ "error event should attribute the failure to the matching table"
395
+ );
396
+ assert!(file_path.to_string_lossy().contains("bad.txt"));
387
397
  }
388
398
  other => panic!("expected error event, got: {other:?}"),
389
399
  }
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "dirsql"
7
- version = "0.1.3"
7
+ version = "0.1.5"
8
8
  description = "Ephemeral SQL index over a local directory"
9
9
  license = "MIT"
10
10
  requires-python = ">=3.12"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes