dirsql 0.3.11__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.
Files changed (131) hide show
  1. {dirsql-0.3.11 → dirsql-0.3.12}/Cargo.lock +1 -1
  2. {dirsql-0.3.11 → dirsql-0.3.12}/PKG-INFO +1 -1
  3. {dirsql-0.3.11 → dirsql-0.3.12}/packages/python/Cargo.toml +1 -1
  4. {dirsql-0.3.11 → dirsql-0.3.12}/packages/rust/src/cli/mod.rs +45 -0
  5. {dirsql-0.3.11 → dirsql-0.3.12}/packages/rust/src/cli/router.rs +48 -0
  6. {dirsql-0.3.11 → dirsql-0.3.12}/packages/rust/src/cli/serialize.rs +31 -0
  7. {dirsql-0.3.11 → dirsql-0.3.12}/packages/rust/src/config.rs +4 -8
  8. {dirsql-0.3.11 → dirsql-0.3.12}/packages/rust/src/db.rs +56 -10
  9. {dirsql-0.3.11 → dirsql-0.3.12}/packages/rust/src/differ.rs +10 -6
  10. {dirsql-0.3.11 → dirsql-0.3.12}/packages/rust/src/lib.rs +513 -40
  11. {dirsql-0.3.11 → dirsql-0.3.12}/packages/rust/src/matcher.rs +65 -12
  12. {dirsql-0.3.11 → dirsql-0.3.12}/packages/rust/src/persist.rs +155 -0
  13. {dirsql-0.3.11 → dirsql-0.3.12}/packages/rust/src/watcher.rs +62 -35
  14. {dirsql-0.3.11 → dirsql-0.3.12}/packages/rust/tests/async_sdk.rs +119 -0
  15. {dirsql-0.3.11 → dirsql-0.3.12}/packages/rust/tests/cli_e2e.rs +3 -5
  16. {dirsql-0.3.11 → dirsql-0.3.12}/packages/rust/tests/cli_integration.rs +128 -5
  17. {dirsql-0.3.11 → dirsql-0.3.12}/packages/rust/tests/from_config.rs +138 -5
  18. {dirsql-0.3.11 → dirsql-0.3.12}/packages/rust/tests/init_integration.rs +1 -1
  19. {dirsql-0.3.11 → dirsql-0.3.12}/packages/rust/tests/persist.rs +39 -3
  20. {dirsql-0.3.11 → dirsql-0.3.12}/packages/rust/tests/sdk.rs +156 -0
  21. {dirsql-0.3.11 → dirsql-0.3.12}/pyproject.toml +15 -8
  22. {dirsql-0.3.11 → dirsql-0.3.12}/Cargo.toml +0 -0
  23. {dirsql-0.3.11 → dirsql-0.3.12}/README.md +0 -0
  24. {dirsql-0.3.11 → dirsql-0.3.12}/dirsql/__init__.py +0 -0
  25. {dirsql-0.3.11 → dirsql-0.3.12}/dirsql/_async.py +0 -0
  26. {dirsql-0.3.11 → dirsql-0.3.12}/dirsql/_cli/__init__.py +0 -0
  27. {dirsql-0.3.11 → dirsql-0.3.12}/dirsql/_cli/binary_path.py +0 -0
  28. {dirsql-0.3.11 → dirsql-0.3.12}/dirsql/_cli/is_windows.py +0 -0
  29. {dirsql-0.3.11 → dirsql-0.3.12}/dirsql/_cli/main.py +0 -0
  30. {dirsql-0.3.11 → dirsql-0.3.12}/dirsql/test_async.py +0 -0
  31. {dirsql-0.3.11 → dirsql-0.3.12}/docs/.claude/CLAUDE.md +0 -0
  32. {dirsql-0.3.11 → dirsql-0.3.12}/docs/.vitepress/config.ts +0 -0
  33. {dirsql-0.3.11 → dirsql-0.3.12}/docs/.vitepress/theme/index.ts +0 -0
  34. {dirsql-0.3.11 → dirsql-0.3.12}/docs/.vitepress/theme/lang.ts +0 -0
  35. {dirsql-0.3.11 → dirsql-0.3.12}/docs/AGENTS.md +0 -0
  36. {dirsql-0.3.11 → dirsql-0.3.12}/docs/api/index.md +0 -0
  37. {dirsql-0.3.11 → dirsql-0.3.12}/docs/cli/config.md +0 -0
  38. {dirsql-0.3.11 → dirsql-0.3.12}/docs/cli/http-api.md +0 -0
  39. {dirsql-0.3.11 → dirsql-0.3.12}/docs/cli/index.md +0 -0
  40. {dirsql-0.3.11 → dirsql-0.3.12}/docs/cli/init.md +0 -0
  41. {dirsql-0.3.11 → dirsql-0.3.12}/docs/cli/server.md +0 -0
  42. {dirsql-0.3.11 → dirsql-0.3.12}/docs/getting-started.md +0 -0
  43. {dirsql-0.3.11 → dirsql-0.3.12}/docs/guide/async.md +0 -0
  44. {dirsql-0.3.11 → dirsql-0.3.12}/docs/guide/crdt.md +0 -0
  45. {dirsql-0.3.11 → dirsql-0.3.12}/docs/guide/persistence.md +0 -0
  46. {dirsql-0.3.11 → dirsql-0.3.12}/docs/guide/querying.md +0 -0
  47. {dirsql-0.3.11 → dirsql-0.3.12}/docs/guide/tables.md +0 -0
  48. {dirsql-0.3.11 → dirsql-0.3.12}/docs/guide/watching.md +0 -0
  49. {dirsql-0.3.11 → dirsql-0.3.12}/docs/index.md +0 -0
  50. {dirsql-0.3.11 → dirsql-0.3.12}/docs/migrations.md +0 -0
  51. {dirsql-0.3.11 → dirsql-0.3.12}/docs/package.json +0 -0
  52. {dirsql-0.3.11 → dirsql-0.3.12}/docs/playwright.config.ts +0 -0
  53. {dirsql-0.3.11 → dirsql-0.3.12}/docs/pnpm-lock.yaml +0 -0
  54. {dirsql-0.3.11 → dirsql-0.3.12}/docs/pnpm-workspace.yaml +0 -0
  55. {dirsql-0.3.11 → dirsql-0.3.12}/docs/tests/integration/home.spec.ts +0 -0
  56. {dirsql-0.3.11 → dirsql-0.3.12}/docs/tests/integration/language-flag.spec.ts +0 -0
  57. {dirsql-0.3.11 → dirsql-0.3.12}/docs/tests/unit/config.test.ts +0 -0
  58. {dirsql-0.3.11 → dirsql-0.3.12}/docs/tests/unit/lang.test.ts +0 -0
  59. {dirsql-0.3.11 → dirsql-0.3.12}/docs/vitest.config.ts +0 -0
  60. {dirsql-0.3.11 → dirsql-0.3.12}/packages/python/README.md +0 -0
  61. {dirsql-0.3.11 → dirsql-0.3.12}/packages/python/conftest.py +0 -0
  62. {dirsql-0.3.11 → dirsql-0.3.12}/packages/python/docs/.claude/CLAUDE.md +0 -0
  63. {dirsql-0.3.11 → dirsql-0.3.12}/packages/python/docs/.vitepress/config.ts +0 -0
  64. {dirsql-0.3.11 → dirsql-0.3.12}/packages/python/docs/.vitepress/theme/index.ts +0 -0
  65. {dirsql-0.3.11 → dirsql-0.3.12}/packages/python/docs/.vitepress/theme/lang.ts +0 -0
  66. {dirsql-0.3.11 → dirsql-0.3.12}/packages/python/docs/AGENTS.md +0 -0
  67. {dirsql-0.3.11 → dirsql-0.3.12}/packages/python/docs/api/index.md +0 -0
  68. {dirsql-0.3.11 → dirsql-0.3.12}/packages/python/docs/cli/config.md +0 -0
  69. {dirsql-0.3.11 → dirsql-0.3.12}/packages/python/docs/cli/http-api.md +0 -0
  70. {dirsql-0.3.11 → dirsql-0.3.12}/packages/python/docs/cli/index.md +0 -0
  71. {dirsql-0.3.11 → dirsql-0.3.12}/packages/python/docs/cli/init.md +0 -0
  72. {dirsql-0.3.11 → dirsql-0.3.12}/packages/python/docs/cli/server.md +0 -0
  73. {dirsql-0.3.11 → dirsql-0.3.12}/packages/python/docs/getting-started.md +0 -0
  74. {dirsql-0.3.11 → dirsql-0.3.12}/packages/python/docs/guide/async.md +0 -0
  75. {dirsql-0.3.11 → dirsql-0.3.12}/packages/python/docs/guide/crdt.md +0 -0
  76. {dirsql-0.3.11 → dirsql-0.3.12}/packages/python/docs/guide/persistence.md +0 -0
  77. {dirsql-0.3.11 → dirsql-0.3.12}/packages/python/docs/guide/querying.md +0 -0
  78. {dirsql-0.3.11 → dirsql-0.3.12}/packages/python/docs/guide/tables.md +0 -0
  79. {dirsql-0.3.11 → dirsql-0.3.12}/packages/python/docs/guide/watching.md +0 -0
  80. {dirsql-0.3.11 → dirsql-0.3.12}/packages/python/docs/index.md +0 -0
  81. {dirsql-0.3.11 → dirsql-0.3.12}/packages/python/docs/migrations.md +0 -0
  82. {dirsql-0.3.11 → dirsql-0.3.12}/packages/python/docs/package.json +0 -0
  83. {dirsql-0.3.11 → dirsql-0.3.12}/packages/python/docs/playwright.config.ts +0 -0
  84. {dirsql-0.3.11 → dirsql-0.3.12}/packages/python/docs/pnpm-lock.yaml +0 -0
  85. {dirsql-0.3.11 → dirsql-0.3.12}/packages/python/docs/pnpm-workspace.yaml +0 -0
  86. {dirsql-0.3.11 → dirsql-0.3.12}/packages/python/docs/tests/integration/home.spec.ts +0 -0
  87. {dirsql-0.3.11 → dirsql-0.3.12}/packages/python/docs/tests/integration/language-flag.spec.ts +0 -0
  88. {dirsql-0.3.11 → dirsql-0.3.12}/packages/python/docs/tests/unit/config.test.ts +0 -0
  89. {dirsql-0.3.11 → dirsql-0.3.12}/packages/python/docs/tests/unit/lang.test.ts +0 -0
  90. {dirsql-0.3.11 → dirsql-0.3.12}/packages/python/docs/vitest.config.ts +0 -0
  91. {dirsql-0.3.11 → dirsql-0.3.12}/packages/python/src/lib.rs +0 -0
  92. {dirsql-0.3.11 → dirsql-0.3.12}/packages/python/tests/__init__.py +0 -0
  93. {dirsql-0.3.11 → dirsql-0.3.12}/packages/python/tests/conftest.py +0 -0
  94. {dirsql-0.3.11 → dirsql-0.3.12}/packages/python/tests/e2e/__init__.py +0 -0
  95. {dirsql-0.3.11 → dirsql-0.3.12}/packages/python/tests/integration/__init__.py +0 -0
  96. {dirsql-0.3.11 → dirsql-0.3.12}/packages/python/tests/integration/test_async_dirsql.py +0 -0
  97. {dirsql-0.3.11 → dirsql-0.3.12}/packages/python/tests/integration/test_binding.py +0 -0
  98. {dirsql-0.3.11 → dirsql-0.3.12}/packages/python/tests/integration/test_dirsql.py +0 -0
  99. {dirsql-0.3.11 → dirsql-0.3.12}/packages/python/tests/integration/test_docs_examples.py +0 -0
  100. {dirsql-0.3.11 → dirsql-0.3.12}/packages/python/tests/integration/test_docs_gaps.py +0 -0
  101. {dirsql-0.3.11 → dirsql-0.3.12}/packages/python/tests/integration/test_from_config.py +0 -0
  102. {dirsql-0.3.11 → dirsql-0.3.12}/packages/python/tests/integration/test_persist.py +0 -0
  103. {dirsql-0.3.11 → dirsql-0.3.12}/packages/rust/Cargo.toml +0 -0
  104. {dirsql-0.3.11 → dirsql-0.3.12}/packages/rust/README.md +0 -0
  105. {dirsql-0.3.11 → dirsql-0.3.12}/packages/rust/benches/db_bench.rs +0 -0
  106. {dirsql-0.3.11 → dirsql-0.3.12}/packages/rust/benches/differ_bench.rs +0 -0
  107. {dirsql-0.3.11 → dirsql-0.3.12}/packages/rust/benches/matcher_bench.rs +0 -0
  108. {dirsql-0.3.11 → dirsql-0.3.12}/packages/rust/benches/scanner_bench.rs +0 -0
  109. {dirsql-0.3.11 → dirsql-0.3.12}/packages/rust/docs/api/index.md +0 -0
  110. {dirsql-0.3.11 → dirsql-0.3.12}/packages/rust/docs/cli/config.md +0 -0
  111. {dirsql-0.3.11 → dirsql-0.3.12}/packages/rust/docs/cli/http-api.md +0 -0
  112. {dirsql-0.3.11 → dirsql-0.3.12}/packages/rust/docs/cli/index.md +0 -0
  113. {dirsql-0.3.11 → dirsql-0.3.12}/packages/rust/docs/cli/init.md +0 -0
  114. {dirsql-0.3.11 → dirsql-0.3.12}/packages/rust/docs/cli/server.md +0 -0
  115. {dirsql-0.3.11 → dirsql-0.3.12}/packages/rust/docs/getting-started.md +0 -0
  116. {dirsql-0.3.11 → dirsql-0.3.12}/packages/rust/docs/guide/async.md +0 -0
  117. {dirsql-0.3.11 → dirsql-0.3.12}/packages/rust/docs/guide/crdt.md +0 -0
  118. {dirsql-0.3.11 → dirsql-0.3.12}/packages/rust/docs/guide/persistence.md +0 -0
  119. {dirsql-0.3.11 → dirsql-0.3.12}/packages/rust/docs/guide/querying.md +0 -0
  120. {dirsql-0.3.11 → dirsql-0.3.12}/packages/rust/docs/guide/tables.md +0 -0
  121. {dirsql-0.3.11 → dirsql-0.3.12}/packages/rust/docs/guide/watching.md +0 -0
  122. {dirsql-0.3.11 → dirsql-0.3.12}/packages/rust/docs/index.md +0 -0
  123. {dirsql-0.3.11 → dirsql-0.3.12}/packages/rust/docs/migrations.md +0 -0
  124. {dirsql-0.3.11 → dirsql-0.3.12}/packages/rust/src/bin/dirsql.rs +0 -0
  125. {dirsql-0.3.11 → dirsql-0.3.12}/packages/rust/src/cli/init.rs +0 -0
  126. {dirsql-0.3.11 → dirsql-0.3.12}/packages/rust/src/cli/server.rs +0 -0
  127. {dirsql-0.3.11 → dirsql-0.3.12}/packages/rust/src/scanner.rs +0 -0
  128. {dirsql-0.3.11 → dirsql-0.3.12}/packages/rust/tests/docs_examples.rs +0 -0
  129. {dirsql-0.3.11 → dirsql-0.3.12}/packages/rust/tests/docs_gaps.rs +0 -0
  130. {dirsql-0.3.11 → dirsql-0.3.12}/packages/rust/tests/init_e2e.rs +0 -0
  131. {dirsql-0.3.11 → dirsql-0.3.12}/packages/rust/tests/readonly_query.rs +0 -0
