dirsql 0.3.24__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.
- {dirsql-0.3.24 → dirsql-0.3.26}/Cargo.lock +1 -1
- {dirsql-0.3.24 → dirsql-0.3.26}/PKG-INFO +1 -1
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/python/Cargo.toml +1 -1
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/rust/src/config.rs +0 -17
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/rust/src/lib.rs +194 -2
- dirsql-0.3.26/packages/rust/src/scanner.rs +83 -0
- dirsql-0.3.26/packages/rust/tests/config.rs +26 -0
- dirsql-0.3.26/packages/rust/tests/scanner.rs +97 -0
- dirsql-0.3.26/packages/rust/tests/watch_relative_root.rs +134 -0
- dirsql-0.3.24/packages/rust/src/scanner.rs +0 -136
- {dirsql-0.3.24 → dirsql-0.3.26}/Cargo.toml +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/README.md +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/dirsql/__init__.py +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/dirsql/_async.py +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/dirsql/_async_test.py +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/dirsql/_dirsql.pyi +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/dirsql/cli/__init__.py +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/dirsql/cli/binary_path.py +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/dirsql/cli/binary_path_test.py +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/dirsql/cli/interpret/__init__.py +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/dirsql/cli/interpret/dispatch_extract.py +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/dirsql/cli/interpret/dispatch_extract_test.py +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/dirsql/cli/interpret/load_app.py +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/dirsql/cli/interpret/load_app_test.py +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/dirsql/cli/interpret/run.py +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/dirsql/cli/interpret/run_test.py +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/dirsql/cli/interpret/write_message.py +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/dirsql/cli/interpret/write_message_test.py +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/dirsql/cli/is_windows.py +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/dirsql/cli/is_windows_test.py +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/dirsql/cli/main.py +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/dirsql/cli/main_test.py +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/dirsql/py.typed +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/dirsql/resolve_config.py +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/dirsql/resolve_config_test.py +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/docs/.claude/CLAUDE.md +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/docs/.vitepress/config.ts +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/docs/.vitepress/theme/index.ts +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/docs/.vitepress/theme/lang.ts +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/docs/AGENTS.md +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/docs/api/index.md +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/docs/cli/config.md +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/docs/cli/http-api.md +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/docs/cli/index.md +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/docs/cli/init.md +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/docs/cli/server.md +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/docs/getting-started.md +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/docs/guide/async.md +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/docs/guide/crdt.md +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/docs/guide/persistence.md +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/docs/guide/querying.md +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/docs/guide/tables.md +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/docs/guide/watching.md +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/docs/index.md +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/docs/migrations.md +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/docs/package.json +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/docs/playwright.config.ts +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/docs/pnpm-lock.yaml +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/docs/pnpm-workspace.yaml +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/docs/tests/integration/home.spec.ts +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/docs/tests/integration/language-flag.spec.ts +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/docs/tests/unit/config.test.ts +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/docs/tests/unit/lang.test.ts +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/docs/vitest.config.ts +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/python/README.md +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/python/conftest.py +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/python/docs/.claude/CLAUDE.md +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/python/docs/.vitepress/config.ts +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/python/docs/.vitepress/theme/index.ts +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/python/docs/.vitepress/theme/lang.ts +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/python/docs/AGENTS.md +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/python/docs/api/index.md +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/python/docs/cli/config.md +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/python/docs/cli/http-api.md +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/python/docs/cli/index.md +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/python/docs/cli/init.md +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/python/docs/cli/server.md +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/python/docs/getting-started.md +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/python/docs/guide/async.md +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/python/docs/guide/crdt.md +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/python/docs/guide/persistence.md +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/python/docs/guide/querying.md +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/python/docs/guide/tables.md +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/python/docs/guide/watching.md +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/python/docs/index.md +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/python/docs/migrations.md +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/python/docs/package.json +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/python/docs/playwright.config.ts +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/python/docs/pnpm-lock.yaml +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/python/docs/pnpm-workspace.yaml +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/python/docs/tests/integration/home.spec.ts +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/python/docs/tests/integration/language-flag.spec.ts +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/python/docs/tests/unit/config.test.ts +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/python/docs/tests/unit/lang.test.ts +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/python/docs/vitest.config.ts +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/python/src/lib.rs +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/python/tests/__init__.py +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/python/tests/conftest.py +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/python/tests/e2e/__init__.py +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/python/tests/integration/__fixtures__/data/a/meta.json +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/python/tests/integration/__fixtures__/data/b/meta.json +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/python/tests/integration/__fixtures__/dirsql.config.py +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/python/tests/integration/__fixtures__/interpret/data/a/meta.json +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/python/tests/integration/__fixtures__/interpret/data/b/meta.json +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/python/tests/integration/__fixtures__/interpret/dirsql.config.py +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/python/tests/integration/__fixtures__/interpret/dirsql.config_no_app.py +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/python/tests/integration/__fixtures__/interpret/dirsql.config_raises.py +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/python/tests/integration/__init__.py +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/python/tests/integration/interpret_subprocess.py +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/python/tests/integration/test_async_dirsql.py +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/python/tests/integration/test_binding.py +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/python/tests/integration/test_dirsql.py +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/python/tests/integration/test_docs_examples.py +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/python/tests/integration/test_docs_gaps.py +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/python/tests/integration/test_from_config.py +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/python/tests/integration/test_interpret.py +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/python/tests/integration/test_native_config.py +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/python/tests/integration/test_persist.py +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/python/tests/integration/test_serialization.py +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/rust/Cargo.toml +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/rust/README.md +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/rust/benches/db_bench.rs +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/rust/benches/differ_bench.rs +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/rust/benches/matcher_bench.rs +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/rust/benches/scanner_bench.rs +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/rust/docs/api/index.md +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/rust/docs/cli/config.md +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/rust/docs/cli/http-api.md +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/rust/docs/cli/index.md +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/rust/docs/cli/init.md +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/rust/docs/cli/server.md +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/rust/docs/getting-started.md +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/rust/docs/guide/async.md +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/rust/docs/guide/crdt.md +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/rust/docs/guide/persistence.md +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/rust/docs/guide/querying.md +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/rust/docs/guide/tables.md +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/rust/docs/guide/watching.md +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/rust/docs/index.md +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/rust/docs/migrations.md +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/rust/src/bin/dirsql.rs +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/rust/src/cli/init.rs +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/rust/src/cli/mod.rs +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/rust/src/cli/native_config.rs +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/rust/src/cli/router.rs +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/rust/src/cli/serialize.rs +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/rust/src/cli/server.rs +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/rust/src/db.rs +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/rust/src/differ.rs +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/rust/src/matcher.rs +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/rust/src/persist.rs +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/rust/src/watcher.rs +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/rust/tests/async_sdk.rs +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/rust/tests/cli_e2e.rs +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/rust/tests/cli_integration.rs +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/rust/tests/code_review_findings.rs +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/rust/tests/docs_examples.rs +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/rust/tests/docs_gaps.rs +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/rust/tests/extensions.rs +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/rust/tests/from_config.rs +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/rust/tests/init_e2e.rs +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/rust/tests/init_integration.rs +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/rust/tests/persist.rs +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/rust/tests/readonly_query.rs +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/rust/tests/sdk.rs +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/rust/tests/serialization.rs +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/packages/rust/tests/watcher.rs +0 -0
- {dirsql-0.3.24 → dirsql-0.3.26}/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.26"
|
|
8
8
|
edition.workspace = true
|
|
9
9
|
publish = false
|
|
10
10
|
readme = "README.md"
|
|
@@ -256,23 +256,6 @@ glob = "*.json"
|
|
|
256
256
|
assert!(config.ignore.is_empty());
|
|
257
257
|
}
|
|
258
258
|
|
|
259
|
-
#[test]
|
|
260
|
-
fn load_config_from_file() {
|
|
261
|
-
let dir = tempfile::tempdir().unwrap();
|
|
262
|
-
let path = dir.path().join(".dirsql.toml");
|
|
263
|
-
std::fs::write(
|
|
264
|
-
&path,
|
|
265
|
-
r#"
|
|
266
|
-
[[table]]
|
|
267
|
-
ddl = "CREATE TABLE t (_path TEXT)"
|
|
268
|
-
glob = "*.csv"
|
|
269
|
-
"#,
|
|
270
|
-
)
|
|
271
|
-
.unwrap();
|
|
272
|
-
let config = load_config(&path).unwrap();
|
|
273
|
-
assert_eq!(config.tables.len(), 1);
|
|
274
|
-
}
|
|
275
|
-
|
|
276
259
|
#[test]
|
|
277
260
|
fn load_config_missing_file_returns_io_error() {
|
|
278
261
|
let err = load_config(Path::new("/nonexistent/.dirsql.toml")).unwrap_err();
|
|
@@ -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,
|
|
@@ -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,83 @@
|
|
|
1
|
+
use crate::matcher::TableMatcher;
|
|
2
|
+
use std::path::{Path, PathBuf};
|
|
3
|
+
use walkdir::WalkDir;
|
|
4
|
+
|
|
5
|
+
/// Top-level directory name reserved for `dirsql`'s own metadata (e.g. the
|
|
6
|
+
/// persistent cache database). Always excluded from the scan, regardless of
|
|
7
|
+
/// whether persistence is enabled.
|
|
8
|
+
pub const RESERVED_DIR: &str = ".dirsql";
|
|
9
|
+
|
|
10
|
+
/// Walk a directory tree and return all file paths paired with their matching table name.
|
|
11
|
+
/// Ignored paths and directories are skipped. Only files (not directories) are returned.
|
|
12
|
+
///
|
|
13
|
+
/// The top-level `.dirsql/` directory is unconditionally excluded.
|
|
14
|
+
pub fn scan_directory(root: &Path, matcher: &TableMatcher) -> Vec<(PathBuf, String)> {
|
|
15
|
+
let mut results = Vec::new();
|
|
16
|
+
|
|
17
|
+
let walker = WalkDir::new(root).into_iter().filter_entry(|entry| {
|
|
18
|
+
// Skip the reserved `.dirsql/` directory at the top level.
|
|
19
|
+
!is_reserved_dir(entry.depth(), entry.file_type().is_dir(), entry.file_name())
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
for entry in walker.filter_map(|e| e.ok()) {
|
|
23
|
+
let path = entry.path();
|
|
24
|
+
|
|
25
|
+
// Match against relative path so globs like "comments/**/*.jsonl" work
|
|
26
|
+
// regardless of the absolute root directory.
|
|
27
|
+
let rel_path = path.strip_prefix(root).unwrap_or(path);
|
|
28
|
+
|
|
29
|
+
if matcher.is_ignored(rel_path) {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if !entry.file_type().is_file() {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if let Some(table_name) = matcher.match_file(rel_path) {
|
|
38
|
+
results.push((path.to_path_buf(), table_name.to_string()));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
results
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/// True for the reserved top-level `.dirsql/` directory (`depth == 1`), which
|
|
46
|
+
/// the scan unconditionally excludes. Factored out as a pure predicate over the
|
|
47
|
+
/// facts `WalkDir` exposes so it can be unit-tested without walking a real tree
|
|
48
|
+
/// -- the directory-walk behavior itself is covered by `tests/scanner.rs`.
|
|
49
|
+
fn is_reserved_dir(depth: usize, is_dir: bool, file_name: &std::ffi::OsStr) -> bool {
|
|
50
|
+
depth == 1 && is_dir && file_name == RESERVED_DIR
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
#[cfg(test)]
|
|
54
|
+
mod tests {
|
|
55
|
+
use super::*;
|
|
56
|
+
use std::ffi::OsStr;
|
|
57
|
+
|
|
58
|
+
// The directory-walk behavior of `scan_directory` (real temp trees, real
|
|
59
|
+
// files) is exercised by `tests/scanner.rs` -- those are integration tests
|
|
60
|
+
// and stay out of this inline unit module, which the `unit lint` isolation
|
|
61
|
+
// rule keeps free of effectful std. What remains here is the pure
|
|
62
|
+
// reserved-dir predicate the walker filters on.
|
|
63
|
+
|
|
64
|
+
#[test]
|
|
65
|
+
fn is_reserved_dir_matches_top_level_dirsql() {
|
|
66
|
+
assert!(is_reserved_dir(1, true, OsStr::new(RESERVED_DIR)));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
#[test]
|
|
70
|
+
fn is_reserved_dir_rejects_nested_dirsql() {
|
|
71
|
+
// Only the *top-level* `.dirsql/` (depth 1) is reserved; a nested one
|
|
72
|
+
// (e.g. `sub/.dirsql/`) is an ordinary directory.
|
|
73
|
+
assert!(!is_reserved_dir(2, true, OsStr::new(RESERVED_DIR)));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
#[test]
|
|
77
|
+
fn is_reserved_dir_rejects_files_and_other_names() {
|
|
78
|
+
// A file named `.dirsql` is not the reserved directory ...
|
|
79
|
+
assert!(!is_reserved_dir(1, false, OsStr::new(RESERVED_DIR)));
|
|
80
|
+
// ... and an ordinary top-level directory is not reserved.
|
|
81
|
+
assert!(!is_reserved_dir(1, true, OsStr::new("data")));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
//! Integration test for `load_config` reading a real `.dirsql.toml` off disk.
|
|
2
|
+
//!
|
|
3
|
+
//! The pure TOML-parsing tests (`load_config_str`) stay inline in `config.rs`;
|
|
4
|
+
//! this exercises the thin file-reading wrapper, which needs a real file
|
|
5
|
+
//! (effectful std), so per the `unit lint` isolation rule it belongs in the
|
|
6
|
+
//! integration tier rather than the inline unit module.
|
|
7
|
+
|
|
8
|
+
use dirsql::config::load_config;
|
|
9
|
+
use tempfile::TempDir;
|
|
10
|
+
|
|
11
|
+
#[test]
|
|
12
|
+
fn load_config_from_file() {
|
|
13
|
+
let dir = TempDir::new().unwrap();
|
|
14
|
+
let path = dir.path().join(".dirsql.toml");
|
|
15
|
+
std::fs::write(
|
|
16
|
+
&path,
|
|
17
|
+
r#"
|
|
18
|
+
[[table]]
|
|
19
|
+
ddl = "CREATE TABLE t (_path TEXT)"
|
|
20
|
+
glob = "*.csv"
|
|
21
|
+
"#,
|
|
22
|
+
)
|
|
23
|
+
.unwrap();
|
|
24
|
+
let config = load_config(&path).unwrap();
|
|
25
|
+
assert_eq!(config.tables.len(), 1);
|
|
26
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
//! Integration tests for `scan_directory`.
|
|
2
|
+
//!
|
|
3
|
+
//! These build real temp directory trees with real files and assert what the
|
|
4
|
+
//! scanner returns end-to-end through its public API. They were moved out of
|
|
5
|
+
//! `scanner.rs`'s inline `#[cfg(test)]` module so that module stays pure (the
|
|
6
|
+
//! `testing-conventions` `unit lint` isolation rule forbids effectful std --
|
|
7
|
+
//! `std::fs`, temp dirs -- in unit tests). The pure reserved-dir predicate
|
|
8
|
+
//! test remains inline next to the function it covers.
|
|
9
|
+
|
|
10
|
+
use std::fs;
|
|
11
|
+
|
|
12
|
+
use dirsql::matcher::TableMatcher;
|
|
13
|
+
use dirsql::scanner::scan_directory;
|
|
14
|
+
use tempfile::TempDir;
|
|
15
|
+
|
|
16
|
+
#[test]
|
|
17
|
+
fn scan_finds_matching_files() {
|
|
18
|
+
let dir = TempDir::new().unwrap();
|
|
19
|
+
fs::write(dir.path().join("data.csv"), "a,b\n1,2").unwrap();
|
|
20
|
+
fs::write(dir.path().join("readme.md"), "# hi").unwrap();
|
|
21
|
+
|
|
22
|
+
let matcher = TableMatcher::new(&[("**/*.csv", "csv_table")], &[]).unwrap();
|
|
23
|
+
let results = scan_directory(dir.path(), &matcher);
|
|
24
|
+
|
|
25
|
+
assert_eq!(results.len(), 1);
|
|
26
|
+
assert!(results[0].0.ends_with("data.csv"));
|
|
27
|
+
assert_eq!(results[0].1, "csv_table");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
#[test]
|
|
31
|
+
fn scan_skips_ignored_files() {
|
|
32
|
+
let dir = TempDir::new().unwrap();
|
|
33
|
+
fs::write(dir.path().join("data.csv"), "a,b").unwrap();
|
|
34
|
+
fs::write(dir.path().join("data.tmp"), "junk").unwrap();
|
|
35
|
+
|
|
36
|
+
let matcher =
|
|
37
|
+
TableMatcher::new(&[("**/*.csv", "t"), ("**/*.tmp", "t2")], &["**/*.tmp"]).unwrap();
|
|
38
|
+
let results = scan_directory(dir.path(), &matcher);
|
|
39
|
+
|
|
40
|
+
assert_eq!(results.len(), 1);
|
|
41
|
+
assert!(results[0].0.ends_with("data.csv"));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
#[test]
|
|
45
|
+
fn scan_recurses_into_subdirectories() {
|
|
46
|
+
let dir = TempDir::new().unwrap();
|
|
47
|
+
let sub = dir.path().join("nested").join("deep");
|
|
48
|
+
fs::create_dir_all(&sub).unwrap();
|
|
49
|
+
fs::write(sub.join("events.jsonl"), "{}").unwrap();
|
|
50
|
+
|
|
51
|
+
let matcher = TableMatcher::new(&[("**/*.jsonl", "events")], &[]).unwrap();
|
|
52
|
+
let results = scan_directory(dir.path(), &matcher);
|
|
53
|
+
|
|
54
|
+
assert_eq!(results.len(), 1);
|
|
55
|
+
assert!(results[0].0.ends_with("events.jsonl"));
|
|
56
|
+
assert_eq!(results[0].1, "events");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
#[test]
|
|
60
|
+
fn scan_returns_empty_for_no_matches() {
|
|
61
|
+
let dir = TempDir::new().unwrap();
|
|
62
|
+
fs::write(dir.path().join("readme.md"), "# hi").unwrap();
|
|
63
|
+
|
|
64
|
+
let matcher = TableMatcher::new(&[("**/*.csv", "t")], &[]).unwrap();
|
|
65
|
+
let results = scan_directory(dir.path(), &matcher);
|
|
66
|
+
|
|
67
|
+
assert!(results.is_empty());
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
#[test]
|
|
71
|
+
fn scan_skips_directories() {
|
|
72
|
+
let dir = TempDir::new().unwrap();
|
|
73
|
+
// Create a directory that matches the glob -- it should not appear in results
|
|
74
|
+
fs::create_dir(dir.path().join("data.csv")).unwrap();
|
|
75
|
+
|
|
76
|
+
let matcher = TableMatcher::new(&[("**/*.csv", "t")], &[]).unwrap();
|
|
77
|
+
let results = scan_directory(dir.path(), &matcher);
|
|
78
|
+
|
|
79
|
+
assert!(results.is_empty());
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
#[test]
|
|
83
|
+
fn scan_excludes_top_level_dirsql_directory() {
|
|
84
|
+
let dir = TempDir::new().unwrap();
|
|
85
|
+
fs::write(dir.path().join("real.csv"), "a,b\n1,2").unwrap();
|
|
86
|
+
|
|
87
|
+
// Files inside the reserved `.dirsql/` directory (e.g. the cache db)
|
|
88
|
+
// must never be picked up by the scanner.
|
|
89
|
+
fs::create_dir(dir.path().join(".dirsql")).unwrap();
|
|
90
|
+
fs::write(dir.path().join(".dirsql").join("cache.csv"), "a,b\n1,2").unwrap();
|
|
91
|
+
|
|
92
|
+
let matcher = TableMatcher::new(&[("**/*.csv", "t")], &[]).unwrap();
|
|
93
|
+
let results = scan_directory(dir.path(), &matcher);
|
|
94
|
+
|
|
95
|
+
assert_eq!(results.len(), 1);
|
|
96
|
+
assert!(results[0].0.ends_with("real.csv"));
|
|
97
|
+
}
|
|
@@ -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
|
+
}
|