dirsql 0.3.61__tar.gz → 0.3.62__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.61 → dirsql-0.3.62}/Cargo.lock +1 -1
- {dirsql-0.3.61 → dirsql-0.3.62}/PKG-INFO +1 -1
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/python/Cargo.toml +1 -1
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/python/e2e-attestation.json +2 -2
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/rust/src/db.rs +122 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/rust/src/lib.rs +110 -106
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/rust/src/persist.rs +0 -145
- {dirsql-0.3.61 → dirsql-0.3.62}/Cargo.toml +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/README.md +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/dirsql/__init__.py +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/dirsql/_async.py +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/dirsql/_dirsql.pyi +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/dirsql/cli/__init__.py +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/dirsql/cli/binary_path.py +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/dirsql/cli/interpret/__init__.py +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/dirsql/cli/is_windows.py +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/dirsql/cli/main.py +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/dirsql/cli/resolve_config_extensions.py +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/dirsql/py.typed +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/dirsql/resolve_config_extensions.py +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/dirsql/resolve_extension.py +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/docs/.claude/CLAUDE.md +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/docs/.vitepress/config.ts +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/docs/.vitepress/theme/index.ts +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/docs/.vitepress/theme/lang.ts +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/docs/AGENTS.md +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/docs/explanation.md +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/docs/getting-started.md +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/docs/howto/columns-from-paths.md +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/docs/howto/define-tables.md +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/docs/howto/embed.md +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/docs/howto/extract-from-contents.md +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/docs/howto/load-extension.md +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/docs/howto/persist.md +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/docs/howto/react-to-changes.md +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/docs/howto/search-by-meaning.md +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/docs/howto/skip-files.md +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/docs/index.md +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/docs/migrations.md +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/docs/package.json +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/docs/playwright.config.ts +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/docs/pnpm-lock.yaml +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/docs/pnpm-workspace.yaml +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/docs/reference/cli.md +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/docs/reference/columns.md +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/docs/reference/config.md +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/docs/reference/hooks.md +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/docs/reference/http-api.md +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/docs/reference/sdk.md +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/docs/tests/integration/home.spec.ts +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/docs/tests/integration/language-flag.spec.ts +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/docs/tests/integration/sidebar.spec.ts +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/docs/tests/unit/config.test.ts +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/docs/tests/unit/lang.test.ts +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/docs/vitest.config.ts +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/python/README.md +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/python/conftest.py +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/python/docs/.claude/CLAUDE.md +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/python/docs/.vitepress/config.ts +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/python/docs/.vitepress/theme/index.ts +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/python/docs/.vitepress/theme/lang.ts +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/python/docs/AGENTS.md +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/python/docs/explanation.md +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/python/docs/getting-started.md +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/python/docs/howto/columns-from-paths.md +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/python/docs/howto/define-tables.md +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/python/docs/howto/embed.md +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/python/docs/howto/extract-from-contents.md +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/python/docs/howto/load-extension.md +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/python/docs/howto/persist.md +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/python/docs/howto/react-to-changes.md +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/python/docs/howto/search-by-meaning.md +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/python/docs/howto/skip-files.md +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/python/docs/index.md +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/python/docs/migrations.md +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/python/docs/package.json +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/python/docs/playwright.config.ts +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/python/docs/pnpm-lock.yaml +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/python/docs/pnpm-workspace.yaml +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/python/docs/reference/cli.md +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/python/docs/reference/columns.md +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/python/docs/reference/config.md +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/python/docs/reference/hooks.md +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/python/docs/reference/http-api.md +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/python/docs/reference/sdk.md +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/python/docs/tests/integration/home.spec.ts +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/python/docs/tests/integration/language-flag.spec.ts +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/python/docs/tests/integration/sidebar.spec.ts +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/python/docs/tests/unit/config.test.ts +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/python/docs/tests/unit/lang.test.ts +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/python/docs/vitest.config.ts +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/python/src/lib.rs +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/python/tests/__init__.py +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/python/tests/binding/__init__.py +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/python/tests/conftest.py +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/python/tests/e2e/__init__.py +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/python/tests/integration/__init__.py +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/python/tests/smoke/__init__.py +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/rust/Cargo.toml +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/rust/README.md +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/rust/benches/db_bench.rs +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/rust/benches/differ_bench.rs +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/rust/benches/matcher_bench.rs +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/rust/benches/scanner_bench.rs +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/rust/docs/explanation.md +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/rust/docs/getting-started.md +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/rust/docs/howto/columns-from-paths.md +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/rust/docs/howto/define-tables.md +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/rust/docs/howto/embed.md +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/rust/docs/howto/extract-from-contents.md +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/rust/docs/howto/load-extension.md +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/rust/docs/howto/persist.md +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/rust/docs/howto/react-to-changes.md +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/rust/docs/howto/search-by-meaning.md +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/rust/docs/howto/skip-files.md +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/rust/docs/index.md +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/rust/docs/migrations.md +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/rust/docs/reference/cli.md +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/rust/docs/reference/columns.md +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/rust/docs/reference/config.md +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/rust/docs/reference/hooks.md +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/rust/docs/reference/http-api.md +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/rust/docs/reference/sdk.md +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/rust/src/bin/dirsql.rs +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/rust/src/cli/init.rs +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/rust/src/cli/mod.rs +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/rust/src/cli/router.rs +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/rust/src/cli/serialize.rs +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/rust/src/cli/server.rs +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/rust/src/command.rs +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/rust/src/config.rs +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/rust/src/differ.rs +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/rust/src/matcher.rs +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/rust/src/scanner.rs +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/packages/rust/src/watcher.rs +0 -0
- {dirsql-0.3.61 → dirsql-0.3.62}/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.62"
|
|
8
8
|
edition.workspace = true
|
|
9
9
|
publish = false
|
|
10
10
|
readme = "README.md"
|
|
@@ -312,6 +312,56 @@ impl Db {
|
|
|
312
312
|
Ok(())
|
|
313
313
|
}
|
|
314
314
|
|
|
315
|
+
/// Read back the rows a given file produced for `table`, ordered by row
|
|
316
|
+
/// index. Ownership and ordering come from the `_dirsql_internal_rows`
|
|
317
|
+
/// mapping (joined on `rowid`); user columns are qualified with the table
|
|
318
|
+
/// alias so a user column named like a mapping column stays unambiguous.
|
|
319
|
+
///
|
|
320
|
+
/// This is the watcher's diffing source (#401): `handle_upsert` /
|
|
321
|
+
/// `handle_delete` snapshot a file's previous rows here instead of holding
|
|
322
|
+
/// a corpus-wide in-memory copy. A row read back compares equal to the
|
|
323
|
+
/// normalized row that was inserted as long as the extract's value types
|
|
324
|
+
/// match the declared column affinities (SQLite coerces on insert
|
|
325
|
+
/// otherwise, e.g. `Integer(5)` into a TEXT column comes back
|
|
326
|
+
/// `Text("5")`).
|
|
327
|
+
pub fn get_rows_by_file(
|
|
328
|
+
&self,
|
|
329
|
+
table: &str,
|
|
330
|
+
file_path: &str,
|
|
331
|
+
) -> Result<Vec<HashMap<String, Value>>> {
|
|
332
|
+
let user_columns = self.get_table_columns(table)?;
|
|
333
|
+
let mut col_list = user_columns
|
|
334
|
+
.iter()
|
|
335
|
+
.map(|c| format!("t.\"{c}\""))
|
|
336
|
+
.collect::<Vec<_>>()
|
|
337
|
+
.join(", ");
|
|
338
|
+
if col_list.is_empty() {
|
|
339
|
+
// No columns can only happen for a nonexistent table (SQLite
|
|
340
|
+
// requires ≥1 declared column); keep the SELECT list valid and
|
|
341
|
+
// let SQLite report "no such table".
|
|
342
|
+
col_list = "1".to_string();
|
|
343
|
+
}
|
|
344
|
+
let sql = format!(
|
|
345
|
+
"SELECT {col_list} FROM \"{table}\" AS t \
|
|
346
|
+
JOIN _dirsql_internal_rows AS m ON m.rowid_ref = t.rowid \
|
|
347
|
+
WHERE m.table_name = ?1 AND m.file_path = ?2 ORDER BY m.row_index"
|
|
348
|
+
);
|
|
349
|
+
let mut stmt = self.conn.prepare(&sql)?;
|
|
350
|
+
let rows = stmt.query_map(rusqlite::params![table, file_path], |row| {
|
|
351
|
+
let mut map = HashMap::new();
|
|
352
|
+
for (i, name) in user_columns.iter().enumerate() {
|
|
353
|
+
let v: rusqlite::types::Value = row.get(i)?;
|
|
354
|
+
map.insert(name.clone(), Value::from(v));
|
|
355
|
+
}
|
|
356
|
+
Ok(map)
|
|
357
|
+
})?;
|
|
358
|
+
let mut out = Vec::new();
|
|
359
|
+
for row in rows {
|
|
360
|
+
out.push(row?);
|
|
361
|
+
}
|
|
362
|
+
Ok(out)
|
|
363
|
+
}
|
|
364
|
+
|
|
315
365
|
/// Delete all rows that were produced by a given file path.
|
|
316
366
|
///
|
|
317
367
|
/// The user-row deletes and the matching `_dirsql_internal_rows` mapping
|
|
@@ -1206,6 +1256,78 @@ mod tests {
|
|
|
1206
1256
|
assert_eq!(deleted, 0);
|
|
1207
1257
|
}
|
|
1208
1258
|
|
|
1259
|
+
// --- get_rows_by_file: the watcher's diffing source (#401) ---
|
|
1260
|
+
|
|
1261
|
+
#[test]
|
|
1262
|
+
fn get_rows_by_file_returns_rows_in_row_index_order() {
|
|
1263
|
+
let db = Db::new().unwrap();
|
|
1264
|
+
db.create_table("CREATE TABLE t (id TEXT)").unwrap();
|
|
1265
|
+
// Insert with row indices out of insertion order; the read must
|
|
1266
|
+
// follow row_index, not rowid / insertion order.
|
|
1267
|
+
let second = HashMap::from([("id".into(), Value::Text("second".into()))]);
|
|
1268
|
+
let first = HashMap::from([("id".into(), Value::Text("first".into()))]);
|
|
1269
|
+
db.insert_row("t", &second, "a.json", 1).unwrap();
|
|
1270
|
+
db.insert_row("t", &first, "a.json", 0).unwrap();
|
|
1271
|
+
|
|
1272
|
+
let rows = db.get_rows_by_file("t", "a.json").unwrap();
|
|
1273
|
+
assert_eq!(rows.len(), 2);
|
|
1274
|
+
assert_eq!(rows[0]["id"], Value::Text("first".into()));
|
|
1275
|
+
assert_eq!(rows[1]["id"], Value::Text("second".into()));
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
#[test]
|
|
1279
|
+
fn get_rows_by_file_scopes_by_file_and_table() {
|
|
1280
|
+
let db = Db::new().unwrap();
|
|
1281
|
+
db.create_table("CREATE TABLE t (id TEXT)").unwrap();
|
|
1282
|
+
db.create_table("CREATE TABLE u (id TEXT)").unwrap();
|
|
1283
|
+
let t_row = HashMap::from([("id".into(), Value::Text("t-row".into()))]);
|
|
1284
|
+
let u_row = HashMap::from([("id".into(), Value::Text("u-row".into()))]);
|
|
1285
|
+
let other = HashMap::from([("id".into(), Value::Text("other-file".into()))]);
|
|
1286
|
+
db.insert_row("t", &t_row, "a.json", 0).unwrap();
|
|
1287
|
+
db.insert_row("u", &u_row, "a.json", 0).unwrap();
|
|
1288
|
+
db.insert_row("t", &other, "b.json", 0).unwrap();
|
|
1289
|
+
|
|
1290
|
+
let rows = db.get_rows_by_file("t", "a.json").unwrap();
|
|
1291
|
+
assert_eq!(rows.len(), 1);
|
|
1292
|
+
assert_eq!(rows[0]["id"], Value::Text("t-row".into()));
|
|
1293
|
+
|
|
1294
|
+
// A file with no rows in this table reads back empty.
|
|
1295
|
+
let none = db.get_rows_by_file("u", "b.json").unwrap();
|
|
1296
|
+
assert!(none.is_empty());
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
#[test]
|
|
1300
|
+
fn get_rows_by_file_round_trips_all_value_variants() {
|
|
1301
|
+
// The watcher's positional diff compares a read-back row against a
|
|
1302
|
+
// freshly normalized one, so every Value variant must survive
|
|
1303
|
+
// insert -> select unchanged when the declared column affinity
|
|
1304
|
+
// matches the value type.
|
|
1305
|
+
let db = Db::new().unwrap();
|
|
1306
|
+
db.create_table("CREATE TABLE t (i INTEGER, r REAL, s TEXT, b BLOB, n TEXT)")
|
|
1307
|
+
.unwrap();
|
|
1308
|
+
let row = HashMap::from([
|
|
1309
|
+
("i".to_string(), Value::Integer(42)),
|
|
1310
|
+
("r".to_string(), Value::Real(1.5)),
|
|
1311
|
+
("s".to_string(), Value::Text("hello".into())),
|
|
1312
|
+
("b".to_string(), Value::Blob(vec![0, 1, 2])),
|
|
1313
|
+
("n".to_string(), Value::Null),
|
|
1314
|
+
]);
|
|
1315
|
+
db.insert_row("t", &row, "a.json", 0).unwrap();
|
|
1316
|
+
|
|
1317
|
+
let rows = db.get_rows_by_file("t", "a.json").unwrap();
|
|
1318
|
+
assert_eq!(rows.len(), 1);
|
|
1319
|
+
assert_eq!(rows[0], row);
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
#[test]
|
|
1323
|
+
fn get_rows_by_file_missing_table_returns_error() {
|
|
1324
|
+
// A nonexistent table has no columns, so the SELECT list falls back
|
|
1325
|
+
// to `1` and SQLite reports the missing table.
|
|
1326
|
+
let db = Db::new().unwrap();
|
|
1327
|
+
let err = db.get_rows_by_file("ghost", "a.json").unwrap_err();
|
|
1328
|
+
assert!(err.to_string().contains("no such table"), "got: {err}");
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1209
1331
|
// --- Error path: Db::open on an unopenable path ---
|
|
1210
1332
|
|
|
1211
1333
|
#[test]
|
|
@@ -33,8 +33,8 @@ use crate::matcher::TableMatcher;
|
|
|
33
33
|
use crate::persist::{
|
|
34
34
|
CachedFile, FileStat, build_meta, canonical_root, compute_glob_config_hash,
|
|
35
35
|
create_sidecar_tables, delete_file as cache_delete_file, drop_user_tables, ensure_parent_dir,
|
|
36
|
-
hash_file, meta_is_compatible, now_ns, read_cached_files, read_meta,
|
|
37
|
-
|
|
36
|
+
hash_file, meta_is_compatible, now_ns, read_cached_files, read_meta, resolve_persist_path,
|
|
37
|
+
upsert_file, write_meta,
|
|
38
38
|
};
|
|
39
39
|
use crate::scanner::scan_directory;
|
|
40
40
|
use crate::watcher::{FileEvent, Watcher};
|
|
@@ -249,8 +249,6 @@ struct DirSqlInner {
|
|
|
249
249
|
extract_map: HashMap<String, Arc<ExtractFn>>,
|
|
250
250
|
/// Table name -> strict flag, resolved once.
|
|
251
251
|
strict_map: HashMap<String, bool>,
|
|
252
|
-
/// Cached rows per file path for positional diffing on modify/delete.
|
|
253
|
-
file_rows: Mutex<HashMap<String, (String, Vec<Row>)>>,
|
|
254
252
|
/// Lazily-created filesystem watcher, shared by both the polling API
|
|
255
253
|
/// ([`DirSQL::poll_events`]) and the channel-based API ([`DirSQL::watch`]).
|
|
256
254
|
watcher: Mutex<Option<Watcher>>,
|
|
@@ -502,23 +500,26 @@ impl DirSQL {
|
|
|
502
500
|
}
|
|
503
501
|
|
|
504
502
|
fn handle_delete(&self, table: &str, rel_path: &str) -> Vec<RowEvent> {
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
503
|
+
// Snapshot the file's rows from the row store before deleting them —
|
|
504
|
+
// the Delete events carry the old-row payloads. Read and delete under
|
|
505
|
+
// one lock acquisition so a concurrent event for the same file can't
|
|
506
|
+
// interleave between them.
|
|
507
|
+
let old_rows = {
|
|
508
|
+
let db = match self.inner.db.lock() {
|
|
509
|
+
Ok(db) => db,
|
|
510
|
+
Err(e) => return vec![error_event(Some(table), rel_path, e.to_string())],
|
|
511
|
+
};
|
|
512
|
+
let old_rows = match db.get_rows_by_file(table, rel_path) {
|
|
513
|
+
Ok(rows) => rows,
|
|
514
|
+
Err(e) => return vec![error_event(Some(table), rel_path, e.to_string())],
|
|
515
|
+
};
|
|
516
|
+
if let Err(e) = db.delete_rows_by_file(table, rel_path) {
|
|
517
|
+
return vec![error_event(Some(table), rel_path, e.to_string())];
|
|
518
|
+
}
|
|
519
|
+
old_rows
|
|
515
520
|
};
|
|
516
521
|
|
|
517
|
-
|
|
518
|
-
return vec![error_event(Some(table), rel_path, e.to_string())];
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
row_events
|
|
522
|
+
differ::diff(table, Some(&old_rows), None, rel_path)
|
|
522
523
|
}
|
|
523
524
|
|
|
524
525
|
fn handle_upsert(&self, table: &str, abs_path: &Path, rel_path: &str) -> Vec<RowEvent> {
|
|
@@ -549,7 +550,11 @@ impl DirSQL {
|
|
|
549
550
|
|
|
550
551
|
let strict = *self.inner.strict_map.get(table).unwrap_or(&false);
|
|
551
552
|
|
|
552
|
-
|
|
553
|
+
// Normalize the new rows, snapshot the file's previous rows from the
|
|
554
|
+
// row store, and apply the delete+insert — all under one lock
|
|
555
|
+
// acquisition, so a concurrent event for the same file can't
|
|
556
|
+
// interleave between the read and the write.
|
|
557
|
+
let (old_rows, new_rows) = {
|
|
553
558
|
let db = match self.inner.db.lock() {
|
|
554
559
|
Ok(g) => g,
|
|
555
560
|
Err(e) => return vec![error_event(Some(table), rel_path, e.to_string())],
|
|
@@ -559,42 +564,33 @@ impl DirSQL {
|
|
|
559
564
|
Err(e) => return vec![error_event(Some(table), rel_path, e.to_string())],
|
|
560
565
|
};
|
|
561
566
|
let raw_rows = merge_filesystem_facts(raw_rows, &captures, &stat, &declared_columns);
|
|
562
|
-
let mut
|
|
567
|
+
let mut new_rows = Vec::with_capacity(raw_rows.len());
|
|
563
568
|
for raw in &raw_rows {
|
|
564
569
|
match db.normalize_row(table, raw, strict) {
|
|
565
|
-
Ok(row) =>
|
|
570
|
+
Ok(row) => new_rows.push(row),
|
|
566
571
|
Err(e) => return vec![error_event(Some(table), rel_path, e.to_string())],
|
|
567
572
|
}
|
|
568
573
|
}
|
|
569
|
-
normalized
|
|
570
|
-
};
|
|
571
574
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
let row_events = differ::diff(table, old_rows.as_deref(), Some(&new_rows), rel_path);
|
|
575
|
+
let old_rows = match db.get_rows_by_file(table, rel_path) {
|
|
576
|
+
Ok(rows) => rows,
|
|
577
|
+
Err(e) => return vec![error_event(Some(table), rel_path, e.to_string())],
|
|
578
|
+
};
|
|
578
579
|
|
|
579
|
-
|
|
580
|
-
Ok(db) => db.delete_rows_by_file(table, rel_path).and_then(|_| {
|
|
580
|
+
let db_result = db.delete_rows_by_file(table, rel_path).and_then(|_| {
|
|
581
581
|
for (i, row) in new_rows.iter().enumerate() {
|
|
582
582
|
db.insert_row(table, row, rel_path, i)?;
|
|
583
583
|
}
|
|
584
584
|
Ok(())
|
|
585
|
-
})
|
|
586
|
-
Err(e)
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
if let Err(e) = db_result {
|
|
590
|
-
return vec![error_event(Some(table), rel_path, e.to_string())];
|
|
591
|
-
}
|
|
585
|
+
});
|
|
586
|
+
if let Err(e) = db_result {
|
|
587
|
+
return vec![error_event(Some(table), rel_path, e.to_string())];
|
|
588
|
+
}
|
|
592
589
|
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
}
|
|
590
|
+
(old_rows, new_rows)
|
|
591
|
+
};
|
|
596
592
|
|
|
597
|
-
|
|
593
|
+
differ::diff(table, Some(&old_rows), Some(&new_rows), rel_path)
|
|
598
594
|
}
|
|
599
595
|
|
|
600
596
|
pub(crate) fn build_from_resolved(resolved: ResolvedBuild) -> Result<Self> {
|
|
@@ -674,7 +670,7 @@ impl DirSQL {
|
|
|
674
670
|
// Build the list of files needing re-parse. When persist is
|
|
675
671
|
// enabled, files whose stat tuple matches the cache (and that pass
|
|
676
672
|
// the racy-window check) are trusted instead of re-parsed.
|
|
677
|
-
let (scanned_files,
|
|
673
|
+
let (scanned_files, _trusted, deleted) = match &persist_ctx {
|
|
678
674
|
None => {
|
|
679
675
|
let mut files = Vec::with_capacity(scanned.len());
|
|
680
676
|
for (path, table_name) in scanned {
|
|
@@ -699,10 +695,8 @@ impl DirSQL {
|
|
|
699
695
|
scanned_files,
|
|
700
696
|
persist: persist_ctx.map(|ctx| PreparedPersist {
|
|
701
697
|
db: ctx.db,
|
|
702
|
-
trusted,
|
|
703
698
|
deleted,
|
|
704
699
|
meta: ctx.expected_meta,
|
|
705
|
-
cold_rebuild: ctx.cold_rebuild,
|
|
706
700
|
}),
|
|
707
701
|
poll_interval,
|
|
708
702
|
})
|
|
@@ -740,7 +734,7 @@ impl DirSQL {
|
|
|
740
734
|
} = prepared;
|
|
741
735
|
|
|
742
736
|
let (db, persist_ready) = match persist {
|
|
743
|
-
Some(p) => (p.db, Some((p.
|
|
737
|
+
Some(p) => (p.db, Some((p.deleted, p.meta))),
|
|
744
738
|
None => (Db::new()?, None),
|
|
745
739
|
};
|
|
746
740
|
|
|
@@ -776,19 +770,11 @@ impl DirSQL {
|
|
|
776
770
|
ddl_map.insert(table_name, table.ddl);
|
|
777
771
|
}
|
|
778
772
|
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
//
|
|
782
|
-
//
|
|
783
|
-
if let Some((
|
|
784
|
-
for tf in trusted {
|
|
785
|
-
let user_columns = db.get_table_columns(&tf.table_name).map_err(map_db_error)?;
|
|
786
|
-
let rows =
|
|
787
|
-
read_rows_for_file(db.conn(), &tf.table_name, &tf.rel_path, &user_columns)
|
|
788
|
-
.map_err(DirSqlError::sqlite)?;
|
|
789
|
-
file_rows.insert(tf.rel_path.clone(), (tf.table_name.clone(), rows));
|
|
790
|
-
}
|
|
791
|
-
|
|
773
|
+
// Drop cached rows for files that disappeared since the last cache
|
|
774
|
+
// write. Trusted files need no work at all: their rows already live
|
|
775
|
+
// in the on-disk SQLite, and the watcher's diffing path reads a
|
|
776
|
+
// file's previous rows back from the row store on demand (#401).
|
|
777
|
+
if let Some((deleted, _)) = persist_ready.as_ref() {
|
|
792
778
|
for (rel_path, table_name) in deleted {
|
|
793
779
|
db.delete_rows_by_file(table_name, rel_path)
|
|
794
780
|
.map_err(map_db_error)?;
|
|
@@ -824,7 +810,6 @@ impl DirSQL {
|
|
|
824
810
|
let raw_rows =
|
|
825
811
|
merge_filesystem_facts(raw_rows, &captures, &stat_virtuals, &declared_columns);
|
|
826
812
|
|
|
827
|
-
let mut rows = Vec::with_capacity(raw_rows.len());
|
|
828
813
|
// When updating an existing file in the persistent cache, drop
|
|
829
814
|
// its old rows before inserting the new ones.
|
|
830
815
|
if persist_ready.is_some() {
|
|
@@ -834,7 +819,6 @@ impl DirSQL {
|
|
|
834
819
|
for (row_index, raw_row) in raw_rows.iter().enumerate() {
|
|
835
820
|
let row = db.normalize_row(&table_name, raw_row, strict)?;
|
|
836
821
|
db.insert_row(&table_name, &row, &rel_path, row_index)?;
|
|
837
|
-
rows.push(row);
|
|
838
822
|
}
|
|
839
823
|
|
|
840
824
|
// Update the persistent file index after a successful parse.
|
|
@@ -852,13 +836,11 @@ impl DirSQL {
|
|
|
852
836
|
)
|
|
853
837
|
.map_err(DirSqlError::sqlite)?;
|
|
854
838
|
}
|
|
855
|
-
|
|
856
|
-
file_rows.insert(rel_path, (table_name, rows));
|
|
857
839
|
}
|
|
858
840
|
|
|
859
841
|
// Write the meta block last so a crash mid-build leaves an
|
|
860
842
|
// incompatible cache that is detected on the next startup.
|
|
861
|
-
if let Some((_,
|
|
843
|
+
if let Some((_, meta)) = persist_ready.as_ref() {
|
|
862
844
|
write_meta(db.conn(), meta).map_err(DirSqlError::sqlite)?;
|
|
863
845
|
}
|
|
864
846
|
|
|
@@ -876,7 +858,6 @@ impl DirSQL {
|
|
|
876
858
|
matcher,
|
|
877
859
|
extract_map,
|
|
878
860
|
strict_map,
|
|
879
|
-
file_rows: Mutex::new(file_rows),
|
|
880
861
|
watcher: Mutex::new(None),
|
|
881
862
|
poll_used: AtomicBool::new(false),
|
|
882
863
|
watch_thread_started: AtomicBool::new(false),
|
|
@@ -1211,12 +1192,14 @@ pub struct PreparedBuild {
|
|
|
1211
1192
|
#[doc(hidden)]
|
|
1212
1193
|
pub struct PreparedPersist {
|
|
1213
1194
|
db: Db,
|
|
1214
|
-
trusted: Vec<TrustedFile>,
|
|
1215
1195
|
deleted: Vec<(String, String)>,
|
|
1216
1196
|
meta: HashMap<String, String>,
|
|
1217
|
-
cold_rebuild: bool,
|
|
1218
1197
|
}
|
|
1219
1198
|
|
|
1199
|
+
/// A file the reconcile decided to trust: its cached rows are kept as-is and
|
|
1200
|
+
/// the file is not re-parsed. Production consumes the decision implicitly
|
|
1201
|
+
/// (trusted files are excluded from the re-parse list); the list itself is
|
|
1202
|
+
/// returned so tests can assert on the trust decision directly.
|
|
1220
1203
|
#[doc(hidden)]
|
|
1221
1204
|
pub struct TrustedFile {
|
|
1222
1205
|
pub rel_path: String,
|
|
@@ -1228,7 +1211,6 @@ struct PersistContext {
|
|
|
1228
1211
|
db: Db,
|
|
1229
1212
|
cached: HashMap<String, CachedFile>,
|
|
1230
1213
|
expected_meta: HashMap<String, String>,
|
|
1231
|
-
cold_rebuild: bool,
|
|
1232
1214
|
}
|
|
1233
1215
|
|
|
1234
1216
|
fn compile_matcher(
|
|
@@ -1263,8 +1245,8 @@ fn compile_matcher(
|
|
|
1263
1245
|
|
|
1264
1246
|
/// Open (or create) the persistent SQLite cache and read its meta. If the
|
|
1265
1247
|
/// meta is missing or incompatible with the current build, the cache is
|
|
1266
|
-
/// wiped and the resulting [`PersistContext`]
|
|
1267
|
-
/// so the rest of the pipeline
|
|
1248
|
+
/// wiped and the resulting [`PersistContext`] carries an empty file index,
|
|
1249
|
+
/// so the rest of the pipeline treats every file as new.
|
|
1268
1250
|
fn prepare_persist(
|
|
1269
1251
|
root: &Path,
|
|
1270
1252
|
tables: &[Table],
|
|
@@ -1284,19 +1266,17 @@ fn prepare_persist(
|
|
|
1284
1266
|
let cached_meta = read_meta(db.conn()).map_err(DirSqlError::sqlite)?;
|
|
1285
1267
|
let compatible = !cached_meta.is_empty() && meta_is_compatible(&cached_meta, &expected_meta);
|
|
1286
1268
|
|
|
1287
|
-
let
|
|
1288
|
-
|
|
1289
|
-
(files, false)
|
|
1269
|
+
let cached = if compatible {
|
|
1270
|
+
read_cached_files(db.conn()).map_err(DirSqlError::sqlite)?
|
|
1290
1271
|
} else {
|
|
1291
1272
|
drop_user_tables(db.conn()).map_err(DirSqlError::sqlite)?;
|
|
1292
|
-
|
|
1273
|
+
HashMap::new()
|
|
1293
1274
|
};
|
|
1294
1275
|
|
|
1295
1276
|
Ok(PersistContext {
|
|
1296
1277
|
db,
|
|
1297
1278
|
cached,
|
|
1298
1279
|
expected_meta,
|
|
1299
|
-
cold_rebuild,
|
|
1300
1280
|
})
|
|
1301
1281
|
}
|
|
1302
1282
|
|
|
@@ -2324,7 +2304,6 @@ mod internal_tests {
|
|
|
2324
2304
|
db: Db::new().unwrap(),
|
|
2325
2305
|
cached,
|
|
2326
2306
|
expected_meta: HashMap::new(),
|
|
2327
|
-
cold_rebuild: false,
|
|
2328
2307
|
};
|
|
2329
2308
|
let scanned = vec![(abs.clone(), "t".to_string())];
|
|
2330
2309
|
let (to_parse, trusted, deleted) =
|
|
@@ -2363,7 +2342,6 @@ mod internal_tests {
|
|
|
2363
2342
|
db: Db::new().unwrap(),
|
|
2364
2343
|
cached,
|
|
2365
2344
|
expected_meta: HashMap::new(),
|
|
2366
|
-
cold_rebuild: false,
|
|
2367
2345
|
};
|
|
2368
2346
|
let scanned = vec![(abs.clone(), "t".to_string())];
|
|
2369
2347
|
let (to_parse, trusted, _deleted) =
|
|
@@ -2381,7 +2359,6 @@ mod internal_tests {
|
|
|
2381
2359
|
db: Db::new().unwrap(),
|
|
2382
2360
|
cached: HashMap::new(),
|
|
2383
2361
|
expected_meta: HashMap::new(),
|
|
2384
|
-
cold_rebuild: false,
|
|
2385
2362
|
};
|
|
2386
2363
|
let missing = dir.path().join("ghost.txt");
|
|
2387
2364
|
let scanned = vec![(missing, "t".to_string())];
|
|
@@ -2527,19 +2504,9 @@ mod internal_tests {
|
|
|
2527
2504
|
assert!(dbg.contains("poisoned lock"), "expected poison text: {dbg}");
|
|
2528
2505
|
}
|
|
2529
2506
|
|
|
2530
|
-
#[test]
|
|
2531
|
-
fn handle_delete_surfaces_file_rows_poison() {
|
|
2532
|
-
let (_dir, db, _abs, rel) = upsert_fixture();
|
|
2533
|
-
poison(&db.inner.file_rows);
|
|
2534
|
-
let events = db.handle_delete("items", &rel);
|
|
2535
|
-
assert_single_lock_error(&events);
|
|
2536
|
-
}
|
|
2537
|
-
|
|
2538
2507
|
#[test]
|
|
2539
2508
|
fn handle_delete_surfaces_db_poison() {
|
|
2540
2509
|
let (_dir, db, _abs, rel) = upsert_fixture();
|
|
2541
|
-
// Only the db mutex is poisoned; the file_rows lock succeeds first,
|
|
2542
|
-
// so the error comes from the db-lock arm.
|
|
2543
2510
|
poison(&db.inner.db);
|
|
2544
2511
|
let events = db.handle_delete("items", &rel);
|
|
2545
2512
|
assert_single_lock_error(&events);
|
|
@@ -2548,9 +2515,9 @@ mod internal_tests {
|
|
|
2548
2515
|
#[test]
|
|
2549
2516
|
fn handle_delete_surfaces_db_failure() {
|
|
2550
2517
|
let (_dir, db, _abs, _rel) = upsert_fixture();
|
|
2551
|
-
// No SQL table named `ghost` exists, so
|
|
2552
|
-
// `
|
|
2553
|
-
// the
|
|
2518
|
+
// No SQL table named `ghost` exists, so the old-row snapshot issues a
|
|
2519
|
+
// `SELECT ... FROM "ghost" ...` that fails with "no such table". That
|
|
2520
|
+
// drives the get_rows_by_file Err arm of `handle_delete`.
|
|
2554
2521
|
let events = db.handle_delete("ghost", "whatever.txt");
|
|
2555
2522
|
assert_eq!(events.len(), 1, "expected one error event: {events:?}");
|
|
2556
2523
|
let dbg = format!("{:?}", events[0]);
|
|
@@ -2558,6 +2525,33 @@ mod internal_tests {
|
|
|
2558
2525
|
assert!(dbg.contains("no such table"), "expected a SQL error: {dbg}");
|
|
2559
2526
|
}
|
|
2560
2527
|
|
|
2528
|
+
#[test]
|
|
2529
|
+
fn handle_delete_surfaces_delete_failure() {
|
|
2530
|
+
// The old-row snapshot succeeds but the delete itself fails: a trigger
|
|
2531
|
+
// aborts every DELETE on items, driving `handle_delete`'s
|
|
2532
|
+
// delete_rows_by_file Err arm (the snapshot read runs first, so the
|
|
2533
|
+
// ghost-table test above no longer reaches it).
|
|
2534
|
+
let (_dir, db, abs, rel) = upsert_fixture();
|
|
2535
|
+
let events = db.handle_upsert("items", &abs, &rel);
|
|
2536
|
+
assert_eq!(events.len(), 1, "fixture insert failed: {events:?}");
|
|
2537
|
+
{
|
|
2538
|
+
let guard = db.inner.db.lock().unwrap();
|
|
2539
|
+
guard
|
|
2540
|
+
.conn()
|
|
2541
|
+
.execute(
|
|
2542
|
+
"CREATE TRIGGER items_no_delete BEFORE DELETE ON items \
|
|
2543
|
+
BEGIN SELECT RAISE(ABORT, 'delete forbidden by test trigger'); END",
|
|
2544
|
+
[],
|
|
2545
|
+
)
|
|
2546
|
+
.unwrap();
|
|
2547
|
+
}
|
|
2548
|
+
let events = db.handle_delete("items", &rel);
|
|
2549
|
+
assert_eq!(events.len(), 1, "expected one error event: {events:?}");
|
|
2550
|
+
let dbg = format!("{:?}", events[0]);
|
|
2551
|
+
assert!(dbg.contains("Error"), "expected an Error event: {dbg}");
|
|
2552
|
+
assert!(dbg.contains("delete forbidden"), "got: {dbg}");
|
|
2553
|
+
}
|
|
2554
|
+
|
|
2561
2555
|
#[test]
|
|
2562
2556
|
fn handle_upsert_surfaces_db_poison() {
|
|
2563
2557
|
let (_dir, db, abs, rel) = upsert_fixture();
|
|
@@ -2567,13 +2561,27 @@ mod internal_tests {
|
|
|
2567
2561
|
}
|
|
2568
2562
|
|
|
2569
2563
|
#[test]
|
|
2570
|
-
fn
|
|
2564
|
+
fn handle_upsert_surfaces_insert_failure() {
|
|
2565
|
+
// Normalize and the old-row snapshot succeed, but the write-back
|
|
2566
|
+
// fails: a trigger aborts every INSERT on items, driving
|
|
2567
|
+
// `handle_upsert`'s db_result Err arm.
|
|
2571
2568
|
let (_dir, db, abs, rel) = upsert_fixture();
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2569
|
+
{
|
|
2570
|
+
let guard = db.inner.db.lock().unwrap();
|
|
2571
|
+
guard
|
|
2572
|
+
.conn()
|
|
2573
|
+
.execute(
|
|
2574
|
+
"CREATE TRIGGER items_no_insert BEFORE INSERT ON items \
|
|
2575
|
+
BEGIN SELECT RAISE(ABORT, 'insert forbidden by test trigger'); END",
|
|
2576
|
+
[],
|
|
2577
|
+
)
|
|
2578
|
+
.unwrap();
|
|
2579
|
+
}
|
|
2575
2580
|
let events = db.handle_upsert("items", &abs, &rel);
|
|
2576
|
-
|
|
2581
|
+
assert_eq!(events.len(), 1, "expected one error event: {events:?}");
|
|
2582
|
+
let dbg = format!("{:?}", events[0]);
|
|
2583
|
+
assert!(dbg.contains("Error"), "expected an Error event: {dbg}");
|
|
2584
|
+
assert!(dbg.contains("insert forbidden"), "got: {dbg}");
|
|
2577
2585
|
}
|
|
2578
2586
|
|
|
2579
2587
|
// ----- handle_upsert clean early-returns --------------------------------
|
|
@@ -2807,10 +2815,10 @@ mod internal_tests {
|
|
|
2807
2815
|
// ----- handle_upsert success + extract error ---------------------------
|
|
2808
2816
|
|
|
2809
2817
|
/// The full success path of `handle_upsert`: stat OK, extract runs, columns
|
|
2810
|
-
/// resolved, rows normalized and inserted
|
|
2811
|
-
///
|
|
2818
|
+
/// resolved, rows normalized and inserted. Returns one Insert; the stored
|
|
2819
|
+
/// row is then read back from SQLite and diffs to a Delete on removal.
|
|
2812
2820
|
#[test]
|
|
2813
|
-
fn
|
|
2821
|
+
fn handle_upsert_inserts_and_diffs_rows() {
|
|
2814
2822
|
let (_dir, db, abs, rel) = upsert_fixture();
|
|
2815
2823
|
let events = db.handle_upsert("items", &abs, &rel);
|
|
2816
2824
|
assert_eq!(events.len(), 1, "got: {events:?}");
|
|
@@ -2909,14 +2917,13 @@ mod internal_tests {
|
|
|
2909
2917
|
assert!(second.query("SELECT * FROM t").is_ok());
|
|
2910
2918
|
}
|
|
2911
2919
|
|
|
2912
|
-
/// `prepare_persist` on a fresh cache reports a cold rebuild
|
|
2913
|
-
/// file index and a populated expected-meta map.
|
|
2920
|
+
/// `prepare_persist` on a fresh cache reports a cold rebuild: an empty
|
|
2921
|
+
/// file index (so every file re-parses) and a populated expected-meta map.
|
|
2914
2922
|
#[test]
|
|
2915
2923
|
fn prepare_persist_cold_start_reports_rebuild() {
|
|
2916
2924
|
let dir = TempDir::new().unwrap();
|
|
2917
2925
|
let tables = vec![Table::new("CREATE TABLE t (x TEXT)", "*.txt", |_| vec![])];
|
|
2918
2926
|
let ctx = prepare_persist(dir.path(), &tables, &[], None).unwrap();
|
|
2919
|
-
assert!(ctx.cold_rebuild);
|
|
2920
2927
|
assert!(ctx.cached.is_empty());
|
|
2921
2928
|
assert!(!ctx.expected_meta.is_empty());
|
|
2922
2929
|
}
|
|
@@ -2946,7 +2953,6 @@ mod internal_tests {
|
|
|
2946
2953
|
db: Db::new().unwrap(),
|
|
2947
2954
|
cached,
|
|
2948
2955
|
expected_meta: HashMap::new(),
|
|
2949
|
-
cold_rebuild: false,
|
|
2950
2956
|
};
|
|
2951
2957
|
let scanned = vec![(abs, "t".to_string())];
|
|
2952
2958
|
let (to_parse, trusted, deleted) =
|
|
@@ -2979,7 +2985,6 @@ mod internal_tests {
|
|
|
2979
2985
|
db: Db::new().unwrap(),
|
|
2980
2986
|
cached,
|
|
2981
2987
|
expected_meta: HashMap::new(),
|
|
2982
|
-
cold_rebuild: false,
|
|
2983
2988
|
};
|
|
2984
2989
|
let scanned = vec![(abs, "t".to_string())];
|
|
2985
2990
|
let (to_parse, trusted, _deleted) =
|
|
@@ -3007,7 +3012,6 @@ mod internal_tests {
|
|
|
3007
3012
|
db: Db::new().unwrap(),
|
|
3008
3013
|
cached,
|
|
3009
3014
|
expected_meta: HashMap::new(),
|
|
3010
|
-
cold_rebuild: false,
|
|
3011
3015
|
};
|
|
3012
3016
|
let fake = FakeFs::default();
|
|
3013
3017
|
let (to_parse, trusted, deleted) =
|