@@ -499,7 +499,7 @@ dependencies = [
499
499
 
500
500
  [[package]]
501
501
  name = "dirsql-py-ext"
502
- version = "0.3.11"
502
+ version = "0.3.12"
503
503
  dependencies = [
504
504
  "dirsql",
505
505
  "pyo3",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dirsql
3
- Version: 0.3.11
3
+ Version: 0.3.12
4
4
  Requires-Dist: pytest>=8 ; extra == 'dev'
5
5
  Requires-Dist: pytest-describe>=2 ; extra == 'dev'
6
6
  Requires-Dist: pytest-asyncio>=0.23 ; extra == 'dev'
@@ -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.11"
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
- match err {
196
- ConfigError::Toml(_) => {}
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
- match err {
233
- ConfigError::Io(_) => {}
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
- assert!(matches!(
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
- assert!(matches!(
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
- match result {
562
- rusqlite::types::ToSqlOutput::Owned(rusqlite::types::Value::Real(f)) => {
563
- assert!((f - 1.5).abs() < f64::EPSILON);
564
- }
565
- _ => panic!("expected Real"),
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
- assert!(events.iter().all(|e| match e {
301
- RowEvent::Insert { file_path, .. }
302
- | RowEvent::Update { file_path, .. }
303
- | RowEvent::Delete { file_path, .. } => file_path == "t.jsonl",
304
- RowEvent::Error { .. } => false,
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 ---