dirsql 0.3.25__tar.gz → 0.3.27__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.
- {dirsql-0.3.25 → dirsql-0.3.27}/Cargo.lock +1 -1
- {dirsql-0.3.25 → dirsql-0.3.27}/PKG-INFO +1 -1
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/python/Cargo.toml +1 -1
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/rust/src/lib.rs +264 -49
- dirsql-0.3.27/packages/rust/tests/watch_relative_root.rs +134 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/Cargo.toml +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/README.md +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/dirsql/__init__.py +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/dirsql/_async.py +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/dirsql/_async_test.py +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/dirsql/_dirsql.pyi +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/dirsql/cli/__init__.py +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/dirsql/cli/binary_path.py +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/dirsql/cli/binary_path_test.py +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/dirsql/cli/interpret/__init__.py +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/dirsql/cli/interpret/dispatch_extract.py +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/dirsql/cli/interpret/dispatch_extract_test.py +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/dirsql/cli/interpret/load_app.py +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/dirsql/cli/interpret/load_app_test.py +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/dirsql/cli/interpret/run.py +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/dirsql/cli/interpret/run_test.py +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/dirsql/cli/interpret/write_message.py +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/dirsql/cli/interpret/write_message_test.py +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/dirsql/cli/is_windows.py +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/dirsql/cli/is_windows_test.py +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/dirsql/cli/main.py +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/dirsql/cli/main_test.py +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/dirsql/py.typed +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/dirsql/resolve_config.py +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/dirsql/resolve_config_test.py +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/docs/.claude/CLAUDE.md +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/docs/.vitepress/config.ts +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/docs/.vitepress/theme/index.ts +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/docs/.vitepress/theme/lang.ts +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/docs/AGENTS.md +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/docs/api/index.md +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/docs/cli/config.md +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/docs/cli/http-api.md +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/docs/cli/index.md +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/docs/cli/init.md +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/docs/cli/server.md +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/docs/getting-started.md +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/docs/guide/async.md +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/docs/guide/crdt.md +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/docs/guide/persistence.md +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/docs/guide/querying.md +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/docs/guide/tables.md +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/docs/guide/watching.md +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/docs/index.md +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/docs/migrations.md +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/docs/package.json +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/docs/playwright.config.ts +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/docs/pnpm-lock.yaml +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/docs/pnpm-workspace.yaml +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/docs/tests/integration/home.spec.ts +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/docs/tests/integration/language-flag.spec.ts +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/docs/tests/unit/config.test.ts +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/docs/tests/unit/lang.test.ts +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/docs/vitest.config.ts +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/python/README.md +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/python/conftest.py +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/python/docs/.claude/CLAUDE.md +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/python/docs/.vitepress/config.ts +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/python/docs/.vitepress/theme/index.ts +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/python/docs/.vitepress/theme/lang.ts +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/python/docs/AGENTS.md +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/python/docs/api/index.md +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/python/docs/cli/config.md +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/python/docs/cli/http-api.md +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/python/docs/cli/index.md +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/python/docs/cli/init.md +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/python/docs/cli/server.md +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/python/docs/getting-started.md +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/python/docs/guide/async.md +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/python/docs/guide/crdt.md +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/python/docs/guide/persistence.md +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/python/docs/guide/querying.md +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/python/docs/guide/tables.md +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/python/docs/guide/watching.md +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/python/docs/index.md +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/python/docs/migrations.md +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/python/docs/package.json +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/python/docs/playwright.config.ts +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/python/docs/pnpm-lock.yaml +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/python/docs/pnpm-workspace.yaml +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/python/docs/tests/integration/home.spec.ts +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/python/docs/tests/integration/language-flag.spec.ts +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/python/docs/tests/unit/config.test.ts +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/python/docs/tests/unit/lang.test.ts +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/python/docs/vitest.config.ts +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/python/src/lib.rs +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/python/tests/__init__.py +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/python/tests/conftest.py +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/python/tests/e2e/__init__.py +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/python/tests/integration/__fixtures__/data/a/meta.json +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/python/tests/integration/__fixtures__/data/b/meta.json +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/python/tests/integration/__fixtures__/dirsql.config.py +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/python/tests/integration/__fixtures__/interpret/data/a/meta.json +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/python/tests/integration/__fixtures__/interpret/data/b/meta.json +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/python/tests/integration/__fixtures__/interpret/dirsql.config.py +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/python/tests/integration/__fixtures__/interpret/dirsql.config_no_app.py +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/python/tests/integration/__fixtures__/interpret/dirsql.config_raises.py +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/python/tests/integration/__init__.py +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/python/tests/integration/interpret_subprocess.py +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/python/tests/integration/test_async_dirsql.py +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/python/tests/integration/test_binding.py +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/python/tests/integration/test_dirsql.py +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/python/tests/integration/test_docs_examples.py +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/python/tests/integration/test_docs_gaps.py +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/python/tests/integration/test_from_config.py +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/python/tests/integration/test_interpret.py +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/python/tests/integration/test_native_config.py +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/python/tests/integration/test_persist.py +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/python/tests/integration/test_serialization.py +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/rust/Cargo.toml +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/rust/README.md +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/rust/benches/db_bench.rs +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/rust/benches/differ_bench.rs +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/rust/benches/matcher_bench.rs +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/rust/benches/scanner_bench.rs +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/rust/docs/api/index.md +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/rust/docs/cli/config.md +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/rust/docs/cli/http-api.md +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/rust/docs/cli/index.md +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/rust/docs/cli/init.md +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/rust/docs/cli/server.md +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/rust/docs/getting-started.md +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/rust/docs/guide/async.md +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/rust/docs/guide/crdt.md +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/rust/docs/guide/persistence.md +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/rust/docs/guide/querying.md +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/rust/docs/guide/tables.md +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/rust/docs/guide/watching.md +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/rust/docs/index.md +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/rust/docs/migrations.md +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/rust/src/bin/dirsql.rs +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/rust/src/cli/init.rs +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/rust/src/cli/mod.rs +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/rust/src/cli/native_config.rs +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/rust/src/cli/router.rs +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/rust/src/cli/serialize.rs +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/rust/src/cli/server.rs +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/rust/src/config.rs +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/rust/src/db.rs +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/rust/src/differ.rs +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/rust/src/matcher.rs +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/rust/src/persist.rs +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/rust/src/scanner.rs +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/rust/src/watcher.rs +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/rust/tests/async_sdk.rs +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/rust/tests/cli_e2e.rs +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/rust/tests/cli_integration.rs +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/rust/tests/code_review_findings.rs +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/rust/tests/config.rs +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/rust/tests/docs_examples.rs +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/rust/tests/docs_gaps.rs +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/rust/tests/extensions.rs +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/rust/tests/from_config.rs +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/rust/tests/init_e2e.rs +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/rust/tests/init_integration.rs +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/rust/tests/persist.rs +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/rust/tests/readonly_query.rs +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/rust/tests/scanner.rs +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/rust/tests/sdk.rs +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/rust/tests/serialization.rs +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/packages/rust/tests/watcher.rs +0 -0
- {dirsql-0.3.25 → dirsql-0.3.27}/pyproject.toml +0 -0
|
@@ -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.
|
|
7
|
+
version = "0.3.27"
|
|
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
|
-
|
|
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.
|
|
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,
|
|
@@ -1457,6 +1484,40 @@ const STAT_CTIME: &str = "_ctime";
|
|
|
1457
1484
|
/// (`_path`, `_basename`, `_dir`, `_ext`) and stat-derived (`_size`,
|
|
1458
1485
|
/// `_mtime`, `_ctime`).
|
|
1459
1486
|
fn compute_stat_virtuals(rel_path: &str, abs_path: &Path) -> Row {
|
|
1487
|
+
// Read the file's stats once; a missing/unreadable file yields all-`None`,
|
|
1488
|
+
// which `stat_virtuals` renders as absent `_size`/`_mtime`/`_ctime`
|
|
1489
|
+
// columns. `_mtime`/`_ctime` are `None` when the platform can't supply
|
|
1490
|
+
// them (or the value predates the epoch). The pure column-building logic
|
|
1491
|
+
// lives in `stat_virtuals`.
|
|
1492
|
+
let (size, mtime_secs, ctime_secs) = match std::fs::metadata(abs_path) {
|
|
1493
|
+
Ok(metadata) => {
|
|
1494
|
+
let to_secs = |t: std::io::Result<std::time::SystemTime>| {
|
|
1495
|
+
t.ok()
|
|
1496
|
+
.and_then(|st| st.duration_since(std::time::UNIX_EPOCH).ok())
|
|
1497
|
+
.map(|d| d.as_secs() as i64)
|
|
1498
|
+
};
|
|
1499
|
+
(
|
|
1500
|
+
Some(metadata.len() as i64),
|
|
1501
|
+
to_secs(metadata.modified()),
|
|
1502
|
+
to_secs(metadata.created()),
|
|
1503
|
+
)
|
|
1504
|
+
}
|
|
1505
|
+
Err(_) => (None, None, None),
|
|
1506
|
+
};
|
|
1507
|
+
stat_virtuals(rel_path, size, mtime_secs, ctime_secs)
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
/// Pure core of [`compute_stat_virtuals`]: build the filesystem-fact columns
|
|
1511
|
+
/// from the relative path plus already-read stat values (each `None` when the
|
|
1512
|
+
/// corresponding fact is unavailable). Split out so the column-mapping logic
|
|
1513
|
+
/// is unit-testable without touching the filesystem; the metadata read lives
|
|
1514
|
+
/// in the caller.
|
|
1515
|
+
fn stat_virtuals(
|
|
1516
|
+
rel_path: &str,
|
|
1517
|
+
size: Option<i64>,
|
|
1518
|
+
mtime_secs: Option<i64>,
|
|
1519
|
+
ctime_secs: Option<i64>,
|
|
1520
|
+
) -> Row {
|
|
1460
1521
|
let mut out = Row::new();
|
|
1461
1522
|
|
|
1462
1523
|
out.insert(STAT_PATH.into(), Value::Text(rel_path.to_string()));
|
|
@@ -1484,18 +1545,14 @@ fn compute_stat_virtuals(rel_path: &str, abs_path: &Path) -> Row {
|
|
|
1484
1545
|
);
|
|
1485
1546
|
}
|
|
1486
1547
|
|
|
1487
|
-
if let
|
|
1488
|
-
out.insert(STAT_SIZE.into(), Value::Integer(
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
&& let Ok(d) = ctime.duration_since(std::time::UNIX_EPOCH)
|
|
1496
|
-
{
|
|
1497
|
-
out.insert(STAT_CTIME.into(), Value::Integer(d.as_secs() as i64));
|
|
1498
|
-
}
|
|
1548
|
+
if let Some(size) = size {
|
|
1549
|
+
out.insert(STAT_SIZE.into(), Value::Integer(size));
|
|
1550
|
+
}
|
|
1551
|
+
if let Some(mtime) = mtime_secs {
|
|
1552
|
+
out.insert(STAT_MTIME.into(), Value::Integer(mtime));
|
|
1553
|
+
}
|
|
1554
|
+
if let Some(ctime) = ctime_secs {
|
|
1555
|
+
out.insert(STAT_CTIME.into(), Value::Integer(ctime));
|
|
1499
1556
|
}
|
|
1500
1557
|
|
|
1501
1558
|
out
|
|
@@ -1753,26 +1810,29 @@ mod internal_tests {
|
|
|
1753
1810
|
use super::*;
|
|
1754
1811
|
use tempfile::TempDir;
|
|
1755
1812
|
|
|
1756
|
-
/// A
|
|
1757
|
-
/// `Some` arms of `
|
|
1758
|
-
///
|
|
1813
|
+
/// A relative path with a basename, parent dir, and extension exercises the
|
|
1814
|
+
/// `Some` arms of `stat_virtuals`' path inspection, plus the `Some` arms of
|
|
1815
|
+
/// the size/mtime/ctime inserts. The metadata read that supplies those
|
|
1816
|
+
/// values lives in `compute_stat_virtuals` and is covered by the
|
|
1817
|
+
/// integration suite (real-file scans).
|
|
1759
1818
|
#[test]
|
|
1760
|
-
fn
|
|
1761
|
-
let
|
|
1762
|
-
let abs = dir.path().join("sub.txt");
|
|
1763
|
-
std::fs::create_dir_all(dir.path().join("sub")).ok();
|
|
1764
|
-
std::fs::write(&abs, b"hello").unwrap();
|
|
1765
|
-
let stat = compute_stat_virtuals("nested/sub.txt", &abs);
|
|
1819
|
+
fn stat_virtuals_populates_all_fields() {
|
|
1820
|
+
let stat = stat_virtuals("nested/sub.txt", Some(5), Some(100), Some(50));
|
|
1766
1821
|
assert_eq!(stat[STAT_PATH], Value::Text("nested/sub.txt".into()));
|
|
1767
1822
|
assert_eq!(stat[STAT_BASENAME], Value::Text("sub.txt".into()));
|
|
1768
1823
|
assert_eq!(stat[STAT_DIR], Value::Text("nested".into()));
|
|
1769
1824
|
assert_eq!(stat[STAT_EXT], Value::Text("txt".into()));
|
|
1770
1825
|
assert!(matches!(stat.get(STAT_SIZE), Some(Value::Integer(5))));
|
|
1826
|
+
assert!(matches!(stat.get(STAT_MTIME), Some(Value::Integer(100))));
|
|
1827
|
+
assert!(matches!(stat.get(STAT_CTIME), Some(Value::Integer(50))));
|
|
1771
1828
|
}
|
|
1772
1829
|
|
|
1773
1830
|
/// A bare filename has no parent component and no extension, and a
|
|
1774
|
-
/// nonexistent abs path makes `std::fs::metadata`
|
|
1775
|
-
///
|
|
1831
|
+
/// nonexistent abs path makes `compute_stat_virtuals`' `std::fs::metadata`
|
|
1832
|
+
/// read fail (its `Err` arm -> all-`None`). This drives the skip branches:
|
|
1833
|
+
/// no `_ext`, no `_size`/`_mtime`/`_ctime`. (Calling the real
|
|
1834
|
+
/// `compute_stat_virtuals` with a nonexistent path keeps the test free of a
|
|
1835
|
+
/// direct `std::fs` call while still covering the read-failure arm.)
|
|
1776
1836
|
#[test]
|
|
1777
1837
|
fn compute_stat_virtuals_skips_absent_fields() {
|
|
1778
1838
|
let stat = compute_stat_virtuals("bare", Path::new("/nonexistent-xyz/bare"));
|
|
@@ -1867,6 +1927,171 @@ mod internal_tests {
|
|
|
1867
1927
|
assert_eq!(events.len(), 1, "non-ignored path must produce one event");
|
|
1868
1928
|
}
|
|
1869
1929
|
|
|
1930
|
+
// -----------------------------------------------------------------------
|
|
1931
|
+
// #250: canonical `watch_root` and the strip-prefix fallbacks.
|
|
1932
|
+
//
|
|
1933
|
+
// `std::env::set_current_dir` is process-global, so the relative-root test
|
|
1934
|
+
// serializes through this lock and restores the cwd on the way out.
|
|
1935
|
+
// -----------------------------------------------------------------------
|
|
1936
|
+
|
|
1937
|
+
fn cwd_lock() -> &'static Mutex<()> {
|
|
1938
|
+
static LOCK: std::sync::OnceLock<Mutex<()>> = std::sync::OnceLock::new();
|
|
1939
|
+
LOCK.get_or_init(|| Mutex::new(()))
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
/// Building with a **relative** root canonicalizes `watch_root` to an
|
|
1943
|
+
/// absolute path while leaving `root` (and therefore `config()` / `_path`)
|
|
1944
|
+
/// exactly as the caller supplied it. This is the core of the #250 fix:
|
|
1945
|
+
/// `start_watching` watches `watch_root`, so `notify` never sees `.`.
|
|
1946
|
+
#[test]
|
|
1947
|
+
fn relative_root_canonicalizes_watch_root_only() {
|
|
1948
|
+
let dir = TempDir::new().unwrap();
|
|
1949
|
+
let canonical = std::fs::canonicalize(dir.path()).unwrap();
|
|
1950
|
+
|
|
1951
|
+
let _guard = cwd_lock().lock().unwrap_or_else(|p| p.into_inner());
|
|
1952
|
+
let original = std::env::current_dir().unwrap();
|
|
1953
|
+
std::env::set_current_dir(&canonical).unwrap();
|
|
1954
|
+
|
|
1955
|
+
let db = DirSQL::new(
|
|
1956
|
+
".",
|
|
1957
|
+
vec![Table::new("CREATE TABLE t (x TEXT)", "*.txt", |_| vec![])],
|
|
1958
|
+
)
|
|
1959
|
+
.unwrap();
|
|
1960
|
+
|
|
1961
|
+
// Restore cwd before asserting so a failure can't strand the process.
|
|
1962
|
+
std::env::set_current_dir(&original).unwrap();
|
|
1963
|
+
|
|
1964
|
+
// `root` is preserved verbatim; `config()` echoes it.
|
|
1965
|
+
assert_eq!(db.inner.root, PathBuf::from("."));
|
|
1966
|
+
assert_eq!(db.config().root, PathBuf::from("."));
|
|
1967
|
+
// `watch_root` is absolute and points at the canonical temp dir.
|
|
1968
|
+
assert!(
|
|
1969
|
+
db.inner.watch_root.is_absolute(),
|
|
1970
|
+
"watch_root must be absolute, got {:?}",
|
|
1971
|
+
db.inner.watch_root
|
|
1972
|
+
);
|
|
1973
|
+
assert_eq!(db.inner.watch_root, canonical);
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
/// With an absolute root the canonical `watch_root` equals the (already
|
|
1977
|
+
/// canonical) root on this platform, and `process_file_event` strips that
|
|
1978
|
+
/// prefix to yield a root-relative `_path` — the first `strip_prefix`
|
|
1979
|
+
/// (watch_root) arm.
|
|
1980
|
+
#[test]
|
|
1981
|
+
fn process_file_event_strips_watch_root_prefix() {
|
|
1982
|
+
let dir = TempDir::new().unwrap();
|
|
1983
|
+
let canonical = std::fs::canonicalize(dir.path()).unwrap();
|
|
1984
|
+
let db = DirSQL::new(
|
|
1985
|
+
&canonical,
|
|
1986
|
+
vec![Table::new(
|
|
1987
|
+
"CREATE TABLE items (name TEXT, _path TEXT)",
|
|
1988
|
+
"**/*.txt",
|
|
1989
|
+
|_| {
|
|
1990
|
+
vec![Row::from_iter([(
|
|
1991
|
+
"name".to_string(),
|
|
1992
|
+
Value::Text("x".into()),
|
|
1993
|
+
)])]
|
|
1994
|
+
},
|
|
1995
|
+
)],
|
|
1996
|
+
)
|
|
1997
|
+
.unwrap();
|
|
1998
|
+
|
|
1999
|
+
let abs = canonical.join("nested").join("a.txt");
|
|
2000
|
+
std::fs::create_dir_all(canonical.join("nested")).unwrap();
|
|
2001
|
+
std::fs::write(&abs, b"").unwrap();
|
|
2002
|
+
|
|
2003
|
+
let events = db.process_file_event(FileEvent::Created(abs));
|
|
2004
|
+
assert_eq!(events.len(), 1, "expected one insert: {events:?}");
|
|
2005
|
+
match &events[0] {
|
|
2006
|
+
RowEvent::Insert { row, .. } => {
|
|
2007
|
+
assert_eq!(
|
|
2008
|
+
row.get("_path"),
|
|
2009
|
+
Some(&Value::Text("nested/a.txt".to_string())),
|
|
2010
|
+
"watch_root prefix must be stripped to a root-relative path"
|
|
2011
|
+
);
|
|
2012
|
+
}
|
|
2013
|
+
other => panic!("expected Insert, got {other:?}"),
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
/// When an event path lies under the user-supplied `root` but not under
|
|
2018
|
+
/// the canonical `watch_root`, the `.or_else` fallback strips `root`
|
|
2019
|
+
/// instead. We force that split by pointing `watch_root` at a sibling that
|
|
2020
|
+
/// is not a prefix of the event path, leaving `root` as the real dir.
|
|
2021
|
+
#[test]
|
|
2022
|
+
fn process_file_event_falls_back_to_root_prefix() {
|
|
2023
|
+
let dir = TempDir::new().unwrap();
|
|
2024
|
+
let canonical = std::fs::canonicalize(dir.path()).unwrap();
|
|
2025
|
+
let mut db = DirSQL::new(
|
|
2026
|
+
&canonical,
|
|
2027
|
+
vec![Table::new(
|
|
2028
|
+
"CREATE TABLE items (name TEXT, _path TEXT)",
|
|
2029
|
+
"**/*.txt",
|
|
2030
|
+
|_| {
|
|
2031
|
+
vec![Row::from_iter([(
|
|
2032
|
+
"name".to_string(),
|
|
2033
|
+
Value::Text("x".into()),
|
|
2034
|
+
)])]
|
|
2035
|
+
},
|
|
2036
|
+
)],
|
|
2037
|
+
)
|
|
2038
|
+
.unwrap();
|
|
2039
|
+
|
|
2040
|
+
// Repoint watch_root to a non-prefix sibling so the first strip misses
|
|
2041
|
+
// and the `.or_else(root)` arm runs. `root` stays the real dir.
|
|
2042
|
+
Arc::get_mut(&mut db.inner).unwrap().watch_root = canonical.join("does-not-prefix");
|
|
2043
|
+
|
|
2044
|
+
let abs = canonical.join("b.txt");
|
|
2045
|
+
std::fs::write(&abs, b"").unwrap();
|
|
2046
|
+
let events = db.process_file_event(FileEvent::Created(abs));
|
|
2047
|
+
assert_eq!(events.len(), 1, "expected one insert: {events:?}");
|
|
2048
|
+
match &events[0] {
|
|
2049
|
+
RowEvent::Insert { row, .. } => {
|
|
2050
|
+
assert_eq!(
|
|
2051
|
+
row.get("_path"),
|
|
2052
|
+
Some(&Value::Text("b.txt".to_string())),
|
|
2053
|
+
"root fallback must strip the user-supplied root prefix"
|
|
2054
|
+
);
|
|
2055
|
+
}
|
|
2056
|
+
other => panic!("expected Insert, got {other:?}"),
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
/// When the event path is under neither `watch_root` nor `root`, the final
|
|
2061
|
+
/// `unwrap_or(&abs_path)` arm keeps the absolute path. A path that matches
|
|
2062
|
+
/// no table glob then yields no events, but the strip fallback is still
|
|
2063
|
+
/// executed — we assert the no-event outcome to pin the arm without relying
|
|
2064
|
+
/// on a row.
|
|
2065
|
+
#[test]
|
|
2066
|
+
fn process_file_event_keeps_absolute_path_when_no_prefix_matches() {
|
|
2067
|
+
let dir = TempDir::new().unwrap();
|
|
2068
|
+
let canonical = std::fs::canonicalize(dir.path()).unwrap();
|
|
2069
|
+
let db = DirSQL::new(
|
|
2070
|
+
&canonical,
|
|
2071
|
+
vec![Table::new(
|
|
2072
|
+
"CREATE TABLE items (name TEXT)",
|
|
2073
|
+
"*.txt",
|
|
2074
|
+
|_| {
|
|
2075
|
+
vec![Row::from_iter([(
|
|
2076
|
+
"name".to_string(),
|
|
2077
|
+
Value::Text("x".into()),
|
|
2078
|
+
)])]
|
|
2079
|
+
},
|
|
2080
|
+
)],
|
|
2081
|
+
)
|
|
2082
|
+
.unwrap();
|
|
2083
|
+
|
|
2084
|
+
// A path outside both roots: neither strip matches, so the absolute
|
|
2085
|
+
// path is used as the relative path. It does not match `*.txt` at the
|
|
2086
|
+
// root, so no events are produced.
|
|
2087
|
+
let outside = PathBuf::from("/some/elsewhere/c.md");
|
|
2088
|
+
let events = db.process_file_event(FileEvent::Created(outside));
|
|
2089
|
+
assert!(
|
|
2090
|
+
events.is_empty(),
|
|
2091
|
+
"unmatched absolute path must produce no events: {events:?}"
|
|
2092
|
+
);
|
|
2093
|
+
}
|
|
2094
|
+
|
|
1870
2095
|
/// Drive `reconcile_scan` directly with a cached file whose
|
|
1871
2096
|
/// `snapshot_ns <= mtime_ns`, forcing the racy-window hash-confirm branch.
|
|
1872
2097
|
/// With a matching content hash the file is trusted.
|
|
@@ -1966,36 +2191,26 @@ mod internal_tests {
|
|
|
1966
2191
|
// public-API integration suite.
|
|
1967
2192
|
// -----------------------------------------------------------------------
|
|
1968
2193
|
|
|
1969
|
-
/// Poison a mutex by panicking while holding its guard on
|
|
2194
|
+
/// Poison a mutex by panicking while holding its guard. `catch_unwind` on
|
|
2195
|
+
/// the current thread does this without `std::thread` (the `unit lint`
|
|
2196
|
+
/// isolation rule keeps effectful std out of unit tests): the guard's
|
|
2197
|
+
/// `Drop` runs during unwinding and marks the mutex poisoned.
|
|
1970
2198
|
fn poison<T: Send>(m: &Mutex<T>) {
|
|
1971
|
-
let _ = std::
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
})
|
|
1976
|
-
.join()
|
|
1977
|
-
});
|
|
2199
|
+
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
|
2200
|
+
let _g = m.lock().unwrap();
|
|
2201
|
+
panic!("poison");
|
|
2202
|
+
}));
|
|
1978
2203
|
assert!(m.is_poisoned(), "mutex should be poisoned");
|
|
1979
2204
|
}
|
|
1980
2205
|
|
|
1981
|
-
/// Build a
|
|
1982
|
-
///
|
|
1983
|
-
///
|
|
1984
|
-
///
|
|
2206
|
+
/// Build a tableless `DirSQL` over an empty temp dir. These tests only
|
|
2207
|
+
/// need a live instance whose inner mutexes can be poisoned, so there is no
|
|
2208
|
+
/// table or file to stage (which keeps `std::fs` out of this unit module).
|
|
2209
|
+
/// Extract-closure coverage lives in the `process_file_event_*` tests.
|
|
1985
2210
|
fn simple_db() -> (TempDir, DirSQL) {
|
|
1986
2211
|
let dir = TempDir::new().unwrap();
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
dir.path(),
|
|
1990
|
-
vec![Table::new("CREATE TABLE t (name TEXT)", "*.txt", |_| {
|
|
1991
|
-
vec![Row::from_iter([(
|
|
1992
|
-
"name".to_string(),
|
|
1993
|
-
Value::Text("x".into()),
|
|
1994
|
-
)])]
|
|
1995
|
-
})],
|
|
1996
|
-
Vec::<String>::new(),
|
|
1997
|
-
)
|
|
1998
|
-
.unwrap();
|
|
2212
|
+
let db =
|
|
2213
|
+
DirSQL::with_ignore(dir.path(), Vec::<Table>::new(), Vec::<String>::new()).unwrap();
|
|
1999
2214
|
(dir, db)
|
|
2000
2215
|
}
|
|
2001
2216
|
|
|
@@ -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
|
|
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
|
|
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
|
{dirsql-0.3.25 → dirsql-0.3.27}/packages/python/docs/tests/integration/language-flag.spec.ts
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dirsql-0.3.25 → dirsql-0.3.27}/packages/python/tests/integration/__fixtures__/data/a/meta.json
RENAMED
|
File without changes
|
{dirsql-0.3.25 → dirsql-0.3.27}/packages/python/tests/integration/__fixtures__/data/b/meta.json
RENAMED
|
File without changes
|
{dirsql-0.3.25 → dirsql-0.3.27}/packages/python/tests/integration/__fixtures__/dirsql.config.py
RENAMED
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|