dirsql 0.3.25__tar.gz → 0.3.26__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 (167) hide show
  1. {dirsql-0.3.25 → dirsql-0.3.26}/Cargo.lock +1 -1
  2. {dirsql-0.3.25 → dirsql-0.3.26}/PKG-INFO +1 -1
  3. {dirsql-0.3.25 → dirsql-0.3.26}/packages/python/Cargo.toml +1 -1
  4. {dirsql-0.3.25 → dirsql-0.3.26}/packages/rust/src/lib.rs +194 -2
  5. dirsql-0.3.26/packages/rust/tests/watch_relative_root.rs +134 -0
  6. {dirsql-0.3.25 → dirsql-0.3.26}/Cargo.toml +0 -0
  7. {dirsql-0.3.25 → dirsql-0.3.26}/README.md +0 -0
  8. {dirsql-0.3.25 → dirsql-0.3.26}/dirsql/__init__.py +0 -0
  9. {dirsql-0.3.25 → dirsql-0.3.26}/dirsql/_async.py +0 -0
  10. {dirsql-0.3.25 → dirsql-0.3.26}/dirsql/_async_test.py +0 -0
  11. {dirsql-0.3.25 → dirsql-0.3.26}/dirsql/_dirsql.pyi +0 -0
  12. {dirsql-0.3.25 → dirsql-0.3.26}/dirsql/cli/__init__.py +0 -0
  13. {dirsql-0.3.25 → dirsql-0.3.26}/dirsql/cli/binary_path.py +0 -0
  14. {dirsql-0.3.25 → dirsql-0.3.26}/dirsql/cli/binary_path_test.py +0 -0
  15. {dirsql-0.3.25 → dirsql-0.3.26}/dirsql/cli/interpret/__init__.py +0 -0
  16. {dirsql-0.3.25 → dirsql-0.3.26}/dirsql/cli/interpret/dispatch_extract.py +0 -0
  17. {dirsql-0.3.25 → dirsql-0.3.26}/dirsql/cli/interpret/dispatch_extract_test.py +0 -0
  18. {dirsql-0.3.25 → dirsql-0.3.26}/dirsql/cli/interpret/load_app.py +0 -0
  19. {dirsql-0.3.25 → dirsql-0.3.26}/dirsql/cli/interpret/load_app_test.py +0 -0
  20. {dirsql-0.3.25 → dirsql-0.3.26}/dirsql/cli/interpret/run.py +0 -0
  21. {dirsql-0.3.25 → dirsql-0.3.26}/dirsql/cli/interpret/run_test.py +0 -0
  22. {dirsql-0.3.25 → dirsql-0.3.26}/dirsql/cli/interpret/write_message.py +0 -0
  23. {dirsql-0.3.25 → dirsql-0.3.26}/dirsql/cli/interpret/write_message_test.py +0 -0
  24. {dirsql-0.3.25 → dirsql-0.3.26}/dirsql/cli/is_windows.py +0 -0
  25. {dirsql-0.3.25 → dirsql-0.3.26}/dirsql/cli/is_windows_test.py +0 -0
  26. {dirsql-0.3.25 → dirsql-0.3.26}/dirsql/cli/main.py +0 -0
  27. {dirsql-0.3.25 → dirsql-0.3.26}/dirsql/cli/main_test.py +0 -0
  28. {dirsql-0.3.25 → dirsql-0.3.26}/dirsql/py.typed +0 -0
  29. {dirsql-0.3.25 → dirsql-0.3.26}/dirsql/resolve_config.py +0 -0
  30. {dirsql-0.3.25 → dirsql-0.3.26}/dirsql/resolve_config_test.py +0 -0
  31. {dirsql-0.3.25 → dirsql-0.3.26}/docs/.claude/CLAUDE.md +0 -0
  32. {dirsql-0.3.25 → dirsql-0.3.26}/docs/.vitepress/config.ts +0 -0
  33. {dirsql-0.3.25 → dirsql-0.3.26}/docs/.vitepress/theme/index.ts +0 -0
  34. {dirsql-0.3.25 → dirsql-0.3.26}/docs/.vitepress/theme/lang.ts +0 -0
  35. {dirsql-0.3.25 → dirsql-0.3.26}/docs/AGENTS.md +0 -0
  36. {dirsql-0.3.25 → dirsql-0.3.26}/docs/api/index.md +0 -0
  37. {dirsql-0.3.25 → dirsql-0.3.26}/docs/cli/config.md +0 -0
  38. {dirsql-0.3.25 → dirsql-0.3.26}/docs/cli/http-api.md +0 -0
  39. {dirsql-0.3.25 → dirsql-0.3.26}/docs/cli/index.md +0 -0
  40. {dirsql-0.3.25 → dirsql-0.3.26}/docs/cli/init.md +0 -0
  41. {dirsql-0.3.25 → dirsql-0.3.26}/docs/cli/server.md +0 -0
  42. {dirsql-0.3.25 → dirsql-0.3.26}/docs/getting-started.md +0 -0
  43. {dirsql-0.3.25 → dirsql-0.3.26}/docs/guide/async.md +0 -0
  44. {dirsql-0.3.25 → dirsql-0.3.26}/docs/guide/crdt.md +0 -0
  45. {dirsql-0.3.25 → dirsql-0.3.26}/docs/guide/persistence.md +0 -0
  46. {dirsql-0.3.25 → dirsql-0.3.26}/docs/guide/querying.md +0 -0
  47. {dirsql-0.3.25 → dirsql-0.3.26}/docs/guide/tables.md +0 -0
  48. {dirsql-0.3.25 → dirsql-0.3.26}/docs/guide/watching.md +0 -0
  49. {dirsql-0.3.25 → dirsql-0.3.26}/docs/index.md +0 -0
  50. {dirsql-0.3.25 → dirsql-0.3.26}/docs/migrations.md +0 -0
  51. {dirsql-0.3.25 → dirsql-0.3.26}/docs/package.json +0 -0
  52. {dirsql-0.3.25 → dirsql-0.3.26}/docs/playwright.config.ts +0 -0
  53. {dirsql-0.3.25 → dirsql-0.3.26}/docs/pnpm-lock.yaml +0 -0
  54. {dirsql-0.3.25 → dirsql-0.3.26}/docs/pnpm-workspace.yaml +0 -0
  55. {dirsql-0.3.25 → dirsql-0.3.26}/docs/tests/integration/home.spec.ts +0 -0
  56. {dirsql-0.3.25 → dirsql-0.3.26}/docs/tests/integration/language-flag.spec.ts +0 -0
  57. {dirsql-0.3.25 → dirsql-0.3.26}/docs/tests/unit/config.test.ts +0 -0
  58. {dirsql-0.3.25 → dirsql-0.3.26}/docs/tests/unit/lang.test.ts +0 -0
  59. {dirsql-0.3.25 → dirsql-0.3.26}/docs/vitest.config.ts +0 -0
  60. {dirsql-0.3.25 → dirsql-0.3.26}/packages/python/README.md +0 -0
  61. {dirsql-0.3.25 → dirsql-0.3.26}/packages/python/conftest.py +0 -0
  62. {dirsql-0.3.25 → dirsql-0.3.26}/packages/python/docs/.claude/CLAUDE.md +0 -0
  63. {dirsql-0.3.25 → dirsql-0.3.26}/packages/python/docs/.vitepress/config.ts +0 -0
  64. {dirsql-0.3.25 → dirsql-0.3.26}/packages/python/docs/.vitepress/theme/index.ts +0 -0
  65. {dirsql-0.3.25 → dirsql-0.3.26}/packages/python/docs/.vitepress/theme/lang.ts +0 -0
  66. {dirsql-0.3.25 → dirsql-0.3.26}/packages/python/docs/AGENTS.md +0 -0
  67. {dirsql-0.3.25 → dirsql-0.3.26}/packages/python/docs/api/index.md +0 -0
  68. {dirsql-0.3.25 → dirsql-0.3.26}/packages/python/docs/cli/config.md +0 -0
  69. {dirsql-0.3.25 → dirsql-0.3.26}/packages/python/docs/cli/http-api.md +0 -0
  70. {dirsql-0.3.25 → dirsql-0.3.26}/packages/python/docs/cli/index.md +0 -0
  71. {dirsql-0.3.25 → dirsql-0.3.26}/packages/python/docs/cli/init.md +0 -0
  72. {dirsql-0.3.25 → dirsql-0.3.26}/packages/python/docs/cli/server.md +0 -0
  73. {dirsql-0.3.25 → dirsql-0.3.26}/packages/python/docs/getting-started.md +0 -0
  74. {dirsql-0.3.25 → dirsql-0.3.26}/packages/python/docs/guide/async.md +0 -0
  75. {dirsql-0.3.25 → dirsql-0.3.26}/packages/python/docs/guide/crdt.md +0 -0
  76. {dirsql-0.3.25 → dirsql-0.3.26}/packages/python/docs/guide/persistence.md +0 -0
  77. {dirsql-0.3.25 → dirsql-0.3.26}/packages/python/docs/guide/querying.md +0 -0
  78. {dirsql-0.3.25 → dirsql-0.3.26}/packages/python/docs/guide/tables.md +0 -0
  79. {dirsql-0.3.25 → dirsql-0.3.26}/packages/python/docs/guide/watching.md +0 -0
  80. {dirsql-0.3.25 → dirsql-0.3.26}/packages/python/docs/index.md +0 -0
  81. {dirsql-0.3.25 → dirsql-0.3.26}/packages/python/docs/migrations.md +0 -0
  82. {dirsql-0.3.25 → dirsql-0.3.26}/packages/python/docs/package.json +0 -0
  83. {dirsql-0.3.25 → dirsql-0.3.26}/packages/python/docs/playwright.config.ts +0 -0
  84. {dirsql-0.3.25 → dirsql-0.3.26}/packages/python/docs/pnpm-lock.yaml +0 -0
  85. {dirsql-0.3.25 → dirsql-0.3.26}/packages/python/docs/pnpm-workspace.yaml +0 -0
  86. {dirsql-0.3.25 → dirsql-0.3.26}/packages/python/docs/tests/integration/home.spec.ts +0 -0
  87. {dirsql-0.3.25 → dirsql-0.3.26}/packages/python/docs/tests/integration/language-flag.spec.ts +0 -0
  88. {dirsql-0.3.25 → dirsql-0.3.26}/packages/python/docs/tests/unit/config.test.ts +0 -0
  89. {dirsql-0.3.25 → dirsql-0.3.26}/packages/python/docs/tests/unit/lang.test.ts +0 -0
  90. {dirsql-0.3.25 → dirsql-0.3.26}/packages/python/docs/vitest.config.ts +0 -0
  91. {dirsql-0.3.25 → dirsql-0.3.26}/packages/python/src/lib.rs +0 -0
  92. {dirsql-0.3.25 → dirsql-0.3.26}/packages/python/tests/__init__.py +0 -0
  93. {dirsql-0.3.25 → dirsql-0.3.26}/packages/python/tests/conftest.py +0 -0
  94. {dirsql-0.3.25 → dirsql-0.3.26}/packages/python/tests/e2e/__init__.py +0 -0
  95. {dirsql-0.3.25 → dirsql-0.3.26}/packages/python/tests/integration/__fixtures__/data/a/meta.json +0 -0
  96. {dirsql-0.3.25 → dirsql-0.3.26}/packages/python/tests/integration/__fixtures__/data/b/meta.json +0 -0
  97. {dirsql-0.3.25 → dirsql-0.3.26}/packages/python/tests/integration/__fixtures__/dirsql.config.py +0 -0
  98. {dirsql-0.3.25 → dirsql-0.3.26}/packages/python/tests/integration/__fixtures__/interpret/data/a/meta.json +0 -0
  99. {dirsql-0.3.25 → dirsql-0.3.26}/packages/python/tests/integration/__fixtures__/interpret/data/b/meta.json +0 -0
  100. {dirsql-0.3.25 → dirsql-0.3.26}/packages/python/tests/integration/__fixtures__/interpret/dirsql.config.py +0 -0
  101. {dirsql-0.3.25 → dirsql-0.3.26}/packages/python/tests/integration/__fixtures__/interpret/dirsql.config_no_app.py +0 -0
  102. {dirsql-0.3.25 → dirsql-0.3.26}/packages/python/tests/integration/__fixtures__/interpret/dirsql.config_raises.py +0 -0
  103. {dirsql-0.3.25 → dirsql-0.3.26}/packages/python/tests/integration/__init__.py +0 -0
  104. {dirsql-0.3.25 → dirsql-0.3.26}/packages/python/tests/integration/interpret_subprocess.py +0 -0
  105. {dirsql-0.3.25 → dirsql-0.3.26}/packages/python/tests/integration/test_async_dirsql.py +0 -0
  106. {dirsql-0.3.25 → dirsql-0.3.26}/packages/python/tests/integration/test_binding.py +0 -0
  107. {dirsql-0.3.25 → dirsql-0.3.26}/packages/python/tests/integration/test_dirsql.py +0 -0
  108. {dirsql-0.3.25 → dirsql-0.3.26}/packages/python/tests/integration/test_docs_examples.py +0 -0
  109. {dirsql-0.3.25 → dirsql-0.3.26}/packages/python/tests/integration/test_docs_gaps.py +0 -0
  110. {dirsql-0.3.25 → dirsql-0.3.26}/packages/python/tests/integration/test_from_config.py +0 -0
  111. {dirsql-0.3.25 → dirsql-0.3.26}/packages/python/tests/integration/test_interpret.py +0 -0
  112. {dirsql-0.3.25 → dirsql-0.3.26}/packages/python/tests/integration/test_native_config.py +0 -0
  113. {dirsql-0.3.25 → dirsql-0.3.26}/packages/python/tests/integration/test_persist.py +0 -0
  114. {dirsql-0.3.25 → dirsql-0.3.26}/packages/python/tests/integration/test_serialization.py +0 -0
  115. {dirsql-0.3.25 → dirsql-0.3.26}/packages/rust/Cargo.toml +0 -0
  116. {dirsql-0.3.25 → dirsql-0.3.26}/packages/rust/README.md +0 -0
  117. {dirsql-0.3.25 → dirsql-0.3.26}/packages/rust/benches/db_bench.rs +0 -0
  118. {dirsql-0.3.25 → dirsql-0.3.26}/packages/rust/benches/differ_bench.rs +0 -0
  119. {dirsql-0.3.25 → dirsql-0.3.26}/packages/rust/benches/matcher_bench.rs +0 -0
  120. {dirsql-0.3.25 → dirsql-0.3.26}/packages/rust/benches/scanner_bench.rs +0 -0
  121. {dirsql-0.3.25 → dirsql-0.3.26}/packages/rust/docs/api/index.md +0 -0
  122. {dirsql-0.3.25 → dirsql-0.3.26}/packages/rust/docs/cli/config.md +0 -0
  123. {dirsql-0.3.25 → dirsql-0.3.26}/packages/rust/docs/cli/http-api.md +0 -0
  124. {dirsql-0.3.25 → dirsql-0.3.26}/packages/rust/docs/cli/index.md +0 -0
  125. {dirsql-0.3.25 → dirsql-0.3.26}/packages/rust/docs/cli/init.md +0 -0
  126. {dirsql-0.3.25 → dirsql-0.3.26}/packages/rust/docs/cli/server.md +0 -0
  127. {dirsql-0.3.25 → dirsql-0.3.26}/packages/rust/docs/getting-started.md +0 -0
  128. {dirsql-0.3.25 → dirsql-0.3.26}/packages/rust/docs/guide/async.md +0 -0
  129. {dirsql-0.3.25 → dirsql-0.3.26}/packages/rust/docs/guide/crdt.md +0 -0
  130. {dirsql-0.3.25 → dirsql-0.3.26}/packages/rust/docs/guide/persistence.md +0 -0
  131. {dirsql-0.3.25 → dirsql-0.3.26}/packages/rust/docs/guide/querying.md +0 -0
  132. {dirsql-0.3.25 → dirsql-0.3.26}/packages/rust/docs/guide/tables.md +0 -0
  133. {dirsql-0.3.25 → dirsql-0.3.26}/packages/rust/docs/guide/watching.md +0 -0
  134. {dirsql-0.3.25 → dirsql-0.3.26}/packages/rust/docs/index.md +0 -0
  135. {dirsql-0.3.25 → dirsql-0.3.26}/packages/rust/docs/migrations.md +0 -0
  136. {dirsql-0.3.25 → dirsql-0.3.26}/packages/rust/src/bin/dirsql.rs +0 -0
  137. {dirsql-0.3.25 → dirsql-0.3.26}/packages/rust/src/cli/init.rs +0 -0
  138. {dirsql-0.3.25 → dirsql-0.3.26}/packages/rust/src/cli/mod.rs +0 -0
  139. {dirsql-0.3.25 → dirsql-0.3.26}/packages/rust/src/cli/native_config.rs +0 -0
  140. {dirsql-0.3.25 → dirsql-0.3.26}/packages/rust/src/cli/router.rs +0 -0
  141. {dirsql-0.3.25 → dirsql-0.3.26}/packages/rust/src/cli/serialize.rs +0 -0
  142. {dirsql-0.3.25 → dirsql-0.3.26}/packages/rust/src/cli/server.rs +0 -0
  143. {dirsql-0.3.25 → dirsql-0.3.26}/packages/rust/src/config.rs +0 -0
  144. {dirsql-0.3.25 → dirsql-0.3.26}/packages/rust/src/db.rs +0 -0
  145. {dirsql-0.3.25 → dirsql-0.3.26}/packages/rust/src/differ.rs +0 -0
  146. {dirsql-0.3.25 → dirsql-0.3.26}/packages/rust/src/matcher.rs +0 -0
  147. {dirsql-0.3.25 → dirsql-0.3.26}/packages/rust/src/persist.rs +0 -0
  148. {dirsql-0.3.25 → dirsql-0.3.26}/packages/rust/src/scanner.rs +0 -0
  149. {dirsql-0.3.25 → dirsql-0.3.26}/packages/rust/src/watcher.rs +0 -0
  150. {dirsql-0.3.25 → dirsql-0.3.26}/packages/rust/tests/async_sdk.rs +0 -0
  151. {dirsql-0.3.25 → dirsql-0.3.26}/packages/rust/tests/cli_e2e.rs +0 -0
  152. {dirsql-0.3.25 → dirsql-0.3.26}/packages/rust/tests/cli_integration.rs +0 -0
  153. {dirsql-0.3.25 → dirsql-0.3.26}/packages/rust/tests/code_review_findings.rs +0 -0
  154. {dirsql-0.3.25 → dirsql-0.3.26}/packages/rust/tests/config.rs +0 -0
  155. {dirsql-0.3.25 → dirsql-0.3.26}/packages/rust/tests/docs_examples.rs +0 -0
  156. {dirsql-0.3.25 → dirsql-0.3.26}/packages/rust/tests/docs_gaps.rs +0 -0
  157. {dirsql-0.3.25 → dirsql-0.3.26}/packages/rust/tests/extensions.rs +0 -0
  158. {dirsql-0.3.25 → dirsql-0.3.26}/packages/rust/tests/from_config.rs +0 -0
  159. {dirsql-0.3.25 → dirsql-0.3.26}/packages/rust/tests/init_e2e.rs +0 -0
  160. {dirsql-0.3.25 → dirsql-0.3.26}/packages/rust/tests/init_integration.rs +0 -0
  161. {dirsql-0.3.25 → dirsql-0.3.26}/packages/rust/tests/persist.rs +0 -0
  162. {dirsql-0.3.25 → dirsql-0.3.26}/packages/rust/tests/readonly_query.rs +0 -0
  163. {dirsql-0.3.25 → dirsql-0.3.26}/packages/rust/tests/scanner.rs +0 -0
  164. {dirsql-0.3.25 → dirsql-0.3.26}/packages/rust/tests/sdk.rs +0 -0
  165. {dirsql-0.3.25 → dirsql-0.3.26}/packages/rust/tests/serialization.rs +0 -0
  166. {dirsql-0.3.25 → dirsql-0.3.26}/packages/rust/tests/watcher.rs +0 -0
  167. {dirsql-0.3.25 → dirsql-0.3.26}/pyproject.toml +0 -0
