dirsql 0.3.80__tar.gz → 0.3.82__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.80 → dirsql-0.3.82}/Cargo.lock +1 -1
- {dirsql-0.3.80 → dirsql-0.3.82}/PKG-INFO +1 -1
- dirsql-0.3.82/packages/python/Cargo.toml +30 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/python/e2e-attestation.json +2 -2
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/rust/src/db.rs +110 -12
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/rust/src/lib.rs +74 -4
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/rust/src/watcher.rs +29 -0
- dirsql-0.3.80/packages/python/Cargo.toml +0 -22
- {dirsql-0.3.80 → dirsql-0.3.82}/Cargo.toml +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/README.md +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/dirsql/__init__.py +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/dirsql/_async.py +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/dirsql/_dirsql.pyi +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/dirsql/cli/__init__.py +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/dirsql/cli/binary_path.py +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/dirsql/cli/is_windows.py +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/dirsql/cli/main.py +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/dirsql/cli/resolve_config_extensions.py +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/dirsql/py.typed +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/dirsql/resolve_config_extensions.py +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/dirsql/resolve_extension.py +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/docs/.claude/CLAUDE.md +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/docs/.vitepress/config.ts +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/docs/.vitepress/theme/index.ts +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/docs/.vitepress/theme/lang.ts +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/docs/AGENTS.md +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/docs/explanation.md +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/docs/getting-started.md +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/docs/howto/columns-from-paths.md +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/docs/howto/define-tables.md +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/docs/howto/embed.md +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/docs/howto/extract-from-contents.md +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/docs/howto/load-extension.md +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/docs/howto/persist.md +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/docs/howto/react-to-changes.md +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/docs/howto/search-by-meaning.md +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/docs/howto/skip-files.md +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/docs/index.md +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/docs/migrations.md +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/docs/package.json +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/docs/playwright.config.ts +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/docs/pnpm-lock.yaml +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/docs/pnpm-workspace.yaml +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/docs/reference/cli.md +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/docs/reference/columns.md +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/docs/reference/config.md +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/docs/reference/hooks.md +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/docs/reference/http-api.md +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/docs/reference/sdk.md +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/docs/tests/integration/home.spec.ts +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/docs/tests/integration/language-flag.spec.ts +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/docs/tests/integration/sidebar.spec.ts +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/docs/tests/unit/config.test.ts +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/docs/tests/unit/lang.test.ts +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/docs/vitest.config.ts +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/python/README.md +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/python/conftest.py +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/python/docs/.claude/CLAUDE.md +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/python/docs/.vitepress/config.ts +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/python/docs/.vitepress/theme/index.ts +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/python/docs/.vitepress/theme/lang.ts +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/python/docs/AGENTS.md +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/python/docs/explanation.md +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/python/docs/getting-started.md +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/python/docs/howto/columns-from-paths.md +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/python/docs/howto/define-tables.md +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/python/docs/howto/embed.md +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/python/docs/howto/extract-from-contents.md +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/python/docs/howto/load-extension.md +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/python/docs/howto/persist.md +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/python/docs/howto/react-to-changes.md +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/python/docs/howto/search-by-meaning.md +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/python/docs/howto/skip-files.md +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/python/docs/index.md +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/python/docs/migrations.md +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/python/docs/package.json +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/python/docs/playwright.config.ts +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/python/docs/pnpm-lock.yaml +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/python/docs/pnpm-workspace.yaml +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/python/docs/reference/cli.md +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/python/docs/reference/columns.md +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/python/docs/reference/config.md +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/python/docs/reference/hooks.md +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/python/docs/reference/http-api.md +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/python/docs/reference/sdk.md +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/python/docs/tests/integration/home.spec.ts +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/python/docs/tests/integration/language-flag.spec.ts +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/python/docs/tests/integration/sidebar.spec.ts +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/python/docs/tests/unit/config.test.ts +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/python/docs/tests/unit/lang.test.ts +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/python/docs/vitest.config.ts +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/python/src/lib.rs +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/python/tests/__init__.py +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/python/tests/binding/__init__.py +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/python/tests/conftest.py +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/python/tests/e2e/__init__.py +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/python/tests/integration/__init__.py +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/python/tests/smoke/__init__.py +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/rust/Cargo.toml +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/rust/README.md +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/rust/benches/db_bench.rs +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/rust/benches/differ_bench.rs +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/rust/benches/matcher_bench.rs +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/rust/benches/scanner_bench.rs +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/rust/docs/explanation.md +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/rust/docs/getting-started.md +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/rust/docs/howto/columns-from-paths.md +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/rust/docs/howto/define-tables.md +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/rust/docs/howto/embed.md +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/rust/docs/howto/extract-from-contents.md +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/rust/docs/howto/load-extension.md +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/rust/docs/howto/persist.md +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/rust/docs/howto/react-to-changes.md +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/rust/docs/howto/search-by-meaning.md +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/rust/docs/howto/skip-files.md +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/rust/docs/index.md +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/rust/docs/migrations.md +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/rust/docs/reference/cli.md +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/rust/docs/reference/columns.md +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/rust/docs/reference/config.md +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/rust/docs/reference/hooks.md +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/rust/docs/reference/http-api.md +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/rust/docs/reference/sdk.md +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/rust/src/bin/dirsql.rs +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/rust/src/cli/execute.rs +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/rust/src/cli/init.rs +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/rust/src/cli/mod.rs +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/rust/src/cli/router.rs +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/rust/src/cli/serialize.rs +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/rust/src/cli/server.rs +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/rust/src/command.rs +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/rust/src/config.rs +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/rust/src/default_config.toml +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/rust/src/differ.rs +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/rust/src/matcher.rs +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/rust/src/persist.rs +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/packages/rust/src/scanner.rs +0 -0
- {dirsql-0.3.80 → dirsql-0.3.82}/pyproject.toml +0 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "dirsql-py-ext"
|
|
3
|
+
# Explicit literal (not `version.workspace = true`) so putitoutthere's
|
|
4
|
+
# pypi/maturin handler can rewrite it via `write-version` before
|
|
5
|
+
# `maturin build`. `pyproject.toml` declares `dynamic = ["version"]`
|
|
6
|
+
# and maturin reads this field. Mirrors `packages/rust/Cargo.toml`.
|
|
7
|
+
version = "0.3.82"
|
|
8
|
+
# abi3 note: the `abi3-py311` pyo3 feature (below) builds ONE stable-ABI
|
|
9
|
+
# `cp311-abi3` wheel per platform that loads on every CPython >= 3.11
|
|
10
|
+
# (matching `pyproject.toml`'s `requires-python = ">=3.11"`), instead of
|
|
11
|
+
# one wheel per interpreter. putitoutthere's `wheel-abi` detector (#401)
|
|
12
|
+
# reads the abi3 feature straight off this direct `pyo3` dependency and
|
|
13
|
+
# collapses the per-version build fan to a single wheel -- no
|
|
14
|
+
# `putitoutthere.toml` change needed. The floor must stay <= the
|
|
15
|
+
# `requires-python` floor or pip finds no compatible wheel on the low end.
|
|
16
|
+
edition.workspace = true
|
|
17
|
+
publish = false
|
|
18
|
+
readme = "README.md"
|
|
19
|
+
|
|
20
|
+
[lib]
|
|
21
|
+
name = "dirsql_py_ext"
|
|
22
|
+
crate-type = ["cdylib", "rlib"]
|
|
23
|
+
|
|
24
|
+
[dependencies]
|
|
25
|
+
dirsql.workspace = true
|
|
26
|
+
pyo3 = { version = "0.28", features = ["extension-module", "generate-import-lib", "abi3-py311"], optional = true }
|
|
27
|
+
|
|
28
|
+
[features]
|
|
29
|
+
default = []
|
|
30
|
+
extension-module = ["pyo3/extension-module"]
|
|
@@ -526,19 +526,64 @@ impl rusqlite::types::ToSql for Value {
|
|
|
526
526
|
/// dirsql constrains table names to safe unquoted identifiers via
|
|
527
527
|
/// [`validate_identifier`], so the handful of forms above are the only ones
|
|
528
528
|
/// that can actually resolve to a usable table.
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
529
|
+
/// Strip leading whitespace and SQL comments (`-- ...` to end-of-line and
|
|
530
|
+
/// `/* ... */` blocks, repeated) from `s`. An unterminated block comment
|
|
531
|
+
/// consumes the rest of the input. Every returned slice starts on a char
|
|
532
|
+
/// boundary, so callers can index it safely.
|
|
533
|
+
fn skip_ws_comments(s: &str) -> &str {
|
|
534
|
+
let mut s = s;
|
|
535
|
+
loop {
|
|
536
|
+
let trimmed = s.trim_start();
|
|
537
|
+
if let Some(after) = trimmed.strip_prefix("--") {
|
|
538
|
+
match after.find('\n') {
|
|
539
|
+
Some(i) => s = &after[i + 1..],
|
|
540
|
+
None => return "",
|
|
541
|
+
}
|
|
542
|
+
} else if let Some(after) = trimmed.strip_prefix("/*") {
|
|
543
|
+
match after.find("*/") {
|
|
544
|
+
Some(i) => s = &after[i + 2..],
|
|
545
|
+
None => return "",
|
|
546
|
+
}
|
|
547
|
+
} else {
|
|
548
|
+
return trimmed;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/// If `s` begins with the ASCII keyword `kw` (case-insensitive) followed by a
|
|
554
|
+
/// non-identifier boundary, return the remainder after it; otherwise `None`.
|
|
555
|
+
/// The boundary check keeps a longer identifier (`TABLES`, `iffy`) from
|
|
556
|
+
/// matching a keyword prefix.
|
|
557
|
+
fn strip_keyword_ci<'a>(s: &'a str, kw: &str) -> Option<&'a str> {
|
|
558
|
+
let bytes = s.as_bytes();
|
|
559
|
+
if bytes.len() < kw.len() || !bytes[..kw.len()].eq_ignore_ascii_case(kw.as_bytes()) {
|
|
560
|
+
return None;
|
|
561
|
+
}
|
|
562
|
+
let rest = &s[kw.len()..];
|
|
563
|
+
match rest.chars().next() {
|
|
564
|
+
Some(c) if c.is_alphanumeric() || c == '_' => None,
|
|
565
|
+
_ => Some(rest),
|
|
566
|
+
}
|
|
567
|
+
}
|
|
533
568
|
|
|
534
|
-
|
|
535
|
-
//
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
569
|
+
pub fn parse_table_name(ddl: &str) -> Option<String> {
|
|
570
|
+
// Match `CREATE TABLE` only as the *statement head*: skip leading
|
|
571
|
+
// whitespace and line/block comments and scan the original `ddl` by byte
|
|
572
|
+
// offset (never a `to_uppercase()` copy, whose length can differ from the
|
|
573
|
+
// source and mis-slice on Unicode). A `-- CREATE TABLE ...` comment must
|
|
574
|
+
// not hijack the name.
|
|
575
|
+
let rest = skip_ws_comments(ddl);
|
|
576
|
+
let rest = strip_keyword_ci(rest, "CREATE")?;
|
|
577
|
+
let rest = strip_keyword_ci(skip_ws_comments(rest), "TABLE")?;
|
|
578
|
+
let mut rest = skip_ws_comments(rest);
|
|
579
|
+
|
|
580
|
+
// Skip optional "IF NOT EXISTS". The keyword boundary check in
|
|
581
|
+
// `strip_keyword_ci` keeps a table literally named e.g. `iffy` from being
|
|
582
|
+
// mistaken for `IF`.
|
|
583
|
+
if let Some(after_if) = strip_keyword_ci(rest, "IF") {
|
|
584
|
+
let after_not = strip_keyword_ci(skip_ws_comments(after_if), "NOT")?;
|
|
585
|
+
let after_exists = strip_keyword_ci(skip_ws_comments(after_not), "EXISTS")?;
|
|
586
|
+
rest = skip_ws_comments(after_exists);
|
|
542
587
|
}
|
|
543
588
|
|
|
544
589
|
// `schema.table`: keep the last dot-separated segment. Each segment may
|
|
@@ -876,6 +921,59 @@ mod tests {
|
|
|
876
921
|
assert_eq!(parse_table_name("CREATE TABLE (id TEXT)"), None);
|
|
877
922
|
}
|
|
878
923
|
|
|
924
|
+
#[test]
|
|
925
|
+
fn parse_table_name_ignores_leading_line_comment() {
|
|
926
|
+
assert_eq!(
|
|
927
|
+
parse_table_name("-- create table old\nCREATE TABLE t (x TEXT)"),
|
|
928
|
+
Some("t".to_string())
|
|
929
|
+
);
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
#[test]
|
|
933
|
+
fn parse_table_name_ignores_leading_block_comment() {
|
|
934
|
+
assert_eq!(
|
|
935
|
+
parse_table_name("/* CREATE TABLE old */ CREATE TABLE t (x TEXT)"),
|
|
936
|
+
Some("t".to_string())
|
|
937
|
+
);
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
#[test]
|
|
941
|
+
fn parse_table_name_unterminated_comment_is_none() {
|
|
942
|
+
// Unterminated `--` and `/* */` comments consume the rest of the input,
|
|
943
|
+
// leaving no statement head.
|
|
944
|
+
assert_eq!(parse_table_name("-- no newline, no statement"), None);
|
|
945
|
+
assert_eq!(parse_table_name("/* unterminated CREATE TABLE t"), None);
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
#[test]
|
|
949
|
+
fn parse_table_name_rejects_keyword_prefix() {
|
|
950
|
+
// `CREATE TABLES` must not match the `TABLE` keyword prefix.
|
|
951
|
+
assert_eq!(parse_table_name("CREATE TABLES foo (id TEXT)"), None);
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
#[test]
|
|
955
|
+
fn parse_table_name_if_prefixed_name_is_not_if_not_exists() {
|
|
956
|
+
// A table whose name merely starts with `if` is not `IF NOT EXISTS`.
|
|
957
|
+
assert_eq!(
|
|
958
|
+
parse_table_name("CREATE TABLE ifx (id TEXT)"),
|
|
959
|
+
Some("ifx".to_string())
|
|
960
|
+
);
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
#[test]
|
|
964
|
+
fn parse_table_name_unicode_in_comment_does_not_panic() {
|
|
965
|
+
// A case-length-changing char (`fi`) in a comment must neither hijack
|
|
966
|
+
// the name nor panic on a byte-index slice.
|
|
967
|
+
assert_eq!(
|
|
968
|
+
parse_table_name("-- fi\nCREATE TABLE t (x TEXT)"),
|
|
969
|
+
Some("t".to_string())
|
|
970
|
+
);
|
|
971
|
+
assert_eq!(
|
|
972
|
+
parse_table_name("/* fififi */ CREATE TABLE t (x TEXT)"),
|
|
973
|
+
Some("t".to_string())
|
|
974
|
+
);
|
|
975
|
+
}
|
|
976
|
+
|
|
879
977
|
#[test]
|
|
880
978
|
fn get_table_columns_returns_user_columns_only() {
|
|
881
979
|
let db = Db::new().unwrap();
|
|
@@ -484,9 +484,13 @@ impl DirSQL {
|
|
|
484
484
|
}
|
|
485
485
|
|
|
486
486
|
fn handle_upsert(&self, table: &str, abs_path: &Path, rel_path: &str) -> Vec<RowEvent> {
|
|
487
|
-
// The
|
|
488
|
-
|
|
489
|
-
|
|
487
|
+
// The path may have vanished between the watcher event and now, or be a
|
|
488
|
+
// directory (a `mkdir` under the root matches a `**/*` glob). Only
|
|
489
|
+
// regular files become rows — mirror the initial scan, which skips
|
|
490
|
+
// non-files.
|
|
491
|
+
match self.inner.fs.is_file(abs_path) {
|
|
492
|
+
Ok(true) => {}
|
|
493
|
+
Ok(false) => return Vec::new(),
|
|
490
494
|
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Vec::new(),
|
|
491
495
|
Err(e) => return vec![error_event(Some(table), rel_path, e.to_string())],
|
|
492
496
|
}
|
|
@@ -1215,6 +1219,8 @@ fn prepare_persist(
|
|
|
1215
1219
|
/// tests can inject a deterministic double. Production always uses [`RealFs`].
|
|
1216
1220
|
trait FileSystem: Send + Sync {
|
|
1217
1221
|
fn stat(&self, path: &Path) -> std::io::Result<FileStat>;
|
|
1222
|
+
/// Whether `path` is a regular file. `Err(NotFound)` when it doesn't exist.
|
|
1223
|
+
fn is_file(&self, path: &Path) -> std::io::Result<bool>;
|
|
1218
1224
|
/// BLAKE3-hash a file's contents.
|
|
1219
1225
|
fn hash(&self, path: &Path) -> std::io::Result<[u8; 32]>;
|
|
1220
1226
|
/// Canonicalize the watch root, falling back to the literal path.
|
|
@@ -1228,6 +1234,10 @@ impl FileSystem for RealFs {
|
|
|
1228
1234
|
std::fs::metadata(path).map(|m| FileStat::from_metadata(&m))
|
|
1229
1235
|
}
|
|
1230
1236
|
|
|
1237
|
+
fn is_file(&self, path: &Path) -> std::io::Result<bool> {
|
|
1238
|
+
std::fs::metadata(path).map(|m| m.is_file())
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1231
1241
|
fn hash(&self, path: &Path) -> std::io::Result<[u8; 32]> {
|
|
1232
1242
|
hash_file(path)
|
|
1233
1243
|
}
|
|
@@ -1822,15 +1832,19 @@ mod readonly_tests {
|
|
|
1822
1832
|
mod internal_tests {
|
|
1823
1833
|
use super::*;
|
|
1824
1834
|
use std::collections::HashMap as StdHashMap;
|
|
1835
|
+
use std::collections::HashSet as StdHashSet;
|
|
1825
1836
|
use tempfile::TempDir;
|
|
1826
1837
|
|
|
1827
1838
|
/// Deterministic [`FileSystem`] double: canned [`FileStat`]s and hashes;
|
|
1828
|
-
/// any path not present stats/hashes as `NotFound`.
|
|
1839
|
+
/// any path not present stats/hashes as `NotFound`. A path registered via
|
|
1840
|
+
/// [`with_dir`](FakeFs::with_dir) reports as a non-file; any other stat'd
|
|
1841
|
+
/// path reports as a regular file.
|
|
1829
1842
|
#[derive(Default)]
|
|
1830
1843
|
struct FakeFs {
|
|
1831
1844
|
stats: StdHashMap<PathBuf, FileStat>,
|
|
1832
1845
|
hashes: StdHashMap<PathBuf, [u8; 32]>,
|
|
1833
1846
|
canonical_roots: StdHashMap<PathBuf, String>,
|
|
1847
|
+
dirs: StdHashSet<PathBuf>,
|
|
1834
1848
|
}
|
|
1835
1849
|
|
|
1836
1850
|
impl FakeFs {
|
|
@@ -1840,6 +1854,12 @@ mod internal_tests {
|
|
|
1840
1854
|
fs
|
|
1841
1855
|
}
|
|
1842
1856
|
|
|
1857
|
+
/// Register `path` as an existing non-file (a directory).
|
|
1858
|
+
fn with_dir(mut self, path: impl Into<PathBuf>) -> Self {
|
|
1859
|
+
self.dirs.insert(path.into());
|
|
1860
|
+
self
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1843
1863
|
fn set_hash(&mut self, path: impl Into<PathBuf>, hash: [u8; 32]) {
|
|
1844
1864
|
self.hashes.insert(path.into(), hash);
|
|
1845
1865
|
}
|
|
@@ -1862,6 +1882,19 @@ mod internal_tests {
|
|
|
1862
1882
|
})
|
|
1863
1883
|
}
|
|
1864
1884
|
|
|
1885
|
+
fn is_file(&self, path: &Path) -> std::io::Result<bool> {
|
|
1886
|
+
if self.dirs.contains(path) {
|
|
1887
|
+
return Ok(false);
|
|
1888
|
+
}
|
|
1889
|
+
if self.stats.contains_key(path) {
|
|
1890
|
+
return Ok(true);
|
|
1891
|
+
}
|
|
1892
|
+
Err(std::io::Error::new(
|
|
1893
|
+
std::io::ErrorKind::NotFound,
|
|
1894
|
+
"fake: no such file",
|
|
1895
|
+
))
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1865
1898
|
fn hash(&self, path: &Path) -> std::io::Result<[u8; 32]> {
|
|
1866
1899
|
self.hashes.get(path).copied().ok_or_else(|| {
|
|
1867
1900
|
std::io::Error::new(std::io::ErrorKind::NotFound, "fake: no such file")
|
|
@@ -2208,6 +2241,15 @@ mod internal_tests {
|
|
|
2208
2241
|
fs.hash(&missing).is_err(),
|
|
2209
2242
|
"hash of a missing path must error"
|
|
2210
2243
|
);
|
|
2244
|
+
|
|
2245
|
+
assert!(
|
|
2246
|
+
!fs.is_file(dir.path()).unwrap(),
|
|
2247
|
+
"a directory must report is_file=false"
|
|
2248
|
+
);
|
|
2249
|
+
assert!(
|
|
2250
|
+
fs.is_file(&missing).is_err(),
|
|
2251
|
+
"is_file of a missing path must error"
|
|
2252
|
+
);
|
|
2211
2253
|
// A nonexistent path can't canonicalize, so the literal fallback runs.
|
|
2212
2254
|
assert_eq!(
|
|
2213
2255
|
fs.canonical_root(&missing),
|
|
@@ -2371,6 +2413,34 @@ mod internal_tests {
|
|
|
2371
2413
|
assert!(dbg.contains("insert forbidden"), "got: {dbg}");
|
|
2372
2414
|
}
|
|
2373
2415
|
|
|
2416
|
+
#[test]
|
|
2417
|
+
fn handle_upsert_skips_directory() {
|
|
2418
|
+
// A `mkdir` under the root matches a `**/*` glob, but a directory must
|
|
2419
|
+
// not become a row — mirror the initial scan's non-file skip.
|
|
2420
|
+
let dir = TempDir::new().unwrap();
|
|
2421
|
+
let subdir = dir.path().join("subdir");
|
|
2422
|
+
let fake = FakeFs::default().with_dir(subdir.clone());
|
|
2423
|
+
let db = DirSQL::with_ignore_and_fs(
|
|
2424
|
+
dir.path(),
|
|
2425
|
+
vec![Table::new("CREATE TABLE files (name TEXT)", "**/*", |_| {
|
|
2426
|
+
vec![Row::from_iter([(
|
|
2427
|
+
"name".to_string(),
|
|
2428
|
+
Value::Text("x".into()),
|
|
2429
|
+
)])]
|
|
2430
|
+
})],
|
|
2431
|
+
Vec::<String>::new(),
|
|
2432
|
+
Arc::new(fake),
|
|
2433
|
+
)
|
|
2434
|
+
.unwrap();
|
|
2435
|
+
|
|
2436
|
+
let events = db.handle_upsert("files", &subdir, "subdir");
|
|
2437
|
+
assert!(events.is_empty(), "a directory must not produce row events");
|
|
2438
|
+
assert!(
|
|
2439
|
+
db.query("SELECT * FROM files").unwrap().is_empty(),
|
|
2440
|
+
"a directory must not insert a row"
|
|
2441
|
+
);
|
|
2442
|
+
}
|
|
2443
|
+
|
|
2374
2444
|
#[test]
|
|
2375
2445
|
fn handle_upsert_returns_empty_when_file_vanished() {
|
|
2376
2446
|
let (dir, db, _abs, _rel) = upsert_fixture();
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
use notify::event::{ModifyKind, RenameMode};
|
|
1
2
|
use notify::{
|
|
2
3
|
Config, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher as NotifyWatcher,
|
|
3
4
|
};
|
|
@@ -72,6 +73,12 @@ fn translate_event(event: &Event) -> Vec<FileEvent> {
|
|
|
72
73
|
for path in &event.paths {
|
|
73
74
|
let fe = match event.kind {
|
|
74
75
|
EventKind::Create(_) => Some(FileEvent::Created(path.clone())),
|
|
76
|
+
// A rename OUT of the tree (inotify `IN_MOVED_FROM`) leaves no file
|
|
77
|
+
// behind, so it is a removal — otherwise the moved-away file's rows
|
|
78
|
+
// would persist.
|
|
79
|
+
EventKind::Modify(ModifyKind::Name(RenameMode::From)) => {
|
|
80
|
+
Some(FileEvent::Deleted(path.clone()))
|
|
81
|
+
}
|
|
75
82
|
EventKind::Modify(_) => Some(FileEvent::Modified(path.clone())),
|
|
76
83
|
EventKind::Remove(_) => Some(FileEvent::Deleted(path.clone())),
|
|
77
84
|
_ => None,
|
|
@@ -117,6 +124,17 @@ fn remove_event(paths: Vec<PathBuf>) -> Event {
|
|
|
117
124
|
}
|
|
118
125
|
}
|
|
119
126
|
|
|
127
|
+
#[cfg(test)]
|
|
128
|
+
fn rename_from_event(paths: Vec<PathBuf>) -> Event {
|
|
129
|
+
Event {
|
|
130
|
+
kind: EventKind::Modify(notify::event::ModifyKind::Name(
|
|
131
|
+
notify::event::RenameMode::From,
|
|
132
|
+
)),
|
|
133
|
+
paths,
|
|
134
|
+
attrs: Default::default(),
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
120
138
|
#[cfg(test)]
|
|
121
139
|
fn access_event(paths: Vec<PathBuf>) -> Event {
|
|
122
140
|
Event {
|
|
@@ -167,6 +185,17 @@ mod tests {
|
|
|
167
185
|
);
|
|
168
186
|
}
|
|
169
187
|
|
|
188
|
+
#[test]
|
|
189
|
+
fn translate_event_maps_rename_from_to_delete() {
|
|
190
|
+
// inotify emits `Modify(Name(From))` when a file is renamed OUT of the
|
|
191
|
+
// watched tree; it must be treated as a removal so its rows are deleted.
|
|
192
|
+
let results = translate_event(&rename_from_event(vec![PathBuf::from("/tmp/moved.txt")]));
|
|
193
|
+
assert_eq!(
|
|
194
|
+
results,
|
|
195
|
+
vec![FileEvent::Deleted(PathBuf::from("/tmp/moved.txt"))]
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
170
199
|
#[test]
|
|
171
200
|
fn translate_event_multiple_paths() {
|
|
172
201
|
let results = translate_event(&create_event(vec![
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
[package]
|
|
2
|
-
name = "dirsql-py-ext"
|
|
3
|
-
# Explicit literal (not `version.workspace = true`) so putitoutthere's
|
|
4
|
-
# pypi/maturin handler can rewrite it via `write-version` before
|
|
5
|
-
# `maturin build`. `pyproject.toml` declares `dynamic = ["version"]`
|
|
6
|
-
# and maturin reads this field. Mirrors `packages/rust/Cargo.toml`.
|
|
7
|
-
version = "0.3.80"
|
|
8
|
-
edition.workspace = true
|
|
9
|
-
publish = false
|
|
10
|
-
readme = "README.md"
|
|
11
|
-
|
|
12
|
-
[lib]
|
|
13
|
-
name = "dirsql_py_ext"
|
|
14
|
-
crate-type = ["cdylib", "rlib"]
|
|
15
|
-
|
|
16
|
-
[dependencies]
|
|
17
|
-
dirsql.workspace = true
|
|
18
|
-
pyo3 = { version = "0.28", features = ["extension-module", "generate-import-lib"], optional = true }
|
|
19
|
-
|
|
20
|
-
[features]
|
|
21
|
-
default = []
|
|
22
|
-
extension-module = ["pyo3/extension-module"]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
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.80 → dirsql-0.3.82}/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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|