dirsql 0.3.29__tar.gz → 0.3.30__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.29 → dirsql-0.3.30}/Cargo.lock +1 -1
- {dirsql-0.3.29 → dirsql-0.3.30}/PKG-INFO +1 -1
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/python/Cargo.toml +1 -1
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/rust/src/lib.rs +192 -23
- {dirsql-0.3.29 → dirsql-0.3.30}/Cargo.toml +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/README.md +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/dirsql/__init__.py +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/dirsql/_async.py +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/dirsql/_async_test.py +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/dirsql/_dirsql.pyi +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/dirsql/cli/__init__.py +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/dirsql/cli/binary_path.py +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/dirsql/cli/binary_path_test.py +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/dirsql/cli/interpret/__init__.py +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/dirsql/cli/interpret/dispatch_extract.py +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/dirsql/cli/interpret/dispatch_extract_test.py +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/dirsql/cli/interpret/load_app.py +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/dirsql/cli/interpret/load_app_test.py +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/dirsql/cli/interpret/run.py +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/dirsql/cli/interpret/run_test.py +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/dirsql/cli/interpret/write_message.py +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/dirsql/cli/interpret/write_message_test.py +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/dirsql/cli/is_windows.py +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/dirsql/cli/is_windows_test.py +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/dirsql/cli/main.py +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/dirsql/cli/main_test.py +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/dirsql/py.typed +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/dirsql/resolve_config.py +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/dirsql/resolve_config_test.py +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/docs/.claude/CLAUDE.md +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/docs/.vitepress/config.ts +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/docs/.vitepress/theme/index.ts +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/docs/.vitepress/theme/lang.ts +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/docs/AGENTS.md +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/docs/api/index.md +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/docs/cli/config.md +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/docs/cli/http-api.md +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/docs/cli/index.md +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/docs/cli/init.md +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/docs/cli/server.md +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/docs/getting-started.md +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/docs/guide/async.md +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/docs/guide/crdt.md +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/docs/guide/persistence.md +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/docs/guide/querying.md +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/docs/guide/tables.md +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/docs/guide/watching.md +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/docs/index.md +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/docs/migrations.md +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/docs/package.json +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/docs/playwright.config.ts +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/docs/pnpm-lock.yaml +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/docs/pnpm-workspace.yaml +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/docs/tests/integration/home.spec.ts +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/docs/tests/integration/language-flag.spec.ts +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/docs/tests/unit/config.test.ts +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/docs/tests/unit/lang.test.ts +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/docs/vitest.config.ts +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/python/README.md +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/python/conftest.py +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/python/docs/.claude/CLAUDE.md +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/python/docs/.vitepress/config.ts +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/python/docs/.vitepress/theme/index.ts +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/python/docs/.vitepress/theme/lang.ts +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/python/docs/AGENTS.md +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/python/docs/api/index.md +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/python/docs/cli/config.md +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/python/docs/cli/http-api.md +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/python/docs/cli/index.md +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/python/docs/cli/init.md +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/python/docs/cli/server.md +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/python/docs/getting-started.md +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/python/docs/guide/async.md +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/python/docs/guide/crdt.md +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/python/docs/guide/persistence.md +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/python/docs/guide/querying.md +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/python/docs/guide/tables.md +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/python/docs/guide/watching.md +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/python/docs/index.md +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/python/docs/migrations.md +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/python/docs/package.json +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/python/docs/playwright.config.ts +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/python/docs/pnpm-lock.yaml +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/python/docs/pnpm-workspace.yaml +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/python/docs/tests/integration/home.spec.ts +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/python/docs/tests/integration/language-flag.spec.ts +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/python/docs/tests/unit/config.test.ts +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/python/docs/tests/unit/lang.test.ts +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/python/docs/vitest.config.ts +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/python/src/lib.rs +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/python/tests/__init__.py +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/python/tests/conftest.py +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/python/tests/e2e/__init__.py +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/python/tests/integration/__fixtures__/data/a/meta.json +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/python/tests/integration/__fixtures__/data/b/meta.json +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/python/tests/integration/__fixtures__/dirsql.config.py +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/python/tests/integration/__fixtures__/interpret/data/a/meta.json +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/python/tests/integration/__fixtures__/interpret/data/b/meta.json +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/python/tests/integration/__fixtures__/interpret/dirsql.config.py +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/python/tests/integration/__fixtures__/interpret/dirsql.config_no_app.py +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/python/tests/integration/__fixtures__/interpret/dirsql.config_raises.py +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/python/tests/integration/__init__.py +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/python/tests/integration/interpret_subprocess.py +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/python/tests/integration/test_async_dirsql.py +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/python/tests/integration/test_binding.py +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/python/tests/integration/test_dirsql.py +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/python/tests/integration/test_docs_examples.py +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/python/tests/integration/test_docs_gaps.py +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/python/tests/integration/test_from_config.py +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/python/tests/integration/test_interpret.py +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/python/tests/integration/test_native_config.py +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/python/tests/integration/test_persist.py +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/python/tests/integration/test_serialization.py +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/rust/Cargo.toml +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/rust/README.md +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/rust/benches/db_bench.rs +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/rust/benches/differ_bench.rs +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/rust/benches/matcher_bench.rs +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/rust/benches/scanner_bench.rs +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/rust/docs/api/index.md +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/rust/docs/cli/config.md +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/rust/docs/cli/http-api.md +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/rust/docs/cli/index.md +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/rust/docs/cli/init.md +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/rust/docs/cli/server.md +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/rust/docs/getting-started.md +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/rust/docs/guide/async.md +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/rust/docs/guide/crdt.md +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/rust/docs/guide/persistence.md +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/rust/docs/guide/querying.md +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/rust/docs/guide/tables.md +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/rust/docs/guide/watching.md +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/rust/docs/index.md +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/rust/docs/migrations.md +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/rust/src/bin/dirsql.rs +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/rust/src/cli/init.rs +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/rust/src/cli/mod.rs +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/rust/src/cli/native_config.rs +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/rust/src/cli/router.rs +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/rust/src/cli/serialize.rs +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/rust/src/cli/server.rs +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/rust/src/config.rs +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/rust/src/db.rs +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/rust/src/differ.rs +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/rust/src/matcher.rs +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/rust/src/persist.rs +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/rust/src/scanner.rs +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/rust/src/watcher.rs +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/rust/tests/async_sdk.rs +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/rust/tests/cli_e2e.rs +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/rust/tests/cli_integration.rs +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/rust/tests/code_review_findings.rs +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/rust/tests/config.rs +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/rust/tests/docs_examples.rs +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/rust/tests/docs_gaps.rs +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/rust/tests/extensions.rs +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/rust/tests/from_config.rs +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/rust/tests/init_e2e.rs +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/rust/tests/init_integration.rs +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/rust/tests/persist.rs +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/rust/tests/readonly_query.rs +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/rust/tests/scanner.rs +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/rust/tests/sdk.rs +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/rust/tests/serialization.rs +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/rust/tests/watch_relative_root.rs +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/packages/rust/tests/watcher.rs +0 -0
- {dirsql-0.3.29 → dirsql-0.3.30}/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.30"
|
|
8
8
|
edition.workspace = true
|
|
9
9
|
publish = false
|
|
10
10
|
readme = "README.md"
|
|
@@ -273,6 +273,10 @@ struct DirSqlInner {
|
|
|
273
273
|
/// loop. Bounds event-to-stream latency from above (and idle CPU from
|
|
274
274
|
/// below). Defaults to 200ms — see [`DirSQLBuilder::poll_interval`].
|
|
275
275
|
poll_interval: Duration,
|
|
276
|
+
/// Filesystem seam used by the watch-upsert path. Always [`RealFs`] in
|
|
277
|
+
/// production; unit tests inject a deterministic double via the
|
|
278
|
+
/// `with_ignore_and_fs` test-seam constructor.
|
|
279
|
+
fs: Arc<dyn FileSystem>,
|
|
276
280
|
}
|
|
277
281
|
|
|
278
282
|
/// Serializable snapshot of a `DirSQL` instance's resolved runtime state.
|
|
@@ -580,7 +584,7 @@ impl DirSQL {
|
|
|
580
584
|
|
|
581
585
|
fn handle_upsert(&self, table: &str, abs_path: &Path, rel_path: &str) -> Vec<RowEvent> {
|
|
582
586
|
// The file may have vanished between the watcher event and now.
|
|
583
|
-
match
|
|
587
|
+
match self.inner.fs.stat(abs_path) {
|
|
584
588
|
Ok(_) => {}
|
|
585
589
|
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Vec::new(),
|
|
586
590
|
Err(e) => return vec![error_event(Some(table), rel_path, e.to_string())],
|
|
@@ -659,6 +663,36 @@ impl DirSQL {
|
|
|
659
663
|
Self::finish_build(prepared)
|
|
660
664
|
}
|
|
661
665
|
|
|
666
|
+
/// Test-seam build path: identical to [`build_from_resolved`] but stores
|
|
667
|
+
/// the supplied [`FileSystem`] double on the resulting instance so the
|
|
668
|
+
/// watch-upsert path's filesystem read can be faked deterministically.
|
|
669
|
+
/// The prepare phase still uses [`RealFs`] (it has no instance yet), but
|
|
670
|
+
/// the unit tests that exercise this seam build over an empty temp dir, so
|
|
671
|
+
/// the scan touches nothing.
|
|
672
|
+
#[cfg(test)]
|
|
673
|
+
pub(crate) fn with_ignore_and_fs<I, S>(
|
|
674
|
+
root: impl Into<PathBuf>,
|
|
675
|
+
tables: Vec<Table>,
|
|
676
|
+
ignore: I,
|
|
677
|
+
fs: Arc<dyn FileSystem>,
|
|
678
|
+
) -> Result<Self>
|
|
679
|
+
where
|
|
680
|
+
I: IntoIterator<Item = S>,
|
|
681
|
+
S: Into<String>,
|
|
682
|
+
{
|
|
683
|
+
let resolved = ResolvedBuild {
|
|
684
|
+
root: root.into(),
|
|
685
|
+
tables,
|
|
686
|
+
ignore: ignore.into_iter().map(Into::into).collect(),
|
|
687
|
+
extensions: Vec::new(),
|
|
688
|
+
persist: false,
|
|
689
|
+
persist_path: None,
|
|
690
|
+
poll_interval: DEFAULT_POLL_INTERVAL,
|
|
691
|
+
};
|
|
692
|
+
let prepared = Self::prepare_resolved(resolved)?;
|
|
693
|
+
Self::finish_build_with_fs(prepared, fs)
|
|
694
|
+
}
|
|
695
|
+
|
|
662
696
|
/// Split-phase construction — part 1. Performs all I/O that is safe to run
|
|
663
697
|
/// off the host's main thread: validates DDL, compiles the matcher, walks
|
|
664
698
|
/// the directory, opens the persistent cache (when enabled) and decides
|
|
@@ -713,7 +747,7 @@ impl DirSQL {
|
|
|
713
747
|
}
|
|
714
748
|
(files, Vec::new(), Vec::new())
|
|
715
749
|
}
|
|
716
|
-
Some(ctx) => reconcile_scan(&root, scanned, ctx)?,
|
|
750
|
+
Some(ctx) => reconcile_scan(&root, scanned, ctx, &RealFs)?,
|
|
717
751
|
};
|
|
718
752
|
|
|
719
753
|
let _ = table_names;
|
|
@@ -748,6 +782,17 @@ impl DirSQL {
|
|
|
748
782
|
/// run. For the napi-rs binding that is the JS main thread.
|
|
749
783
|
#[doc(hidden)]
|
|
750
784
|
pub fn finish_build(prepared: PreparedBuild) -> Result<Self> {
|
|
785
|
+
Self::finish_build_with_fs(prepared, Arc::new(RealFs))
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/// Test-seam variant of [`finish_build`] that takes the [`FileSystem`]
|
|
789
|
+
/// double to store on the instance. Production always passes
|
|
790
|
+
/// `Arc::new(RealFs)` (via [`finish_build`]); unit tests inject a fake so
|
|
791
|
+
/// the watch-upsert path's `stat` read is deterministic.
|
|
792
|
+
pub(crate) fn finish_build_with_fs(
|
|
793
|
+
prepared: PreparedBuild,
|
|
794
|
+
fs: Arc<dyn FileSystem>,
|
|
795
|
+
) -> Result<Self> {
|
|
751
796
|
let PreparedBuild {
|
|
752
797
|
root,
|
|
753
798
|
tables,
|
|
@@ -914,6 +959,7 @@ impl DirSQL {
|
|
|
914
959
|
poll_used: AtomicBool::new(false),
|
|
915
960
|
watch_thread_started: AtomicBool::new(false),
|
|
916
961
|
poll_interval,
|
|
962
|
+
fs,
|
|
917
963
|
}),
|
|
918
964
|
})
|
|
919
965
|
}
|
|
@@ -1316,6 +1362,33 @@ fn prepare_persist(
|
|
|
1316
1362
|
})
|
|
1317
1363
|
}
|
|
1318
1364
|
|
|
1365
|
+
/// Internal filesystem seam. Every effectful filesystem read performed by the
|
|
1366
|
+
/// persist/reconcile and watch-upsert paths goes through this trait so unit
|
|
1367
|
+
/// tests can inject a deterministic double (avoiding real `std::fs` calls and
|
|
1368
|
+
/// the racy timing windows they imply). Production always uses [`RealFs`],
|
|
1369
|
+
/// which replicates the previous inline `std::fs`/`hash_file` calls exactly --
|
|
1370
|
+
/// this is purely a test seam, not a behavioral change.
|
|
1371
|
+
trait FileSystem: Send + Sync {
|
|
1372
|
+
/// Stat a path. Mirrors `std::fs::metadata(path).map(|m| FileStat::from_metadata(&m))`.
|
|
1373
|
+
fn stat(&self, path: &Path) -> std::io::Result<FileStat>;
|
|
1374
|
+
/// BLAKE3-hash a file's contents. Mirrors [`hash_file`].
|
|
1375
|
+
fn hash(&self, path: &Path) -> std::io::Result<[u8; 32]>;
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
/// Production [`FileSystem`]: delegates to the real `std::fs` / [`hash_file`]
|
|
1379
|
+
/// calls that the persist and watch paths used inline before the seam existed.
|
|
1380
|
+
struct RealFs;
|
|
1381
|
+
|
|
1382
|
+
impl FileSystem for RealFs {
|
|
1383
|
+
fn stat(&self, path: &Path) -> std::io::Result<FileStat> {
|
|
1384
|
+
std::fs::metadata(path).map(|m| FileStat::from_metadata(&m))
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
fn hash(&self, path: &Path) -> std::io::Result<[u8; 32]> {
|
|
1388
|
+
hash_file(path)
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1319
1392
|
/// Decide which files are trusted, which need re-parsing, and which were
|
|
1320
1393
|
/// removed since the last cache write.
|
|
1321
1394
|
#[allow(clippy::type_complexity)]
|
|
@@ -1323,6 +1396,7 @@ fn reconcile_scan(
|
|
|
1323
1396
|
root: &Path,
|
|
1324
1397
|
scanned: Vec<(PathBuf, String)>,
|
|
1325
1398
|
ctx: &PersistContext,
|
|
1399
|
+
fs: &dyn FileSystem,
|
|
1326
1400
|
) -> Result<(Vec<ScannedFile>, Vec<TrustedFile>, Vec<(String, String)>)> {
|
|
1327
1401
|
let mut to_parse = Vec::new();
|
|
1328
1402
|
let mut trusted = Vec::new();
|
|
@@ -1333,8 +1407,7 @@ fn reconcile_scan(
|
|
|
1333
1407
|
let rel_path = relative_path(root, &path);
|
|
1334
1408
|
seen_paths.insert(rel_path.clone());
|
|
1335
1409
|
|
|
1336
|
-
let
|
|
1337
|
-
let stat = FileStat::from_metadata(&metadata);
|
|
1410
|
+
let stat = fs.stat(&path)?;
|
|
1338
1411
|
|
|
1339
1412
|
let cached = ctx.cached.get(&rel_path);
|
|
1340
1413
|
let trust = match cached {
|
|
@@ -1344,7 +1417,7 @@ fn reconcile_scan(
|
|
|
1344
1417
|
true
|
|
1345
1418
|
} else {
|
|
1346
1419
|
// Hash-confirm.
|
|
1347
|
-
match (
|
|
1420
|
+
match (fs.hash(&path).ok(), c.content_hash) {
|
|
1348
1421
|
(Some(live), Some(cached_hash)) => live == cached_hash,
|
|
1349
1422
|
_ => false,
|
|
1350
1423
|
}
|
|
@@ -1808,8 +1881,60 @@ mod readonly_tests {
|
|
|
1808
1881
|
#[cfg(test)]
|
|
1809
1882
|
mod internal_tests {
|
|
1810
1883
|
use super::*;
|
|
1884
|
+
use std::collections::HashMap as StdHashMap;
|
|
1811
1885
|
use tempfile::TempDir;
|
|
1812
1886
|
|
|
1887
|
+
/// Deterministic [`FileSystem`] double for unit tests. Backed by a map of
|
|
1888
|
+
/// canned [`FileStat`]s (and an optional canned hash); any path not present
|
|
1889
|
+
/// stats/hashes as an `io::Error` of kind `NotFound`. Lets the tests of the
|
|
1890
|
+
/// persist/reconcile and watch-upsert paths exercise the metadata-read and
|
|
1891
|
+
/// racy-window branches without touching the real filesystem (and without
|
|
1892
|
+
/// depending on real mtime timing).
|
|
1893
|
+
#[derive(Default)]
|
|
1894
|
+
struct FakeFs {
|
|
1895
|
+
stats: StdHashMap<PathBuf, FileStat>,
|
|
1896
|
+
hashes: StdHashMap<PathBuf, [u8; 32]>,
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
impl FakeFs {
|
|
1900
|
+
fn with_stat(path: impl Into<PathBuf>, stat: FileStat) -> Self {
|
|
1901
|
+
let mut fs = FakeFs::default();
|
|
1902
|
+
fs.stats.insert(path.into(), stat);
|
|
1903
|
+
fs
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
fn set_hash(&mut self, path: impl Into<PathBuf>, hash: [u8; 32]) {
|
|
1907
|
+
self.hashes.insert(path.into(), hash);
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
impl FileSystem for FakeFs {
|
|
1912
|
+
fn stat(&self, path: &Path) -> std::io::Result<FileStat> {
|
|
1913
|
+
self.stats.get(path).cloned().ok_or_else(|| {
|
|
1914
|
+
std::io::Error::new(std::io::ErrorKind::NotFound, "fake: no such file")
|
|
1915
|
+
})
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
fn hash(&self, path: &Path) -> std::io::Result<[u8; 32]> {
|
|
1919
|
+
self.hashes.get(path).copied().ok_or_else(|| {
|
|
1920
|
+
std::io::Error::new(std::io::ErrorKind::NotFound, "fake: no such file")
|
|
1921
|
+
})
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
/// A canned [`FileStat`] for unit tests that don't care about specific
|
|
1926
|
+
/// values, only that a stat succeeds. `snapshot_ns`-comparable via
|
|
1927
|
+
/// `mtime_ns`.
|
|
1928
|
+
fn fake_stat() -> FileStat {
|
|
1929
|
+
FileStat {
|
|
1930
|
+
size: 5,
|
|
1931
|
+
mtime_ns: 1_000,
|
|
1932
|
+
ctime_ns: 1_000,
|
|
1933
|
+
inode: 1,
|
|
1934
|
+
dev: 1,
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1813
1938
|
/// A relative path with a basename, parent dir, and extension exercises the
|
|
1814
1939
|
/// `Some` arms of `stat_virtuals`' path inspection, plus the `Some` arms of
|
|
1815
1940
|
/// the size/mtime/ctime inserts. The metadata read that supplies those
|
|
@@ -1899,7 +2024,11 @@ mod internal_tests {
|
|
|
1899
2024
|
#[test]
|
|
1900
2025
|
fn process_file_event_skips_ignored_paths() {
|
|
1901
2026
|
let dir = TempDir::new().unwrap();
|
|
1902
|
-
let
|
|
2027
|
+
let kept = dir.path().join("keep.txt");
|
|
2028
|
+
// Inject a fake fs so the non-ignored path's stat read succeeds without
|
|
2029
|
+
// staging a real file. The ignored path is dropped before any stat.
|
|
2030
|
+
let fake = FakeFs::with_stat(kept.clone(), fake_stat());
|
|
2031
|
+
let db = DirSQL::with_ignore_and_fs(
|
|
1903
2032
|
dir.path(),
|
|
1904
2033
|
vec![Table::new(
|
|
1905
2034
|
"CREATE TABLE items (name TEXT)",
|
|
@@ -1912,6 +2041,7 @@ mod internal_tests {
|
|
|
1912
2041
|
},
|
|
1913
2042
|
)],
|
|
1914
2043
|
vec!["skip/**"],
|
|
2044
|
+
Arc::new(fake),
|
|
1915
2045
|
)
|
|
1916
2046
|
.unwrap();
|
|
1917
2047
|
|
|
@@ -1921,8 +2051,6 @@ mod internal_tests {
|
|
|
1921
2051
|
assert!(events.is_empty(), "ignored path must produce no events");
|
|
1922
2052
|
|
|
1923
2053
|
// Non-ignored path: the extract closure runs and yields one insert.
|
|
1924
|
-
let kept = dir.path().join("keep.txt");
|
|
1925
|
-
std::fs::write(&kept, b"").unwrap();
|
|
1926
2054
|
let events = db.process_file_event(FileEvent::Created(kept));
|
|
1927
2055
|
assert_eq!(events.len(), 1, "non-ignored path must produce one event");
|
|
1928
2056
|
}
|
|
@@ -2099,10 +2227,12 @@ mod internal_tests {
|
|
|
2099
2227
|
fn reconcile_scan_hash_confirms_in_racy_window() {
|
|
2100
2228
|
let dir = TempDir::new().unwrap();
|
|
2101
2229
|
let abs = dir.path().join("a.txt");
|
|
2102
|
-
|
|
2103
|
-
let
|
|
2104
|
-
|
|
2105
|
-
|
|
2230
|
+
let stat = fake_stat();
|
|
2231
|
+
let live_hash = [7u8; 32];
|
|
2232
|
+
// Fake fs: canned stat + matching hash, so the racy-window hash-confirm
|
|
2233
|
+
// branch sees a live hash equal to the cached one and trusts the file.
|
|
2234
|
+
let mut fake = FakeFs::with_stat(abs.clone(), stat.clone());
|
|
2235
|
+
fake.set_hash(abs.clone(), live_hash);
|
|
2106
2236
|
|
|
2107
2237
|
let mut cached = HashMap::new();
|
|
2108
2238
|
cached.insert(
|
|
@@ -2123,7 +2253,8 @@ mod internal_tests {
|
|
|
2123
2253
|
cold_rebuild: false,
|
|
2124
2254
|
};
|
|
2125
2255
|
let scanned = vec![(abs.clone(), "t".to_string())];
|
|
2126
|
-
let (to_parse, trusted, deleted) =
|
|
2256
|
+
let (to_parse, trusted, deleted) =
|
|
2257
|
+
reconcile_scan(dir.path(), scanned, &ctx, &fake).unwrap();
|
|
2127
2258
|
assert!(to_parse.is_empty());
|
|
2128
2259
|
assert_eq!(trusted.len(), 1);
|
|
2129
2260
|
assert_eq!(trusted[0].rel_path, "a.txt");
|
|
@@ -2136,9 +2267,12 @@ mod internal_tests {
|
|
|
2136
2267
|
fn reconcile_scan_racy_window_without_hash_reparses() {
|
|
2137
2268
|
let dir = TempDir::new().unwrap();
|
|
2138
2269
|
let abs = dir.path().join("b.txt");
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2270
|
+
let stat = fake_stat();
|
|
2271
|
+
// The live hash is available (file present) but the cache stored no
|
|
2272
|
+
// content hash, so the `(Some(live), None)` pair falls through the
|
|
2273
|
+
// `_ => false` arm: the file is NOT trusted and is re-parsed.
|
|
2274
|
+
let mut fake = FakeFs::with_stat(abs.clone(), stat.clone());
|
|
2275
|
+
fake.set_hash(abs.clone(), [9u8; 32]);
|
|
2142
2276
|
|
|
2143
2277
|
let mut cached = HashMap::new();
|
|
2144
2278
|
cached.insert(
|
|
@@ -2158,7 +2292,8 @@ mod internal_tests {
|
|
|
2158
2292
|
cold_rebuild: false,
|
|
2159
2293
|
};
|
|
2160
2294
|
let scanned = vec![(abs.clone(), "t".to_string())];
|
|
2161
|
-
let (to_parse, trusted, _deleted) =
|
|
2295
|
+
let (to_parse, trusted, _deleted) =
|
|
2296
|
+
reconcile_scan(dir.path(), scanned, &ctx, &fake).unwrap();
|
|
2162
2297
|
assert_eq!(to_parse.len(), 1);
|
|
2163
2298
|
assert!(trusted.is_empty());
|
|
2164
2299
|
}
|
|
@@ -2176,7 +2311,32 @@ mod internal_tests {
|
|
|
2176
2311
|
};
|
|
2177
2312
|
let missing = dir.path().join("ghost.txt");
|
|
2178
2313
|
let scanned = vec![(missing, "t".to_string())];
|
|
2179
|
-
|
|
2314
|
+
// An empty fake fs stats every path as NotFound; the `?` in
|
|
2315
|
+
// `reconcile_scan` propagates that error.
|
|
2316
|
+
let fake = FakeFs::default();
|
|
2317
|
+
assert!(reconcile_scan(dir.path(), scanned, &ctx, &fake).is_err());
|
|
2318
|
+
}
|
|
2319
|
+
|
|
2320
|
+
/// Exercise the production [`RealFs`] [`FileSystem`] impl directly so its
|
|
2321
|
+
/// `stat`/`hash` method bodies are covered without the integration suite
|
|
2322
|
+
/// having to deterministically land in `reconcile_scan`'s racy window.
|
|
2323
|
+
/// Both methods run against a path that does not exist (inside a temp dir,
|
|
2324
|
+
/// so no direct `std::fs` call lives in the test): each delegates to its
|
|
2325
|
+
/// real backing (`std::fs::metadata` / `hash_file`) and surfaces the
|
|
2326
|
+
/// resulting `NotFound` error, executing the body either way.
|
|
2327
|
+
#[test]
|
|
2328
|
+
fn real_fs_delegates_stat_and_hash() {
|
|
2329
|
+
let dir = TempDir::new().unwrap();
|
|
2330
|
+
let missing = dir.path().join("nope.txt");
|
|
2331
|
+
let fs = RealFs;
|
|
2332
|
+
assert!(
|
|
2333
|
+
fs.stat(&missing).is_err(),
|
|
2334
|
+
"stat of a missing path must error"
|
|
2335
|
+
);
|
|
2336
|
+
assert!(
|
|
2337
|
+
fs.hash(&missing).is_err(),
|
|
2338
|
+
"hash of a missing path must error"
|
|
2339
|
+
);
|
|
2180
2340
|
}
|
|
2181
2341
|
|
|
2182
2342
|
// -----------------------------------------------------------------------
|
|
@@ -2220,8 +2380,12 @@ mod internal_tests {
|
|
|
2220
2380
|
fn upsert_fixture() -> (TempDir, DirSQL, PathBuf, String) {
|
|
2221
2381
|
let dir = TempDir::new().unwrap();
|
|
2222
2382
|
let abs = dir.path().join("a.txt");
|
|
2223
|
-
|
|
2224
|
-
|
|
2383
|
+
// Inject a fake fs that stats the fixture path successfully (so
|
|
2384
|
+
// `handle_upsert`'s vanished-file guard passes) without staging a real
|
|
2385
|
+
// file. Any other path stats as NotFound, which the vanished-file test
|
|
2386
|
+
// relies on.
|
|
2387
|
+
let fake = FakeFs::with_stat(abs.clone(), fake_stat());
|
|
2388
|
+
let db = DirSQL::with_ignore_and_fs(
|
|
2225
2389
|
dir.path(),
|
|
2226
2390
|
vec![Table::new(
|
|
2227
2391
|
"CREATE TABLE items (name TEXT)",
|
|
@@ -2234,6 +2398,7 @@ mod internal_tests {
|
|
|
2234
2398
|
},
|
|
2235
2399
|
)],
|
|
2236
2400
|
Vec::<String>::new(),
|
|
2401
|
+
Arc::new(fake),
|
|
2237
2402
|
)
|
|
2238
2403
|
.unwrap();
|
|
2239
2404
|
(dir, db, abs, "a.txt".to_string())
|
|
@@ -2360,7 +2525,12 @@ mod internal_tests {
|
|
|
2360
2525
|
// the file is created afterwards and reaches the DB only through
|
|
2361
2526
|
// `handle_upsert`, isolating the arm under test.
|
|
2362
2527
|
let dir = TempDir::new().unwrap();
|
|
2363
|
-
let
|
|
2528
|
+
let abs = dir.path().join("a.txt");
|
|
2529
|
+
// Inject a fake fs so the strict table's `handle_upsert` stat read
|
|
2530
|
+
// succeeds without staging a real file; the normalize-error arm is the
|
|
2531
|
+
// arm under test.
|
|
2532
|
+
let fake = FakeFs::with_stat(abs.clone(), fake_stat());
|
|
2533
|
+
let db = DirSQL::with_ignore_and_fs(
|
|
2364
2534
|
dir.path(),
|
|
2365
2535
|
vec![Table::strict(
|
|
2366
2536
|
"CREATE TABLE items (name TEXT)",
|
|
@@ -2373,11 +2543,10 @@ mod internal_tests {
|
|
|
2373
2543
|
},
|
|
2374
2544
|
)],
|
|
2375
2545
|
Vec::<String>::new(),
|
|
2546
|
+
Arc::new(fake),
|
|
2376
2547
|
)
|
|
2377
2548
|
.unwrap();
|
|
2378
2549
|
|
|
2379
|
-
let abs = dir.path().join("a.txt");
|
|
2380
|
-
std::fs::write(&abs, b"hello").unwrap();
|
|
2381
2550
|
let events = db.handle_upsert("items", &abs, "a.txt");
|
|
2382
2551
|
assert_eq!(events.len(), 1, "expected one error event: {events:?}");
|
|
2383
2552
|
let dbg = format!("{:?}", events[0]);
|
|
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.29 → dirsql-0.3.30}/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.29 → dirsql-0.3.30}/packages/python/tests/integration/__fixtures__/data/a/meta.json
RENAMED
|
File without changes
|
{dirsql-0.3.29 → dirsql-0.3.30}/packages/python/tests/integration/__fixtures__/data/b/meta.json
RENAMED
|
File without changes
|
{dirsql-0.3.29 → dirsql-0.3.30}/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
|
|
File without changes
|