@@ -499,7 +499,7 @@ dependencies = [
499
499
 
500
500
  [[package]]
501
501
  name = "dirsql-py-ext"
502
- version = "0.3.25"
502
+ version = "0.3.26"
503
503
  dependencies = [
504
504
  "dirsql",
505
505
  "pyo3",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dirsql
3
- Version: 0.3.25
3
+ Version: 0.3.26
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'
@@ -4,7 +4,7 @@ name = "dirsql-py-ext"
4
4
  # pypi/maturin handler can rewrite it via `write-version` before
5
5
  # `maturin build`. `pyproject.toml` declares `dynamic = ["version"]`
6
6
  # and maturin reads this field. Mirrors `packages/rust/Cargo.toml`.
7
- version = "0.3.25"
7
+ version = "0.3.26"
8
8
  edition.workspace = true
9
9
  publish = false
10
10
  readme = "README.md"
@@ -227,6 +227,17 @@ impl Table {
227
227
  struct DirSqlInner {
228
228
  db: Mutex<Db>,
229
229
  root: PathBuf,
230
+ /// Canonicalized form of `root`, used **only** for the live filesystem
231
+ /// watcher. `notify` has surprising behavior when handed a relative path
232
+ /// like `.` / `./data` (it may deliver no events at all, or deliver them
233
+ /// under the cwd-joined path so the relative prefix no longer strips):
234
+ /// the CLI binary works around this by canonicalizing its root before
235
+ /// watching, and the SDK now does the same (#250). Derived once at
236
+ /// construction via [`canonical_root`] (literal fallback when
237
+ /// canonicalization fails, e.g. a not-yet-created root), so the user's
238
+ /// `root` — and therefore the initial scan, [`DirSQL::config`], and the
239
+ /// `_path` virtual column — stay byte-for-byte unchanged.
240
+ watch_root: PathBuf,
230
241
  /// Pre-compiled matcher over all table globs plus ignore patterns.
231
242
  /// Built once at construction, reused by the initial scan and every
232
243
  /// subsequent watch iteration.
@@ -393,7 +404,9 @@ impl DirSQL {
393
404
  pub fn start_watching(&self) -> Result<()> {
394
405
  let mut guard = self.inner.watcher.lock().map_err(DirSqlError::lock)?;
395
406
  if guard.is_none() {
396
- let watcher = Watcher::new(&self.inner.root).map_err(DirSqlError::watch)?;
407
+ // Watch the canonicalized root, never the (possibly relative)
408
+ // user-supplied one — `notify` misbehaves on relative paths (#250).
409
+ let watcher = Watcher::new(&self.inner.watch_root).map_err(DirSqlError::watch)?;
397
410
  *guard = Some(watcher);
398
411
  }
399
412
  Ok(())
@@ -515,8 +528,15 @@ impl DirSQL {
515
528
  let abs_path = match &event {
516
529
  FileEvent::Created(p) | FileEvent::Modified(p) | FileEvent::Deleted(p) => p.clone(),
517
530
  };
531
+ // Events now arrive under the canonical `watch_root` (the watcher was
532
+ // started on it), so strip that first; fall back to the user-supplied
533
+ // `root` (covers the already-canonical/absolute-root case and any
534
+ // event whose path predates the watch-root change), then to the raw
535
+ // absolute path. This keeps the computed relative `_path` identical to
536
+ // the pre-#250 behavior for both absolute and relative roots.
518
537
  let rel_path_buf = abs_path
519
- .strip_prefix(&self.inner.root)
538
+ .strip_prefix(&self.inner.watch_root)
539
+ .or_else(|_| abs_path.strip_prefix(&self.inner.root))
520
540
  .unwrap_or(&abs_path)
521
541
  .to_path_buf();
522
542
 
@@ -870,10 +890,17 @@ impl DirSQL {
870
890
  write_meta(db.conn(), meta).map_err(DirSqlError::sqlite)?;
871
891
  }
872
892
 
893
+ // Canonicalize the watch root once, here at the single shared
894
+ // construction point reached by both `build()` and `build_async()`,
895
+ // so the live watcher never sees a relative path (#250). `root` itself
896
+ // is left untouched.
897
+ let watch_root = PathBuf::from(canonical_root(&root));
898
+
873
899
  Ok(Self {
874
900
  inner: Arc::new(DirSqlInner {
875
901
  db: Mutex::new(db),
876
902
  root,
903
+ watch_root,
877
904
  matcher,
878
905
  extract_map,
879
906
  strict_map,
@@ -1867,6 +1894,171 @@ mod internal_tests {
1867
1894
  assert_eq!(events.len(), 1, "non-ignored path must produce one event");
1868
1895
  }
1869
1896
 
1897
+ // -----------------------------------------------------------------------
1898
+ // #250: canonical `watch_root` and the strip-prefix fallbacks.
1899
+ //
1900
+ // `std::env::set_current_dir` is process-global, so the relative-root test
1901
+ // serializes through this lock and restores the cwd on the way out.
1902
+ // -----------------------------------------------------------------------
1903
+
1904
+ fn cwd_lock() -> &'static Mutex<()> {
1905
+ static LOCK: std::sync::OnceLock<Mutex<()>> = std::sync::OnceLock::new();
1906
+ LOCK.get_or_init(|| Mutex::new(()))
1907
+ }
1908
+
1909
+ /// Building with a **relative** root canonicalizes `watch_root` to an
1910
+ /// absolute path while leaving `root` (and therefore `config()` / `_path`)
1911
+ /// exactly as the caller supplied it. This is the core of the #250 fix:
1912
+ /// `start_watching` watches `watch_root`, so `notify` never sees `.`.
1913
+ #[test]
1914
+ fn relative_root_canonicalizes_watch_root_only() {
1915
+ let dir = TempDir::new().unwrap();
1916
+ let canonical = std::fs::canonicalize(dir.path()).unwrap();
1917
+
1918
+ let _guard = cwd_lock().lock().unwrap_or_else(|p| p.into_inner());
1919
+ let original = std::env::current_dir().unwrap();
1920
+ std::env::set_current_dir(&canonical).unwrap();
1921
+
1922
+ let db = DirSQL::new(
1923
+ ".",
1924
+ vec![Table::new("CREATE TABLE t (x TEXT)", "*.txt", |_| vec![])],
1925
+ )
1926
+ .unwrap();
1927
+
1928
+ // Restore cwd before asserting so a failure can't strand the process.
1929
+ std::env::set_current_dir(&original).unwrap();
1930
+
1931
+ // `root` is preserved verbatim; `config()` echoes it.
1932
+ assert_eq!(db.inner.root, PathBuf::from("."));
1933
+ assert_eq!(db.config().root, PathBuf::from("."));
1934
+ // `watch_root` is absolute and points at the canonical temp dir.
1935
+ assert!(
1936
+ db.inner.watch_root.is_absolute(),
1937
+ "watch_root must be absolute, got {:?}",
1938
+ db.inner.watch_root
1939
+ );
1940
+ assert_eq!(db.inner.watch_root, canonical);
1941
+ }
1942
+
1943
+ /// With an absolute root the canonical `watch_root` equals the (already
1944
+ /// canonical) root on this platform, and `process_file_event` strips that
1945
+ /// prefix to yield a root-relative `_path` — the first `strip_prefix`
1946
+ /// (watch_root) arm.
1947
+ #[test]
1948
+ fn process_file_event_strips_watch_root_prefix() {
1949
+ let dir = TempDir::new().unwrap();
1950
+ let canonical = std::fs::canonicalize(dir.path()).unwrap();
1951
+ let db = DirSQL::new(
1952
+ &canonical,
1953
+ vec![Table::new(
1954
+ "CREATE TABLE items (name TEXT, _path TEXT)",
1955
+ "**/*.txt",
1956
+ |_| {
1957
+ vec![Row::from_iter([(
1958
+ "name".to_string(),
1959
+ Value::Text("x".into()),
1960
+ )])]
1961
+ },
1962
+ )],
1963
+ )
1964
+ .unwrap();
1965
+
1966
+ let abs = canonical.join("nested").join("a.txt");
1967
+ std::fs::create_dir_all(canonical.join("nested")).unwrap();
1968
+ std::fs::write(&abs, b"").unwrap();
1969
+
1970
+ let events = db.process_file_event(FileEvent::Created(abs));
1971
+ assert_eq!(events.len(), 1, "expected one insert: {events:?}");
1972
+ match &events[0] {
1973
+ RowEvent::Insert { row, .. } => {
1974
+ assert_eq!(
1975
+ row.get("_path"),
1976
+ Some(&Value::Text("nested/a.txt".to_string())),
1977
+ "watch_root prefix must be stripped to a root-relative path"
1978
+ );
1979
+ }
1980
+ other => panic!("expected Insert, got {other:?}"),
1981
+ }
1982
+ }
1983
+
1984
+ /// When an event path lies under the user-supplied `root` but not under
1985
+ /// the canonical `watch_root`, the `.or_else` fallback strips `root`
1986
+ /// instead. We force that split by pointing `watch_root` at a sibling that
1987
+ /// is not a prefix of the event path, leaving `root` as the real dir.
1988
+ #[test]
1989
+ fn process_file_event_falls_back_to_root_prefix() {
1990
+ let dir = TempDir::new().unwrap();
1991
+ let canonical = std::fs::canonicalize(dir.path()).unwrap();
1992
+ let mut db = DirSQL::new(
1993
+ &canonical,
1994
+ vec![Table::new(
1995
+ "CREATE TABLE items (name TEXT, _path TEXT)",
1996
+ "**/*.txt",
1997
+ |_| {
1998
+ vec![Row::from_iter([(
1999
+ "name".to_string(),
2000
+ Value::Text("x".into()),
2001
+ )])]
2002
+ },
2003
+ )],
2004
+ )
2005
+ .unwrap();
2006
+
2007
+ // Repoint watch_root to a non-prefix sibling so the first strip misses
2008
+ // and the `.or_else(root)` arm runs. `root` stays the real dir.
2009
+ Arc::get_mut(&mut db.inner).unwrap().watch_root = canonical.join("does-not-prefix");
2010
+
2011
+ let abs = canonical.join("b.txt");
2012
+ std::fs::write(&abs, b"").unwrap();
2013
+ let events = db.process_file_event(FileEvent::Created(abs));
2014
+ assert_eq!(events.len(), 1, "expected one insert: {events:?}");
2015
+ match &events[0] {
2016
+ RowEvent::Insert { row, .. } => {
2017
+ assert_eq!(
2018
+ row.get("_path"),
2019
+ Some(&Value::Text("b.txt".to_string())),
2020
+ "root fallback must strip the user-supplied root prefix"
2021
+ );
2022
+ }
2023
+ other => panic!("expected Insert, got {other:?}"),
2024
+ }
2025
+ }
2026
+
2027
+ /// When the event path is under neither `watch_root` nor `root`, the final
2028
+ /// `unwrap_or(&abs_path)` arm keeps the absolute path. A path that matches
2029
+ /// no table glob then yields no events, but the strip fallback is still
2030
+ /// executed — we assert the no-event outcome to pin the arm without relying
2031
+ /// on a row.
2032
+ #[test]
2033
+ fn process_file_event_keeps_absolute_path_when_no_prefix_matches() {
2034
+ let dir = TempDir::new().unwrap();
2035
+ let canonical = std::fs::canonicalize(dir.path()).unwrap();
2036
+ let db = DirSQL::new(
2037
+ &canonical,
2038
+ vec![Table::new(
2039
+ "CREATE TABLE items (name TEXT)",
2040
+ "*.txt",
2041
+ |_| {
2042
+ vec![Row::from_iter([(
2043
+ "name".to_string(),
2044
+ Value::Text("x".into()),
2045
+ )])]
2046
+ },
2047
+ )],
2048
+ )
2049
+ .unwrap();
2050
+
2051
+ // A path outside both roots: neither strip matches, so the absolute
2052
+ // path is used as the relative path. It does not match `*.txt` at the
2053
+ // root, so no events are produced.
2054
+ let outside = PathBuf::from("/some/elsewhere/c.md");
2055
+ let events = db.process_file_event(FileEvent::Created(outside));
2056
+ assert!(
2057
+ events.is_empty(),
2058
+ "unmatched absolute path must produce no events: {events:?}"
2059
+ );
2060
+ }
2061
+
1870
2062
  /// Drive `reconcile_scan` directly with a cached file whose
1871
2063
  /// `snapshot_ns <= mtime_ns`, forcing the racy-window hash-confirm branch.
1872
2064
  /// With a matching content hash the file is trusted.
@@ -0,0 +1,134 @@
1
+ //! Regression test for #250: the file watcher must deliver `RowEvent`s when
2
+ //! `DirSQL` is constructed with a **relative** `root` (e.g. `DirSQL::new(".",
3
+ //! ...)`), not just an absolute one.
4
+ //!
5
+ //! Root cause: `start_watching()` handed the user-supplied (possibly relative)
6
+ //! `root` straight to `notify`, which has surprising behavior when watching
7
+ //! relative paths like `./`. The initial scan worked fine either way; only the
8
+ //! live watcher was broken. The CLI binary already canonicalized its root for
9
+ //! exactly this reason — the SDK did not. The fix watches a canonicalized
10
+ //! `watch_root` while keeping the user-supplied `root` for scanning,
11
+ //! `config()`, and `_path` output.
12
+ //!
13
+ //! `std::env::set_current_dir` mutates **process-global** state, so every test
14
+ //! in this file serializes through `CWD_LOCK` and restores the original cwd on
15
+ //! the way out (via the `CwdGuard` drop), even on panic.
16
+
17
+ use dirsql::{DirSQL, Table, Value};
18
+ use std::fs;
19
+ use std::path::PathBuf;
20
+ use std::sync::{Mutex, MutexGuard, OnceLock};
21
+ use std::time::{Duration, Instant};
22
+
23
+ /// Serializes the process-global cwd across the tests in this file. A `OnceLock`
24
+ /// avoids pulling in a `lazy_static`/`once_cell` dependency just for the test.
25
+ fn cwd_lock() -> &'static Mutex<()> {
26
+ static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
27
+ LOCK.get_or_init(|| Mutex::new(()))
28
+ }
29
+
30
+ /// Holds the cwd lock and the original working directory, restoring it on drop.
31
+ /// Recovers a poisoned lock: a prior test panicking while holding the guard
32
+ /// must not cascade-fail every later cwd test.
33
+ struct CwdGuard {
34
+ _guard: MutexGuard<'static, ()>,
35
+ original: PathBuf,
36
+ }
37
+
38
+ impl CwdGuard {
39
+ fn enter(target: &std::path::Path) -> Self {
40
+ let guard = cwd_lock().lock().unwrap_or_else(|p| p.into_inner());
41
+ let original = std::env::current_dir().expect("read cwd");
42
+ std::env::set_current_dir(target).expect("chdir into target");
43
+ Self {
44
+ _guard: guard,
45
+ original,
46
+ }
47
+ }
48
+ }
49
+
50
+ impl Drop for CwdGuard {
51
+ fn drop(&mut self) {
52
+ // Best-effort restore; if this fails the process is already in a bad
53
+ // state and later tests will surface it.
54
+ let _ = std::env::set_current_dir(&self.original);
55
+ }
56
+ }
57
+
58
+ fn items_table() -> Table {
59
+ Table::new(
60
+ "CREATE TABLE items (name TEXT, _path TEXT)",
61
+ "**/*.txt",
62
+ |path| {
63
+ let content = std::fs::read_to_string(path).unwrap_or_default();
64
+ vec![std::collections::HashMap::from([(
65
+ "name".to_string(),
66
+ Value::Text(content.trim().to_string()),
67
+ )])]
68
+ },
69
+ )
70
+ }
71
+
72
+ /// #250: a watcher built on a relative `root` (`"."`) must still emit an insert
73
+ /// `RowEvent` when a matching file is created after `start_watching()`.
74
+ ///
75
+ /// Without the fix the watcher hands `"."` to `notify`, which never delivers
76
+ /// events, so this poll loop times out with zero events (RED). With the fix the
77
+ /// watcher canonicalizes the root, the create is observed, and an `Insert`
78
+ /// arrives (GREEN). The companion absolute-root assertion is intentionally
79
+ /// *not* a separate `#[test]` so it can't race for the cwd lock; it shares this
80
+ /// one to prove the two roots behave identically.
81
+ #[test]
82
+ fn watch_with_relative_root_emits_events() {
83
+ let dir = tempfile::TempDir::new().unwrap();
84
+ // Canonicalize the temp dir up front: on macOS `TempDir` lives under
85
+ // `/var/...` which is a symlink to `/private/var/...`, and the post-fix
86
+ // `_path` is computed by stripping the *canonical* watch root. Comparing
87
+ // against a canonical base keeps the relative-path assertion portable.
88
+ let canonical_dir = fs::canonicalize(dir.path()).unwrap();
89
+
90
+ let _cwd = CwdGuard::enter(&canonical_dir);
91
+
92
+ // Relative root: the exact shape from the bug report / docs examples.
93
+ let db = DirSQL::new(".", vec![items_table()]).unwrap();
94
+ db.start_watching().unwrap();
95
+
96
+ // Give the watcher a moment to register before mutating the tree.
97
+ std::thread::sleep(Duration::from_millis(250));
98
+ fs::write(canonical_dir.join("apple.txt"), "apple").unwrap();
99
+
100
+ let mut events = Vec::new();
101
+ let deadline = Instant::now() + Duration::from_secs(5);
102
+ while events.is_empty() && Instant::now() < deadline {
103
+ events.extend(db.poll_events(Duration::from_millis(200)).unwrap());
104
+ }
105
+
106
+ let insert = events
107
+ .iter()
108
+ .find(|e| matches!(e, dirsql::RowEvent::Insert { .. }));
109
+ assert!(
110
+ insert.is_some(),
111
+ "relative-root watcher must emit an Insert event (#250); saw: {events:?}"
112
+ );
113
+
114
+ // The relative `_path` must be the root-relative file name, identical to
115
+ // what an absolute root would have produced — proving the canonical
116
+ // watch-root did not leak the absolute prefix into the event path.
117
+ if let Some(dirsql::RowEvent::Insert { row, .. }) = insert {
118
+ assert_eq!(
119
+ row.get("_path"),
120
+ Some(&Value::Text("apple.txt".to_string())),
121
+ "_path must stay root-relative for a relative root"
122
+ );
123
+ }
124
+
125
+ // And the row landed in the in-memory index.
126
+ let rows = db.query("SELECT name FROM items").unwrap();
127
+ assert!(
128
+ rows.iter().any(|r| matches!(
129
+ r.get("name"),
130
+ Some(Value::Text(name)) if name == "apple"
131
+ )),
132
+ "indexed rows should include the created file"
133
+ );
134
+ }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes