dirsql 0.3.56__tar.gz → 0.3.58__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.56 → dirsql-0.3.58}/Cargo.lock +1 -1
- {dirsql-0.3.56 → dirsql-0.3.58}/PKG-INFO +1 -1
- {dirsql-0.3.56 → dirsql-0.3.58}/packages/python/Cargo.toml +1 -1
- {dirsql-0.3.56 → dirsql-0.3.58}/packages/python/e2e-attestation.json +2 -2
- {dirsql-0.3.56 → dirsql-0.3.58}/packages/rust/src/cli/init.rs +17 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/packages/rust/src/cli/server.rs +29 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/packages/rust/src/command.rs +99 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/packages/rust/src/db.rs +77 -2
- {dirsql-0.3.56 → dirsql-0.3.58}/packages/rust/src/lib.rs +559 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/packages/rust/src/persist.rs +146 -6
- {dirsql-0.3.56 → dirsql-0.3.58}/Cargo.toml +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/README.md +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/dirsql/__init__.py +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/dirsql/_async.py +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/dirsql/_dirsql.pyi +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/dirsql/cli/__init__.py +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/dirsql/cli/binary_path.py +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/dirsql/cli/interpret/__init__.py +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/dirsql/cli/is_windows.py +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/dirsql/cli/main.py +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/dirsql/cli/resolve_config_extensions.py +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/dirsql/py.typed +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/dirsql/resolve_config_extensions.py +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/dirsql/resolve_extension.py +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/docs/.claude/CLAUDE.md +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/docs/.vitepress/config.ts +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/docs/.vitepress/theme/index.ts +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/docs/.vitepress/theme/lang.ts +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/docs/AGENTS.md +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/docs/api/index.md +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/docs/cli/config.md +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/docs/cli/http-api.md +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/docs/cli/index.md +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/docs/cli/init.md +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/docs/cli/server.md +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/docs/getting-started.md +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/docs/guide/async.md +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/docs/guide/crdt.md +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/docs/guide/persistence.md +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/docs/guide/querying.md +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/docs/guide/tables.md +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/docs/guide/watching.md +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/docs/index.md +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/docs/migrations.md +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/docs/package.json +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/docs/playwright.config.ts +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/docs/pnpm-lock.yaml +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/docs/pnpm-workspace.yaml +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/docs/tests/integration/home.spec.ts +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/docs/tests/integration/language-flag.spec.ts +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/docs/tests/integration/sidebar.spec.ts +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/docs/tests/unit/config.test.ts +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/docs/tests/unit/lang.test.ts +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/docs/vitest.config.ts +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/packages/python/README.md +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/packages/python/conftest.py +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/packages/python/docs/.claude/CLAUDE.md +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/packages/python/docs/.vitepress/config.ts +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/packages/python/docs/.vitepress/theme/index.ts +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/packages/python/docs/.vitepress/theme/lang.ts +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/packages/python/docs/AGENTS.md +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/packages/python/docs/api/index.md +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/packages/python/docs/cli/config.md +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/packages/python/docs/cli/http-api.md +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/packages/python/docs/cli/index.md +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/packages/python/docs/cli/init.md +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/packages/python/docs/cli/server.md +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/packages/python/docs/getting-started.md +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/packages/python/docs/guide/async.md +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/packages/python/docs/guide/crdt.md +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/packages/python/docs/guide/persistence.md +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/packages/python/docs/guide/querying.md +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/packages/python/docs/guide/tables.md +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/packages/python/docs/guide/watching.md +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/packages/python/docs/index.md +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/packages/python/docs/migrations.md +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/packages/python/docs/package.json +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/packages/python/docs/playwright.config.ts +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/packages/python/docs/pnpm-lock.yaml +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/packages/python/docs/pnpm-workspace.yaml +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/packages/python/docs/tests/integration/home.spec.ts +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/packages/python/docs/tests/integration/language-flag.spec.ts +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/packages/python/docs/tests/integration/sidebar.spec.ts +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/packages/python/docs/tests/unit/config.test.ts +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/packages/python/docs/tests/unit/lang.test.ts +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/packages/python/docs/vitest.config.ts +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/packages/python/src/lib.rs +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/packages/python/tests/__init__.py +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/packages/python/tests/binding/__init__.py +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/packages/python/tests/conftest.py +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/packages/python/tests/e2e/__init__.py +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/packages/python/tests/integration/__init__.py +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/packages/python/tests/smoke/__init__.py +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/packages/rust/Cargo.toml +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/packages/rust/README.md +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/packages/rust/benches/db_bench.rs +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/packages/rust/benches/differ_bench.rs +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/packages/rust/benches/matcher_bench.rs +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/packages/rust/benches/scanner_bench.rs +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/packages/rust/docs/api/index.md +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/packages/rust/docs/cli/config.md +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/packages/rust/docs/cli/http-api.md +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/packages/rust/docs/cli/index.md +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/packages/rust/docs/cli/init.md +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/packages/rust/docs/cli/server.md +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/packages/rust/docs/getting-started.md +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/packages/rust/docs/guide/async.md +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/packages/rust/docs/guide/crdt.md +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/packages/rust/docs/guide/persistence.md +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/packages/rust/docs/guide/querying.md +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/packages/rust/docs/guide/tables.md +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/packages/rust/docs/guide/watching.md +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/packages/rust/docs/index.md +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/packages/rust/docs/migrations.md +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/packages/rust/src/bin/dirsql.rs +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/packages/rust/src/cli/mod.rs +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/packages/rust/src/cli/router.rs +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/packages/rust/src/cli/serialize.rs +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/packages/rust/src/config.rs +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/packages/rust/src/differ.rs +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/packages/rust/src/matcher.rs +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/packages/rust/src/scanner.rs +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/packages/rust/src/watcher.rs +0 -0
- {dirsql-0.3.56 → dirsql-0.3.58}/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.58"
|
|
8
8
|
edition.workspace = true
|
|
9
9
|
publish = false
|
|
10
10
|
readme = "README.md"
|
|
@@ -149,4 +149,21 @@ mod tests {
|
|
|
149
149
|
let msg = format!("{err}").to_lowercase();
|
|
150
150
|
assert!(msg.contains("claude"));
|
|
151
151
|
}
|
|
152
|
+
|
|
153
|
+
/// `run` bails before spawning the agent when the output already exists and
|
|
154
|
+
/// `--force` was not passed (the temp dir itself is an existing path).
|
|
155
|
+
#[test]
|
|
156
|
+
fn run_bails_when_output_exists_without_force() {
|
|
157
|
+
let dir = tempfile::tempdir().unwrap();
|
|
158
|
+
let opts = InitOptions {
|
|
159
|
+
root: dir.path().to_path_buf(),
|
|
160
|
+
output: dir.path().to_path_buf(),
|
|
161
|
+
force: false,
|
|
162
|
+
};
|
|
163
|
+
let err = run(opts).unwrap_err();
|
|
164
|
+
assert!(
|
|
165
|
+
matches!(err, InitError::AlreadyExists { .. }),
|
|
166
|
+
"got: {err:?}"
|
|
167
|
+
);
|
|
168
|
+
}
|
|
152
169
|
}
|
|
@@ -114,4 +114,33 @@ mod tests {
|
|
|
114
114
|
|
|
115
115
|
handle.shutdown().await.expect("graceful shutdown");
|
|
116
116
|
}
|
|
117
|
+
|
|
118
|
+
// A `Ready` state drives the `serve` convenience wrapper plus
|
|
119
|
+
// `start_watch_task` (which attaches `DirSQL::watch` and pumps its stream
|
|
120
|
+
// into the broadcast channel). Built over an empty temp dir so the scan
|
|
121
|
+
// touches nothing; the port-0 bind and graceful shutdown are real.
|
|
122
|
+
#[tokio::test]
|
|
123
|
+
async fn serve_with_a_ready_db_attaches_the_watcher_then_shuts_down() {
|
|
124
|
+
let dir = tempfile::tempdir().unwrap();
|
|
125
|
+
let db = DirSQL::new(dir.path(), Vec::new()).unwrap();
|
|
126
|
+
let config = ServerConfig::bind("127.0.0.1".to_string(), 0);
|
|
127
|
+
let handle = serve(config, db).await.expect("bind on an ephemeral port");
|
|
128
|
+
assert_ne!(handle.local_addr().port(), 0);
|
|
129
|
+
handle.shutdown().await.expect("graceful shutdown");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Binding to a non-local TEST-NET address (RFC 5737) fails with
|
|
133
|
+
// "cannot assign requested address", surfacing `ServerError::Bind` rather
|
|
134
|
+
// than panicking — deterministic and DNS-free.
|
|
135
|
+
#[tokio::test]
|
|
136
|
+
async fn serve_with_state_surfaces_a_bind_error_for_a_nonlocal_address() {
|
|
137
|
+
let config = ServerConfig::bind("192.0.2.1".to_string(), 9);
|
|
138
|
+
// `serve_with_state` returns `Result<ServerHandle, _>`; ServerHandle is
|
|
139
|
+
// not Debug, so match rather than `unwrap_err`.
|
|
140
|
+
let err = match serve_with_state(config, AppState::Unavailable("x".to_string())).await {
|
|
141
|
+
Ok(_) => panic!("expected a bind error"),
|
|
142
|
+
Err(e) => e,
|
|
143
|
+
};
|
|
144
|
+
assert!(matches!(err, ServerError::Bind { .. }), "got: {err:?}");
|
|
145
|
+
}
|
|
117
146
|
}
|
|
@@ -484,4 +484,103 @@ mod tests {
|
|
|
484
484
|
assert!(!Placeholder::new("a", "b").append_if_absent);
|
|
485
485
|
assert!(Placeholder::append("a", "b").append_if_absent);
|
|
486
486
|
}
|
|
487
|
+
|
|
488
|
+
// ----- run_command end-to-end (spawns trivial POSIX commands) ----------
|
|
489
|
+
//
|
|
490
|
+
// `run_command` can only be exercised by actually spawning a child, so
|
|
491
|
+
// these drive `sh`/`echo`/`cat` — universally present on the Linux CI
|
|
492
|
+
// runners. The test code itself statically references only `super::` items,
|
|
493
|
+
// `Duration`, and `std::path::Path`, so the `unit lint` isolation rule is
|
|
494
|
+
// satisfied; the effectful process/thread work lives in production code.
|
|
495
|
+
|
|
496
|
+
fn cwd() -> std::path::PathBuf {
|
|
497
|
+
std::path::PathBuf::from(".")
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
#[test]
|
|
501
|
+
fn run_command_returns_last_nonempty_stdout_line() {
|
|
502
|
+
let out = run_command(
|
|
503
|
+
"sh -c 'echo chatter; echo PAYLOAD'",
|
|
504
|
+
&[],
|
|
505
|
+
&cwd(),
|
|
506
|
+
Duration::from_secs(30),
|
|
507
|
+
None,
|
|
508
|
+
)
|
|
509
|
+
.unwrap();
|
|
510
|
+
assert_eq!(out.payload, "PAYLOAD");
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
#[test]
|
|
514
|
+
fn run_command_writes_stdin_payload_to_the_child() {
|
|
515
|
+
// `cat` echoes its stdin, so the payload round-trips through the
|
|
516
|
+
// stdin-writer thread and the stdout drain.
|
|
517
|
+
let out = run_command(
|
|
518
|
+
"cat",
|
|
519
|
+
&[],
|
|
520
|
+
&cwd(),
|
|
521
|
+
Duration::from_secs(30),
|
|
522
|
+
Some(b"hello-stdin"),
|
|
523
|
+
)
|
|
524
|
+
.unwrap();
|
|
525
|
+
assert_eq!(out.payload, "hello-stdin");
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
#[test]
|
|
529
|
+
fn run_command_reports_nonzero_exit_with_stderr_tail() {
|
|
530
|
+
let err = run_command(
|
|
531
|
+
"sh -c 'echo oops >&2; exit 3'",
|
|
532
|
+
&[],
|
|
533
|
+
&cwd(),
|
|
534
|
+
Duration::from_secs(30),
|
|
535
|
+
None,
|
|
536
|
+
)
|
|
537
|
+
.unwrap_err();
|
|
538
|
+
match err {
|
|
539
|
+
CommandError::NonZeroExit {
|
|
540
|
+
code, stderr_tail, ..
|
|
541
|
+
} => {
|
|
542
|
+
assert_eq!(code, "3");
|
|
543
|
+
assert!(stderr_tail.contains("oops"), "got: {stderr_tail}");
|
|
544
|
+
}
|
|
545
|
+
other => panic!("expected NonZeroExit, got {other:?}"),
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
#[test]
|
|
550
|
+
fn run_command_reports_empty_output_when_stdout_is_blank() {
|
|
551
|
+
let err = run_command("true", &[], &cwd(), Duration::from_secs(30), None).unwrap_err();
|
|
552
|
+
assert!(
|
|
553
|
+
matches!(err, CommandError::EmptyOutput { .. }),
|
|
554
|
+
"got: {err:?}"
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
#[test]
|
|
559
|
+
fn run_command_reports_spawn_failure_for_a_missing_program() {
|
|
560
|
+
let err = run_command(
|
|
561
|
+
"definitely-not-a-real-binary-xyzzy",
|
|
562
|
+
&[],
|
|
563
|
+
&cwd(),
|
|
564
|
+
Duration::from_secs(30),
|
|
565
|
+
None,
|
|
566
|
+
)
|
|
567
|
+
.unwrap_err();
|
|
568
|
+
assert!(matches!(err, CommandError::Spawn { .. }), "got: {err:?}");
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
#[test]
|
|
572
|
+
fn run_command_kills_and_reports_timeout_even_with_stdin() {
|
|
573
|
+
// A short timeout against a long sleep drives the timeout branch,
|
|
574
|
+
// including joining the stdin-writer thread. `sh` ignoring stdin closes
|
|
575
|
+
// the pipe early, which the writer treats as a clean EOF.
|
|
576
|
+
let err = run_command(
|
|
577
|
+
"sh -c 'sleep 30'",
|
|
578
|
+
&[],
|
|
579
|
+
&cwd(),
|
|
580
|
+
Duration::from_millis(50),
|
|
581
|
+
Some(b"x"),
|
|
582
|
+
)
|
|
583
|
+
.unwrap_err();
|
|
584
|
+
assert!(matches!(err, CommandError::Timeout { .. }), "got: {err:?}");
|
|
585
|
+
}
|
|
487
586
|
}
|
|
@@ -301,11 +301,21 @@ impl Db {
|
|
|
301
301
|
/// The user-row deletes and the matching `_dirsql_internal_rows` mapping
|
|
302
302
|
/// deletes commit in ONE transaction (epic #358), so the mapping never
|
|
303
303
|
/// outlives the rows it describes.
|
|
304
|
+
///
|
|
305
|
+
/// Stage 2 (epic #358): row ownership is read from the mapping, not the
|
|
306
|
+
/// injected `_dirsql_file_path` column (now write-only). The user rows to
|
|
307
|
+
/// drop are those whose `rowid` the mapping attributes to `file_path` under
|
|
308
|
+
/// `table`; the mapping rows are then removed in the same transaction.
|
|
304
309
|
pub fn delete_rows_by_file(&self, table: &str, file_path: &str) -> Result<usize> {
|
|
305
310
|
validate_identifier(table)?;
|
|
306
311
|
let tx = self.conn.unchecked_transaction()?;
|
|
307
|
-
let sql = format!(
|
|
308
|
-
|
|
312
|
+
let sql = format!(
|
|
313
|
+
"DELETE FROM {} WHERE rowid IN \
|
|
314
|
+
(SELECT rowid_ref FROM _dirsql_internal_rows \
|
|
315
|
+
WHERE table_name = ?1 AND file_path = ?2)",
|
|
316
|
+
table
|
|
317
|
+
);
|
|
318
|
+
let count = tx.execute(&sql, rusqlite::params![table, file_path])?;
|
|
309
319
|
tx.execute(
|
|
310
320
|
"DELETE FROM _dirsql_internal_rows WHERE table_name = ?1 AND file_path = ?2",
|
|
311
321
|
rusqlite::params![table, file_path],
|
|
@@ -1593,6 +1603,71 @@ mod tests {
|
|
|
1593
1603
|
db.check_row_mapping_equivalence("t").unwrap();
|
|
1594
1604
|
}
|
|
1595
1605
|
|
|
1606
|
+
#[test]
|
|
1607
|
+
fn delete_rows_by_file_reads_mapping_not_columns() {
|
|
1608
|
+
// Stage 2 (epic #358): delete-by-file resolves ownership through the
|
|
1609
|
+
// mapping, not the now-write-only `_dirsql_file_path` column. Corrupt
|
|
1610
|
+
// the column so it disagrees with the mapping and confirm the delete
|
|
1611
|
+
// still follows the mapping.
|
|
1612
|
+
let db = Db::new().unwrap();
|
|
1613
|
+
db.create_table("CREATE TABLE t (id TEXT)").unwrap();
|
|
1614
|
+
db.insert_row(
|
|
1615
|
+
"t",
|
|
1616
|
+
&HashMap::from([("id".into(), Value::Text("a".into()))]),
|
|
1617
|
+
"real.json",
|
|
1618
|
+
0,
|
|
1619
|
+
)
|
|
1620
|
+
.unwrap();
|
|
1621
|
+
// Desync the write-only column from the mapping.
|
|
1622
|
+
db.conn
|
|
1623
|
+
.execute("UPDATE t SET _dirsql_file_path = 'wrong.json'", [])
|
|
1624
|
+
.unwrap();
|
|
1625
|
+
|
|
1626
|
+
// Deleting by the mapping's file removes the row; deleting by the
|
|
1627
|
+
// stale column value is a no-op.
|
|
1628
|
+
assert_eq!(db.delete_rows_by_file("t", "wrong.json").unwrap(), 0);
|
|
1629
|
+
assert_eq!(db.delete_rows_by_file("t", "real.json").unwrap(), 1);
|
|
1630
|
+
let remaining: i64 = db
|
|
1631
|
+
.conn
|
|
1632
|
+
.query_row("SELECT COUNT(*) FROM t", [], |r| r.get(0))
|
|
1633
|
+
.unwrap();
|
|
1634
|
+
assert_eq!(remaining, 0);
|
|
1635
|
+
db.check_row_mapping_equivalence("t").unwrap();
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
#[test]
|
|
1639
|
+
fn delete_rows_by_file_is_scoped_to_its_table() {
|
|
1640
|
+
// The mapping subquery is keyed on (table_name, file_path): a delete on
|
|
1641
|
+
// one table must not touch another table's rows for the same file path,
|
|
1642
|
+
// even when they share a rowid.
|
|
1643
|
+
let db = Db::new().unwrap();
|
|
1644
|
+
db.create_table("CREATE TABLE t1 (id TEXT)").unwrap();
|
|
1645
|
+
db.create_table("CREATE TABLE t2 (id TEXT)").unwrap();
|
|
1646
|
+
db.insert_row(
|
|
1647
|
+
"t1",
|
|
1648
|
+
&HashMap::from([("id".into(), Value::Text("x".into()))]),
|
|
1649
|
+
"shared.json",
|
|
1650
|
+
0,
|
|
1651
|
+
)
|
|
1652
|
+
.unwrap();
|
|
1653
|
+
db.insert_row(
|
|
1654
|
+
"t2",
|
|
1655
|
+
&HashMap::from([("id".into(), Value::Text("y".into()))]),
|
|
1656
|
+
"shared.json",
|
|
1657
|
+
0,
|
|
1658
|
+
)
|
|
1659
|
+
.unwrap();
|
|
1660
|
+
|
|
1661
|
+
assert_eq!(db.delete_rows_by_file("t1", "shared.json").unwrap(), 1);
|
|
1662
|
+
let t2_rows: i64 = db
|
|
1663
|
+
.conn
|
|
1664
|
+
.query_row("SELECT COUNT(*) FROM t2", [], |r| r.get(0))
|
|
1665
|
+
.unwrap();
|
|
1666
|
+
assert_eq!(t2_rows, 1, "t2's row for the same path must be untouched");
|
|
1667
|
+
db.check_row_mapping_equivalence("t1").unwrap();
|
|
1668
|
+
db.check_row_mapping_equivalence("t2").unwrap();
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1596
1671
|
#[test]
|
|
1597
1672
|
fn failed_row_insert_leaves_no_mapping_row() {
|
|
1598
1673
|
// Transactional coupling, forward direction: when the user-row insert
|