dirsql 0.3.10__tar.gz → 0.3.12__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.10 → dirsql-0.3.12}/Cargo.lock +1 -1
- {dirsql-0.3.10 → dirsql-0.3.12}/PKG-INFO +1 -1
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/python/Cargo.toml +1 -1
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/rust/src/cli/mod.rs +45 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/rust/src/cli/router.rs +48 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/rust/src/cli/serialize.rs +31 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/rust/src/config.rs +4 -8
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/rust/src/db.rs +56 -10
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/rust/src/differ.rs +10 -6
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/rust/src/lib.rs +513 -40
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/rust/src/matcher.rs +65 -12
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/rust/src/persist.rs +155 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/rust/src/watcher.rs +62 -35
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/rust/tests/async_sdk.rs +119 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/rust/tests/cli_e2e.rs +3 -5
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/rust/tests/cli_integration.rs +128 -5
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/rust/tests/from_config.rs +138 -5
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/rust/tests/init_integration.rs +1 -1
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/rust/tests/persist.rs +39 -3
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/rust/tests/sdk.rs +156 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/pyproject.toml +15 -8
- {dirsql-0.3.10 → dirsql-0.3.12}/Cargo.toml +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/README.md +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/dirsql/__init__.py +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/dirsql/_async.py +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/dirsql/_cli/__init__.py +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/dirsql/_cli/binary_path.py +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/dirsql/_cli/is_windows.py +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/dirsql/_cli/main.py +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/dirsql/test_async.py +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/docs/.claude/CLAUDE.md +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/docs/.vitepress/config.ts +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/docs/.vitepress/theme/index.ts +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/docs/.vitepress/theme/lang.ts +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/docs/AGENTS.md +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/docs/api/index.md +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/docs/cli/config.md +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/docs/cli/http-api.md +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/docs/cli/index.md +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/docs/cli/init.md +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/docs/cli/server.md +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/docs/getting-started.md +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/docs/guide/async.md +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/docs/guide/crdt.md +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/docs/guide/persistence.md +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/docs/guide/querying.md +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/docs/guide/tables.md +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/docs/guide/watching.md +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/docs/index.md +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/docs/migrations.md +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/docs/package.json +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/docs/playwright.config.ts +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/docs/pnpm-lock.yaml +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/docs/pnpm-workspace.yaml +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/docs/tests/integration/home.spec.ts +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/docs/tests/integration/language-flag.spec.ts +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/docs/tests/unit/config.test.ts +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/docs/tests/unit/lang.test.ts +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/docs/vitest.config.ts +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/python/README.md +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/python/conftest.py +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/python/docs/.claude/CLAUDE.md +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/python/docs/.vitepress/config.ts +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/python/docs/.vitepress/theme/index.ts +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/python/docs/.vitepress/theme/lang.ts +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/python/docs/AGENTS.md +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/python/docs/api/index.md +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/python/docs/cli/config.md +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/python/docs/cli/http-api.md +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/python/docs/cli/index.md +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/python/docs/cli/init.md +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/python/docs/cli/server.md +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/python/docs/getting-started.md +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/python/docs/guide/async.md +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/python/docs/guide/crdt.md +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/python/docs/guide/persistence.md +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/python/docs/guide/querying.md +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/python/docs/guide/tables.md +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/python/docs/guide/watching.md +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/python/docs/index.md +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/python/docs/migrations.md +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/python/docs/package.json +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/python/docs/playwright.config.ts +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/python/docs/pnpm-lock.yaml +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/python/docs/pnpm-workspace.yaml +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/python/docs/tests/integration/home.spec.ts +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/python/docs/tests/integration/language-flag.spec.ts +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/python/docs/tests/unit/config.test.ts +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/python/docs/tests/unit/lang.test.ts +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/python/docs/vitest.config.ts +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/python/src/lib.rs +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/python/tests/__init__.py +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/python/tests/conftest.py +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/python/tests/e2e/__init__.py +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/python/tests/integration/__init__.py +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/python/tests/integration/test_async_dirsql.py +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/python/tests/integration/test_binding.py +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/python/tests/integration/test_dirsql.py +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/python/tests/integration/test_docs_examples.py +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/python/tests/integration/test_docs_gaps.py +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/python/tests/integration/test_from_config.py +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/python/tests/integration/test_persist.py +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/rust/Cargo.toml +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/rust/README.md +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/rust/benches/db_bench.rs +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/rust/benches/differ_bench.rs +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/rust/benches/matcher_bench.rs +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/rust/benches/scanner_bench.rs +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/rust/docs/api/index.md +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/rust/docs/cli/config.md +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/rust/docs/cli/http-api.md +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/rust/docs/cli/index.md +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/rust/docs/cli/init.md +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/rust/docs/cli/server.md +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/rust/docs/getting-started.md +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/rust/docs/guide/async.md +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/rust/docs/guide/crdt.md +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/rust/docs/guide/persistence.md +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/rust/docs/guide/querying.md +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/rust/docs/guide/tables.md +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/rust/docs/guide/watching.md +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/rust/docs/index.md +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/rust/docs/migrations.md +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/rust/src/bin/dirsql.rs +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/rust/src/cli/init.rs +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/rust/src/cli/server.rs +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/rust/src/scanner.rs +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/rust/tests/docs_examples.rs +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/rust/tests/docs_gaps.rs +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/rust/tests/init_e2e.rs +0 -0
- {dirsql-0.3.10 → dirsql-0.3.12}/packages/rust/tests/readonly_query.rs +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.12"
|
|
8
8
|
edition.workspace = true
|
|
9
9
|
publish = false
|
|
10
10
|
readme = "README.md"
|
|
@@ -145,3 +145,48 @@ pub enum ServerError {
|
|
|
145
145
|
#[error("server task panicked: {0}")]
|
|
146
146
|
Join(#[from] JoinError),
|
|
147
147
|
}
|
|
148
|
+
|
|
149
|
+
#[cfg(test)]
|
|
150
|
+
mod tests {
|
|
151
|
+
use super::*;
|
|
152
|
+
use tempfile::TempDir;
|
|
153
|
+
|
|
154
|
+
#[test]
|
|
155
|
+
fn default_config_binds_localhost_7117_with_30s_timeout() {
|
|
156
|
+
let cfg = ServerConfig::default();
|
|
157
|
+
assert_eq!(cfg.host, "localhost");
|
|
158
|
+
assert_eq!(cfg.port, 7117);
|
|
159
|
+
assert_eq!(cfg.query_timeout, Duration::from_secs(30));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
#[test]
|
|
163
|
+
fn app_state_from_dirsql_is_ready() {
|
|
164
|
+
// `AppState: !Debug` (it wraps a `DirSQL`). Assert the `Ready` variant
|
|
165
|
+
// without a dead match arm by routing through `require_ready`, which
|
|
166
|
+
// returns `Ok` only for `Ready` -- a branch-free `.is_ok()` check.
|
|
167
|
+
let dir = TempDir::new().unwrap();
|
|
168
|
+
// Write a matching file so the extract closure runs during the initial
|
|
169
|
+
// scan (otherwise the closure body would be a dead coverage region).
|
|
170
|
+
std::fs::write(dir.path().join("a.txt"), b"").unwrap();
|
|
171
|
+
let db = DirSQL::with_ignore(
|
|
172
|
+
dir.path(),
|
|
173
|
+
vec![crate::Table::new(
|
|
174
|
+
"CREATE TABLE t (name TEXT)",
|
|
175
|
+
"*.txt",
|
|
176
|
+
|_| {
|
|
177
|
+
vec![crate::Row::from_iter([(
|
|
178
|
+
"name".to_string(),
|
|
179
|
+
crate::Value::Text("x".into()),
|
|
180
|
+
)])]
|
|
181
|
+
},
|
|
182
|
+
)],
|
|
183
|
+
Vec::<String>::new(),
|
|
184
|
+
)
|
|
185
|
+
.unwrap();
|
|
186
|
+
let state: AppState = db.into();
|
|
187
|
+
assert!(
|
|
188
|
+
router::require_ready(&state).is_ok(),
|
|
189
|
+
"From<DirSQL> must produce AppState::Ready",
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
@@ -163,3 +163,51 @@ pub(super) fn error_response(status: StatusCode, message: impl Into<String>) ->
|
|
|
163
163
|
);
|
|
164
164
|
resp
|
|
165
165
|
}
|
|
166
|
+
|
|
167
|
+
#[cfg(test)]
|
|
168
|
+
mod tests {
|
|
169
|
+
use super::*;
|
|
170
|
+
use crate::db::DbError;
|
|
171
|
+
|
|
172
|
+
#[test]
|
|
173
|
+
fn classify_core_error_is_bad_request() {
|
|
174
|
+
// A SQLite-level failure is the client's fault (bad SQL), so it maps
|
|
175
|
+
// to 400 rather than a server error.
|
|
176
|
+
let err = DirSqlError::Core(DbError::SchemaMismatch("nope".into()));
|
|
177
|
+
assert_eq!(classify_query_error(&err), StatusCode::BAD_REQUEST);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
#[test]
|
|
181
|
+
fn classify_non_core_error_is_internal_server_error() {
|
|
182
|
+
// Lock/watch/config failures are server-side faults -> 500. This drives
|
|
183
|
+
// the `_ =>` arm of `classify_query_error`.
|
|
184
|
+
let err = DirSqlError::Lock("poisoned".into());
|
|
185
|
+
assert_eq!(
|
|
186
|
+
classify_query_error(&err),
|
|
187
|
+
StatusCode::INTERNAL_SERVER_ERROR
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
#[test]
|
|
192
|
+
fn require_ready_returns_503_response_when_unavailable() {
|
|
193
|
+
// The degraded state yields an error `Response` (rendered as 503 by
|
|
194
|
+
// the HTTP layer) instead of a `DirSQL` handle.
|
|
195
|
+
let state = AppState::Unavailable("config failed to load".into());
|
|
196
|
+
// `DirSQL` isn't `Debug`, so go through `.err()` (which drops the Ok
|
|
197
|
+
// value) rather than `expect_err`.
|
|
198
|
+
let resp = require_ready(&state)
|
|
199
|
+
.err()
|
|
200
|
+
.expect("Unavailable must not yield a db");
|
|
201
|
+
assert_eq!(resp.status(), StatusCode::SERVICE_UNAVAILABLE);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
#[test]
|
|
205
|
+
fn error_response_sets_json_content_type() {
|
|
206
|
+
let resp = error_response(StatusCode::BAD_REQUEST, "boom");
|
|
207
|
+
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
|
|
208
|
+
assert_eq!(
|
|
209
|
+
resp.headers().get(header::CONTENT_TYPE).unwrap(),
|
|
210
|
+
"application/json"
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
@@ -168,11 +168,42 @@ mod tests {
|
|
|
168
168
|
);
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
+
#[test]
|
|
172
|
+
fn delete_event_emits_expected_shape() {
|
|
173
|
+
let mut row: Row = HashMap::new();
|
|
174
|
+
row.insert("id".into(), CellValue::Text("gone".into()));
|
|
175
|
+
let event = RowEvent::Delete {
|
|
176
|
+
table: "posts".into(),
|
|
177
|
+
row,
|
|
178
|
+
file_path: "posts/a.json".into(),
|
|
179
|
+
};
|
|
180
|
+
let parsed: Value = serde_json::from_str(&event_to_json(&event)).unwrap();
|
|
181
|
+
assert_eq!(parsed.get("action").and_then(Value::as_str), Some("delete"));
|
|
182
|
+
assert_eq!(parsed.get("table").and_then(Value::as_str), Some("posts"));
|
|
183
|
+
assert_eq!(
|
|
184
|
+
parsed.get("file_path").and_then(Value::as_str),
|
|
185
|
+
Some("posts/a.json"),
|
|
186
|
+
);
|
|
187
|
+
assert_eq!(
|
|
188
|
+
parsed.pointer("/row/id").and_then(Value::as_str),
|
|
189
|
+
Some("gone")
|
|
190
|
+
);
|
|
191
|
+
assert!(parsed.get("old_row").unwrap().is_null());
|
|
192
|
+
}
|
|
193
|
+
|
|
171
194
|
#[test]
|
|
172
195
|
fn null_cell_becomes_json_null() {
|
|
173
196
|
assert!(cell_to_json(&CellValue::Null).is_null());
|
|
174
197
|
}
|
|
175
198
|
|
|
199
|
+
#[test]
|
|
200
|
+
fn blob_cell_becomes_hex_string() {
|
|
201
|
+
// The Blob arm of `cell_to_json` hex-encodes the bytes into a JSON
|
|
202
|
+
// string (BLOB columns aren't representable as JSON literals).
|
|
203
|
+
let json = cell_to_json(&CellValue::Blob(vec![0xde, 0xad, 0xbe, 0xef]));
|
|
204
|
+
assert_eq!(json.as_str(), Some("deadbeef"));
|
|
205
|
+
}
|
|
206
|
+
|
|
176
207
|
#[test]
|
|
177
208
|
fn non_finite_real_becomes_json_null() {
|
|
178
209
|
assert!(cell_to_json(&CellValue::Real(f64::NAN)).is_null());
|
|
@@ -192,10 +192,8 @@ ignore = ["*.tmp"]
|
|
|
192
192
|
fn invalid_toml_returns_error() {
|
|
193
193
|
let toml = "this is not valid toml [[[";
|
|
194
194
|
let err = load_config_str(toml).unwrap_err();
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
other => panic!("expected Toml error, got: {}", other),
|
|
198
|
-
}
|
|
195
|
+
// Single-line `matches!` pins the variant without a dead fallback arm.
|
|
196
|
+
assert!(matches!(err, ConfigError::Toml(_)), "got: {err:?}");
|
|
199
197
|
}
|
|
200
198
|
|
|
201
199
|
#[test]
|
|
@@ -229,10 +227,8 @@ glob = "*.csv"
|
|
|
229
227
|
#[test]
|
|
230
228
|
fn load_config_missing_file_returns_io_error() {
|
|
231
229
|
let err = load_config(Path::new("/nonexistent/.dirsql.toml")).unwrap_err();
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
other => panic!("expected Io error, got: {}", other),
|
|
235
|
-
}
|
|
230
|
+
// Single-line `matches!` pins the variant without a dead fallback arm.
|
|
231
|
+
assert!(matches!(err, ConfigError::Io(_)), "got: {err:?}");
|
|
236
232
|
}
|
|
237
233
|
|
|
238
234
|
#[test]
|
|
@@ -538,32 +538,36 @@ mod tests {
|
|
|
538
538
|
fn value_to_sql_null() {
|
|
539
539
|
let v = Value::Null;
|
|
540
540
|
let result = v.to_sql().unwrap();
|
|
541
|
-
|
|
541
|
+
// Branch-free: compare against the expected value (`ToSqlOutput: PartialEq`)
|
|
542
|
+
// rather than `matches!`, whose `_ => false` arm is a dead region here.
|
|
543
|
+
assert_eq!(
|
|
542
544
|
result,
|
|
543
545
|
rusqlite::types::ToSqlOutput::Owned(rusqlite::types::Value::Null)
|
|
544
|
-
)
|
|
546
|
+
);
|
|
545
547
|
}
|
|
546
548
|
|
|
547
549
|
#[test]
|
|
548
550
|
fn value_to_sql_integer() {
|
|
549
551
|
let v = Value::Integer(42);
|
|
550
552
|
let result = v.to_sql().unwrap();
|
|
551
|
-
|
|
553
|
+
// Branch-free: see `value_to_sql_null` -- compare values directly.
|
|
554
|
+
assert_eq!(
|
|
552
555
|
result,
|
|
553
556
|
rusqlite::types::ToSqlOutput::Owned(rusqlite::types::Value::Integer(42))
|
|
554
|
-
)
|
|
557
|
+
);
|
|
555
558
|
}
|
|
556
559
|
|
|
557
560
|
#[test]
|
|
558
561
|
fn value_to_sql_real() {
|
|
559
562
|
let v = Value::Real(1.5);
|
|
560
563
|
let result = v.to_sql().unwrap();
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
564
|
+
// Branch-free: `ToSqlOutput` derives `PartialEq`, so compare against
|
|
565
|
+
// the exact expected value instead of a `match` with a dead `_` arm.
|
|
566
|
+
// 1.5 is exactly representable in f64, so equality is precise here.
|
|
567
|
+
assert_eq!(
|
|
568
|
+
result,
|
|
569
|
+
rusqlite::types::ToSqlOutput::Owned(rusqlite::types::Value::Real(1.5))
|
|
570
|
+
);
|
|
567
571
|
}
|
|
568
572
|
|
|
569
573
|
#[test]
|
|
@@ -789,4 +793,46 @@ mod tests {
|
|
|
789
793
|
let deleted = db.delete_rows_by_file("t", "nonexistent.json").unwrap();
|
|
790
794
|
assert_eq!(deleted, 0);
|
|
791
795
|
}
|
|
796
|
+
|
|
797
|
+
// --- Error path: Db::open on an unopenable path ---
|
|
798
|
+
|
|
799
|
+
#[test]
|
|
800
|
+
fn open_on_unopenable_path_returns_error() {
|
|
801
|
+
// A path inside a directory that does not exist cannot be created by
|
|
802
|
+
// SQLite, so `Connection::open` fails and the `?` propagates the
|
|
803
|
+
// rusqlite error through the `#[from]` conversion. (`Db` is not
|
|
804
|
+
// `Debug`; `.map(|_| ())` discards the Ok payload so `unwrap_err`
|
|
805
|
+
// works without a dead `Ok => panic!` arm that coverage would flag.)
|
|
806
|
+
let err = Db::open(Path::new("/nonexistent-dir-xyz/sub/cache.db"))
|
|
807
|
+
.map(|_| ())
|
|
808
|
+
.unwrap_err();
|
|
809
|
+
assert!(matches!(err, DbError::Sqlite(_)), "got: {err}");
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// --- Error path: create_table with a syntactically-valid-looking DDL
|
|
813
|
+
// (has a closing paren so it passes inject_tracking_columns) but invalid
|
|
814
|
+
// SQL, so the failure happens at conn.execute rather than the injector ---
|
|
815
|
+
|
|
816
|
+
#[test]
|
|
817
|
+
fn create_table_invalid_sql_with_paren_fails_at_execute() {
|
|
818
|
+
let db = Db::new().unwrap();
|
|
819
|
+
let err = db
|
|
820
|
+
.create_table("CREATE TABLE (this is not valid)")
|
|
821
|
+
.unwrap_err();
|
|
822
|
+
assert!(matches!(err, DbError::Sqlite(_)), "got: {err}");
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// --- Error path: normalize_row when the underlying table lookup fails.
|
|
826
|
+
// A table name containing whitespace makes the PRAGMA prepare fail, so
|
|
827
|
+
// get_table_columns errors and normalize_row's `?` propagates. ---
|
|
828
|
+
|
|
829
|
+
#[test]
|
|
830
|
+
fn normalize_row_propagates_column_lookup_error() {
|
|
831
|
+
let db = Db::new().unwrap();
|
|
832
|
+
let row = HashMap::from([("id".into(), Value::Text("1".into()))]);
|
|
833
|
+
let err = db
|
|
834
|
+
.normalize_row("bad name with spaces", &row, false)
|
|
835
|
+
.unwrap_err();
|
|
836
|
+
assert!(matches!(err, DbError::Sqlite(_)), "got: {err}");
|
|
837
|
+
}
|
|
792
838
|
}
|
|
@@ -297,12 +297,16 @@ mod tests {
|
|
|
297
297
|
.collect();
|
|
298
298
|
assert_eq!(deletes.len(), 3);
|
|
299
299
|
assert_eq!(inserts.len(), 1);
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
300
|
+
// Branch-free attribution check: every variant's Debug rendering
|
|
301
|
+
// includes its `file_path` field, so asserting on that string covers
|
|
302
|
+
// all variants without a `match` whose `Update`/`Error` arms this test
|
|
303
|
+
// never reaches (those arms would be dead coverage regions).
|
|
304
|
+
assert!(
|
|
305
|
+
events
|
|
306
|
+
.iter()
|
|
307
|
+
.all(|e| format!("{e:?}").contains("file_path: \"t.jsonl\"")),
|
|
308
|
+
"every event must be attributed to t.jsonl: {events:?}"
|
|
309
|
+
);
|
|
306
310
|
}
|
|
307
311
|
|
|
308
312
|
// --- Full replace on heavy modification ---
|