style: cargo fmt --all (round 4 ingest log feature follow-up)
Phase C4 executor 의 마지막 `fix(test): clippy + fmt fixes` commit 이 test file 부분만 fmt 적용. workspace 전체 fmt 누락 발견 → cargo fmt --all 적용. 모든 import alphabetical reorder + line wrapping 정합. 추가 untracked artifact 동시 commit: - docs/superpowers/specs/2026-05-28-v0.20-ingest-log-spec.md (491 line, ACCEPT) - docs/superpowers/plans/2026-05-28-v0.20-ingest-log-plan.md (616 line, ACCEPT) workspace test: 1370 passed / 0 failed / 50 ignored, ingest_log_smoke green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -37,14 +37,16 @@ impl SqliteStore {
|
||||
.created_at
|
||||
.format(&time::format_description::well_known::Rfc3339)
|
||||
.context("format answer.created_at")?;
|
||||
let citations_json = serde_json::to_string(&answer.citations)
|
||||
.context("serialize answer.citations")?;
|
||||
let citations_json =
|
||||
serde_json::to_string(&answer.citations).context("serialize answer.citations")?;
|
||||
let refusal_label: Option<&'static str> =
|
||||
answer.refusal_reason.as_ref().map(refusal_reason_label);
|
||||
let mode_label = search_mode_label(&answer.retrieval.mode);
|
||||
let embedding_id: Option<&str> = answer.embedding.as_ref().map(|m| m.id.as_str());
|
||||
let embedding_dim: Option<i64> =
|
||||
answer.embedding.as_ref().and_then(|m| m.dimensions.map(|d| d as i64));
|
||||
let embedding_dim: Option<i64> = answer
|
||||
.embedding
|
||||
.as_ref()
|
||||
.and_then(|m| m.dimensions.map(|d| d as i64));
|
||||
|
||||
let conn = self.lock_conn();
|
||||
conn.execute(
|
||||
|
||||
@@ -40,11 +40,7 @@ impl kebab_core::DocumentStore for SqliteStore {
|
||||
}
|
||||
};
|
||||
let conn = self.lock_conn();
|
||||
purge_orphan_at_workspace_path(
|
||||
&conn,
|
||||
&asset.workspace_path.0,
|
||||
&asset.asset_id.0,
|
||||
)?;
|
||||
purge_orphan_at_workspace_path(&conn, &asset.workspace_path.0, &asset.asset_id.0)?;
|
||||
upsert_asset_row(&conn, asset, storage_kind, &storage_path)
|
||||
}
|
||||
|
||||
@@ -57,11 +53,7 @@ impl kebab_core::DocumentStore for SqliteStore {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn put_blocks(
|
||||
&self,
|
||||
doc: &kebab_core::DocumentId,
|
||||
blocks: &[kebab_core::Block],
|
||||
) -> Result<()> {
|
||||
fn put_blocks(&self, doc: &kebab_core::DocumentId, blocks: &[kebab_core::Block]) -> Result<()> {
|
||||
let mut conn = self.lock_conn();
|
||||
let tx = conn.transaction().map_err(StoreError::from)?;
|
||||
// DELETE-then-INSERT: §5.4 has no UNIQUE on (doc_id, ordinal)
|
||||
@@ -100,11 +92,7 @@ impl kebab_core::DocumentStore for SqliteStore {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn put_chunks(
|
||||
&self,
|
||||
doc: &kebab_core::DocumentId,
|
||||
chunks: &[kebab_core::Chunk],
|
||||
) -> Result<()> {
|
||||
fn put_chunks(&self, doc: &kebab_core::DocumentId, chunks: &[kebab_core::Chunk]) -> Result<()> {
|
||||
let now = OffsetDateTime::now_utc()
|
||||
.format(&time::format_description::well_known::Rfc3339)
|
||||
.context("format chunk created_at")?;
|
||||
@@ -126,8 +114,8 @@ impl kebab_core::DocumentStore for SqliteStore {
|
||||
.context("serialize chunk.heading_path")?;
|
||||
let source_spans = serde_json::to_string(&chunk.source_spans)
|
||||
.context("serialize chunk.source_spans")?;
|
||||
let block_ids = serde_json::to_string(&chunk.block_ids)
|
||||
.context("serialize chunk.block_ids")?;
|
||||
let block_ids =
|
||||
serde_json::to_string(&chunk.block_ids).context("serialize chunk.block_ids")?;
|
||||
// §5.5 has a `section_label` column but the in-memory Chunk
|
||||
// struct does not carry it (nor does the wire schema §2.6).
|
||||
// Persist NULL until a future bump introduces the field.
|
||||
@@ -193,16 +181,15 @@ impl kebab_core::DocumentStore for SqliteStore {
|
||||
let mut blocks: Vec<kebab_core::Block> = Vec::new();
|
||||
for row in block_rows {
|
||||
let payload_json = row.map_err(StoreError::from)?;
|
||||
let block: kebab_core::Block = serde_json::from_str(&payload_json)
|
||||
.context("deserialize block payload_json")?;
|
||||
let block: kebab_core::Block =
|
||||
serde_json::from_str(&payload_json).context("deserialize block payload_json")?;
|
||||
blocks.push(block);
|
||||
}
|
||||
|
||||
let metadata: kebab_core::Metadata = serde_json::from_str(&row.metadata_json)
|
||||
.context("deserialize metadata_json")?;
|
||||
let metadata: kebab_core::Metadata =
|
||||
serde_json::from_str(&row.metadata_json).context("deserialize metadata_json")?;
|
||||
let provenance: kebab_core::Provenance =
|
||||
serde_json::from_str(&row.provenance_json)
|
||||
.context("deserialize provenance_json")?;
|
||||
serde_json::from_str(&row.provenance_json).context("deserialize provenance_json")?;
|
||||
|
||||
Ok(Some(kebab_core::CanonicalDocument {
|
||||
doc_id: kebab_core::DocumentId(row.doc_id),
|
||||
@@ -248,9 +235,8 @@ impl kebab_core::DocumentStore for SqliteStore {
|
||||
let source_spans: Vec<kebab_core::SourceSpan> =
|
||||
serde_json::from_str(&row.source_spans_json)
|
||||
.context("deserialize chunk.source_spans_json")?;
|
||||
let block_ids: Vec<kebab_core::BlockId> =
|
||||
serde_json::from_str(&row.block_ids_json)
|
||||
.context("deserialize chunk.block_ids_json")?;
|
||||
let block_ids: Vec<kebab_core::BlockId> = serde_json::from_str(&row.block_ids_json)
|
||||
.context("deserialize chunk.block_ids_json")?;
|
||||
Ok(Some(kebab_core::Chunk {
|
||||
chunk_id: kebab_core::ChunkId(row.chunk_id),
|
||||
doc_id: kebab_core::DocumentId(row.doc_id),
|
||||
@@ -264,10 +250,7 @@ impl kebab_core::DocumentStore for SqliteStore {
|
||||
}))
|
||||
}
|
||||
|
||||
fn get_asset(
|
||||
&self,
|
||||
id: &kebab_core::AssetId,
|
||||
) -> Result<Option<kebab_core::RawAsset>> {
|
||||
fn get_asset(&self, id: &kebab_core::AssetId) -> Result<Option<kebab_core::RawAsset>> {
|
||||
let conn = self.lock_conn();
|
||||
let result = conn.query_row(
|
||||
r"SELECT
|
||||
@@ -346,16 +329,15 @@ impl kebab_core::DocumentStore for SqliteStore {
|
||||
let mut blocks: Vec<kebab_core::Block> = Vec::new();
|
||||
for block_row in block_rows {
|
||||
let payload_json = block_row.map_err(StoreError::from)?;
|
||||
let block: kebab_core::Block = serde_json::from_str(&payload_json)
|
||||
.context("deserialize block payload_json")?;
|
||||
let block: kebab_core::Block =
|
||||
serde_json::from_str(&payload_json).context("deserialize block payload_json")?;
|
||||
blocks.push(block);
|
||||
}
|
||||
|
||||
let metadata: kebab_core::Metadata = serde_json::from_str(&row.metadata_json)
|
||||
.context("deserialize metadata_json")?;
|
||||
let metadata: kebab_core::Metadata =
|
||||
serde_json::from_str(&row.metadata_json).context("deserialize metadata_json")?;
|
||||
let provenance: kebab_core::Provenance =
|
||||
serde_json::from_str(&row.provenance_json)
|
||||
.context("deserialize provenance_json")?;
|
||||
serde_json::from_str(&row.provenance_json).context("deserialize provenance_json")?;
|
||||
|
||||
Ok(Some(kebab_core::CanonicalDocument {
|
||||
doc_id,
|
||||
@@ -421,12 +403,14 @@ impl kebab_core::DocumentStore for SqliteStore {
|
||||
if let Some(trust_min) = &filter.trust_min {
|
||||
// Map the enum to its rank: Generated < Secondary < Primary.
|
||||
// (Higher trust strictly contains lower trust.)
|
||||
sql.push_str(" AND CASE d.trust_level
|
||||
sql.push_str(
|
||||
" AND CASE d.trust_level
|
||||
WHEN 'primary' THEN 3
|
||||
WHEN 'secondary' THEN 2
|
||||
WHEN 'generated' THEN 1
|
||||
ELSE 0
|
||||
END >= ?");
|
||||
END >= ?",
|
||||
);
|
||||
let rank: i64 = match trust_min {
|
||||
kebab_core::TrustLevel::Primary => 3,
|
||||
kebab_core::TrustLevel::Secondary => 2,
|
||||
@@ -606,21 +590,27 @@ fn doc_summary_from_sql(row: &rusqlite::Row<'_>) -> rusqlite::Result<kebab_core:
|
||||
// De-serialize the lowercase string forms that match
|
||||
// `#[serde(rename_all = "lowercase")]` on each enum.
|
||||
let source_type: kebab_core::SourceType =
|
||||
serde_json::from_value(serde_json::Value::String(source_type_raw))
|
||||
.map_err(|e| rusqlite::Error::FromSqlConversionFailure(4, rusqlite::types::Type::Text, Box::new(e)))?;
|
||||
serde_json::from_value(serde_json::Value::String(source_type_raw)).map_err(|e| {
|
||||
rusqlite::Error::FromSqlConversionFailure(4, rusqlite::types::Type::Text, Box::new(e))
|
||||
})?;
|
||||
let trust_level: kebab_core::TrustLevel =
|
||||
serde_json::from_value(serde_json::Value::String(trust_level_raw))
|
||||
.map_err(|e| rusqlite::Error::FromSqlConversionFailure(5, rusqlite::types::Type::Text, Box::new(e)))?;
|
||||
serde_json::from_value(serde_json::Value::String(trust_level_raw)).map_err(|e| {
|
||||
rusqlite::Error::FromSqlConversionFailure(5, rusqlite::types::Type::Text, Box::new(e))
|
||||
})?;
|
||||
let created_at = OffsetDateTime::parse(
|
||||
&created_at_raw,
|
||||
&time::format_description::well_known::Rfc3339,
|
||||
)
|
||||
.map_err(|e| rusqlite::Error::FromSqlConversionFailure(7, rusqlite::types::Type::Text, Box::new(e)))?;
|
||||
.map_err(|e| {
|
||||
rusqlite::Error::FromSqlConversionFailure(7, rusqlite::types::Type::Text, Box::new(e))
|
||||
})?;
|
||||
let updated_at = OffsetDateTime::parse(
|
||||
&updated_at_raw,
|
||||
&time::format_description::well_known::Rfc3339,
|
||||
)
|
||||
.map_err(|e| rusqlite::Error::FromSqlConversionFailure(8, rusqlite::types::Type::Text, Box::new(e)))?;
|
||||
.map_err(|e| {
|
||||
rusqlite::Error::FromSqlConversionFailure(8, rusqlite::types::Type::Text, Box::new(e))
|
||||
})?;
|
||||
|
||||
Ok(kebab_core::DocSummary {
|
||||
doc_id: kebab_core::DocumentId(doc_id),
|
||||
@@ -678,14 +668,18 @@ fn asset_from_row(row: &rusqlite::Row<'_>) -> rusqlite::Result<kebab_core::RawAs
|
||||
};
|
||||
|
||||
let workspace_path = kebab_core::WorkspacePath(workspace_path_raw);
|
||||
let media_type: kebab_core::MediaType = serde_json::from_str(&media_type_json)
|
||||
.map_err(|e| rusqlite::Error::FromSqlConversionFailure(3, rusqlite::types::Type::Text, Box::new(e)))?;
|
||||
let media_type: kebab_core::MediaType =
|
||||
serde_json::from_str(&media_type_json).map_err(|e| {
|
||||
rusqlite::Error::FromSqlConversionFailure(3, rusqlite::types::Type::Text, Box::new(e))
|
||||
})?;
|
||||
let checksum = kebab_core::Checksum(checksum_raw.clone());
|
||||
let discovered_at = time::OffsetDateTime::parse(
|
||||
&discovered_at_raw,
|
||||
&time::format_description::well_known::Rfc3339,
|
||||
)
|
||||
.map_err(|e| rusqlite::Error::FromSqlConversionFailure(8, rusqlite::types::Type::Text, Box::new(e)))?;
|
||||
.map_err(|e| {
|
||||
rusqlite::Error::FromSqlConversionFailure(8, rusqlite::types::Type::Text, Box::new(e))
|
||||
})?;
|
||||
|
||||
let storage_path = PathBuf::from(&storage_path_raw);
|
||||
let stored = if storage_kind == "copied" {
|
||||
@@ -702,14 +696,15 @@ fn asset_from_row(row: &rusqlite::Row<'_>) -> rusqlite::Result<kebab_core::RawAs
|
||||
source_uri,
|
||||
workspace_path,
|
||||
media_type,
|
||||
byte_len: u64::try_from(byte_len)
|
||||
.map_err(|e| rusqlite::Error::FromSqlConversionFailure(
|
||||
byte_len: u64::try_from(byte_len).map_err(|e| {
|
||||
rusqlite::Error::FromSqlConversionFailure(
|
||||
// index parameter for named-column path is unused but the
|
||||
// type still requires a number; pass 0 with a clear msg.
|
||||
0,
|
||||
rusqlite::types::Type::Integer,
|
||||
Box::new(e),
|
||||
))?,
|
||||
)
|
||||
})?,
|
||||
checksum,
|
||||
discovered_at,
|
||||
stored,
|
||||
@@ -721,10 +716,8 @@ fn upsert_document(
|
||||
tx: &rusqlite::Transaction<'_>,
|
||||
doc: &kebab_core::CanonicalDocument,
|
||||
) -> Result<()> {
|
||||
let metadata_json = serde_json::to_string(&doc.metadata)
|
||||
.context("serialize metadata")?;
|
||||
let provenance_json = serde_json::to_string(&doc.provenance)
|
||||
.context("serialize provenance")?;
|
||||
let metadata_json = serde_json::to_string(&doc.metadata).context("serialize metadata")?;
|
||||
let provenance_json = serde_json::to_string(&doc.provenance).context("serialize provenance")?;
|
||||
// String form of the lowercase serde representation. We avoid
|
||||
// embedding `serde_json::to_string` quotes (`"markdown"` → just
|
||||
// `markdown` for the column).
|
||||
@@ -811,8 +804,11 @@ fn replace_document_tags(
|
||||
doc_id: &kebab_core::DocumentId,
|
||||
tags: &[String],
|
||||
) -> Result<()> {
|
||||
tx.execute("DELETE FROM document_tags WHERE doc_id = ?", params![doc_id.0])
|
||||
.map_err(StoreError::from)?;
|
||||
tx.execute(
|
||||
"DELETE FROM document_tags WHERE doc_id = ?",
|
||||
params![doc_id.0],
|
||||
)
|
||||
.map_err(StoreError::from)?;
|
||||
let mut stmt = tx
|
||||
.prepare(
|
||||
"INSERT INTO document_tags (doc_id, tag) VALUES (?, ?)
|
||||
|
||||
@@ -54,10 +54,7 @@ impl SqliteStore {
|
||||
/// All rows are written in a single transaction; if any row fails
|
||||
/// the entire batch is rolled back and the caller can retry without
|
||||
/// worrying about partial pending state.
|
||||
pub fn put_embedding_records_pending(
|
||||
&self,
|
||||
rows: &[EmbeddingRecordRow],
|
||||
) -> Result<()> {
|
||||
pub fn put_embedding_records_pending(&self, rows: &[EmbeddingRecordRow]) -> Result<()> {
|
||||
if rows.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
@@ -107,10 +104,7 @@ impl SqliteStore {
|
||||
/// WHERE embedding_id IN (?, ?, …)`) inside one transaction —
|
||||
/// avoids the per-row `execute()` round-trip the previous
|
||||
/// implementation paid.
|
||||
pub fn mark_embedding_records_committed(
|
||||
&self,
|
||||
embedding_ids: &[String],
|
||||
) -> Result<()> {
|
||||
pub fn mark_embedding_records_committed(&self, embedding_ids: &[String]) -> Result<()> {
|
||||
if embedding_ids.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
@@ -208,7 +202,11 @@ mod tests {
|
||||
source_spans_json, token_estimate, chunker_version,
|
||||
policy_hash, block_ids_json, created_at
|
||||
) VALUES (?, ?, 'hi', '[]', NULL, '[]', 1, 'v1', 'hash', '[]', ?)",
|
||||
params![chunk_id, "fedcba9876543210fedcba9876543210", "1970-01-01T00:00:00Z"],
|
||||
params![
|
||||
chunk_id,
|
||||
"fedcba9876543210fedcba9876543210",
|
||||
"1970-01-01T00:00:00Z"
|
||||
],
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
@@ -209,11 +209,9 @@ impl SqliteStore {
|
||||
Err(rusqlite::Error::QueryReturnedNoRows) => return Ok(None),
|
||||
Err(e) => return Err(StoreError::from(e).into()),
|
||||
};
|
||||
let created_at = OffsetDateTime::parse(
|
||||
&created_str,
|
||||
&time::format_description::well_known::Rfc3339,
|
||||
)
|
||||
.with_context(|| format!("parse eval_runs.created_at for {run_id}"))?;
|
||||
let created_at =
|
||||
OffsetDateTime::parse(&created_str, &time::format_description::well_known::Rfc3339)
|
||||
.with_context(|| format!("parse eval_runs.created_at for {run_id}"))?;
|
||||
Ok(Some(EvalRunRecord {
|
||||
run_id,
|
||||
suite,
|
||||
@@ -228,10 +226,7 @@ impl SqliteStore {
|
||||
/// `query_id` ASC for determinism (the table has no insertion-order
|
||||
/// column; query_id ordering matches the BTreeSet sort the loader
|
||||
/// uses for missing-id reporting).
|
||||
pub fn load_eval_query_results(
|
||||
&self,
|
||||
run_id: &str,
|
||||
) -> Result<Vec<EvalQueryResultRecord>> {
|
||||
pub fn load_eval_query_results(&self, run_id: &str) -> Result<Vec<EvalQueryResultRecord>> {
|
||||
let conn = self.lock_conn();
|
||||
let mut stmt = conn
|
||||
.prepare(
|
||||
@@ -258,11 +253,7 @@ impl SqliteStore {
|
||||
/// `Err` (not `Ok(0)`) if the run is missing — we never want to
|
||||
/// silently drop computed metrics. Called once per run by
|
||||
/// P5-2's `store_aggregate`.
|
||||
pub fn update_eval_run_aggregate(
|
||||
&self,
|
||||
run_id: &str,
|
||||
aggregate_json: &str,
|
||||
) -> Result<()> {
|
||||
pub fn update_eval_run_aggregate(&self, run_id: &str, aggregate_json: &str) -> Result<()> {
|
||||
let conn = self.lock_conn();
|
||||
let updated = conn
|
||||
.execute(
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use rusqlite::{params_from_iter, ToSql};
|
||||
use rusqlite::{ToSql, params_from_iter};
|
||||
|
||||
use crate::store::SqliteStore;
|
||||
|
||||
@@ -347,9 +347,7 @@ mod tests {
|
||||
.put_embedding_records_pending(std::slice::from_ref(&embed_row))
|
||||
.unwrap();
|
||||
store
|
||||
.mark_embedding_records_committed(std::slice::from_ref(
|
||||
&embed_row.embedding_id,
|
||||
))
|
||||
.mark_embedding_records_committed(std::slice::from_ref(&embed_row.embedding_id))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
@@ -430,9 +428,7 @@ mod tests {
|
||||
.put_embedding_records_pending(std::slice::from_ref(&embed_row))
|
||||
.unwrap();
|
||||
store
|
||||
.mark_embedding_records_committed(std::slice::from_ref(
|
||||
&embed_row.embedding_id,
|
||||
))
|
||||
.mark_embedding_records_committed(std::slice::from_ref(&embed_row.embedding_id))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
@@ -502,9 +498,7 @@ mod tests {
|
||||
.put_embedding_records_pending(std::slice::from_ref(&embed_row))
|
||||
.unwrap();
|
||||
store
|
||||
.mark_embedding_records_committed(std::slice::from_ref(
|
||||
&embed_row.embedding_id,
|
||||
))
|
||||
.mark_embedding_records_committed(std::slice::from_ref(&embed_row.embedding_id))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
@@ -573,10 +567,38 @@ mod tests {
|
||||
// c3: tags=[ko-style], lang=ko, secondary, notes/c.md
|
||||
// c4: tags=[ko-style], lang=en, generated, src/d.md
|
||||
let chunks = [
|
||||
("11111111111111111111111111111111", "d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1", "notes/a.md", "en", "primary", &["ko-style"][..]),
|
||||
("22222222222222222222222222222222", "d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2", "notes/b.md", "en", "primary", &["other"][..]),
|
||||
("33333333333333333333333333333333", "d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3", "notes/c.md", "ko", "secondary", &["ko-style"][..]),
|
||||
("44444444444444444444444444444444", "d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4", "src/d.md", "en", "generated", &["ko-style"][..]),
|
||||
(
|
||||
"11111111111111111111111111111111",
|
||||
"d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1",
|
||||
"notes/a.md",
|
||||
"en",
|
||||
"primary",
|
||||
&["ko-style"][..],
|
||||
),
|
||||
(
|
||||
"22222222222222222222222222222222",
|
||||
"d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2",
|
||||
"notes/b.md",
|
||||
"en",
|
||||
"primary",
|
||||
&["other"][..],
|
||||
),
|
||||
(
|
||||
"33333333333333333333333333333333",
|
||||
"d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3",
|
||||
"notes/c.md",
|
||||
"ko",
|
||||
"secondary",
|
||||
&["ko-style"][..],
|
||||
),
|
||||
(
|
||||
"44444444444444444444444444444444",
|
||||
"d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4",
|
||||
"src/d.md",
|
||||
"en",
|
||||
"generated",
|
||||
&["ko-style"][..],
|
||||
),
|
||||
];
|
||||
for (c, d, p, l, t, tags) in &chunks {
|
||||
seed_committed(&store, c, d, p, l, tags, t);
|
||||
@@ -588,10 +610,7 @@ mod tests {
|
||||
..Default::default()
|
||||
};
|
||||
let out = store
|
||||
.filter_chunks(
|
||||
&chunks.iter().map(|c| cid(c.0)).collect::<Vec<_>>(),
|
||||
&f,
|
||||
)
|
||||
.filter_chunks(&chunks.iter().map(|c| cid(c.0)).collect::<Vec<_>>(), &f)
|
||||
.unwrap();
|
||||
let mut got: Vec<&str> = out.iter().map(|c| c.0.as_str()).collect();
|
||||
got.sort_unstable();
|
||||
@@ -604,10 +623,7 @@ mod tests {
|
||||
..Default::default()
|
||||
};
|
||||
let out = store
|
||||
.filter_chunks(
|
||||
&chunks.iter().map(|c| cid(c.0)).collect::<Vec<_>>(),
|
||||
&f,
|
||||
)
|
||||
.filter_chunks(&chunks.iter().map(|c| cid(c.0)).collect::<Vec<_>>(), &f)
|
||||
.unwrap();
|
||||
let mut got: Vec<&str> = out.iter().map(|c| c.0.as_str()).collect();
|
||||
got.sort_unstable();
|
||||
@@ -621,10 +637,7 @@ mod tests {
|
||||
..Default::default()
|
||||
};
|
||||
let out = store
|
||||
.filter_chunks(
|
||||
&chunks.iter().map(|c| cid(c.0)).collect::<Vec<_>>(),
|
||||
&f,
|
||||
)
|
||||
.filter_chunks(&chunks.iter().map(|c| cid(c.0)).collect::<Vec<_>>(), &f)
|
||||
.unwrap();
|
||||
let got: Vec<&str> = out.iter().map(|c| c.0.as_str()).collect();
|
||||
assert_eq!(got, vec![chunks[0].0]);
|
||||
@@ -635,10 +648,7 @@ mod tests {
|
||||
..Default::default()
|
||||
};
|
||||
let out = store
|
||||
.filter_chunks(
|
||||
&chunks.iter().map(|c| cid(c.0)).collect::<Vec<_>>(),
|
||||
&f,
|
||||
)
|
||||
.filter_chunks(&chunks.iter().map(|c| cid(c.0)).collect::<Vec<_>>(), &f)
|
||||
.unwrap();
|
||||
let mut got: Vec<&str> = out.iter().map(|c| c.0.as_str()).collect();
|
||||
got.sort_unstable();
|
||||
@@ -652,9 +662,33 @@ mod tests {
|
||||
let c1 = "11111111111111111111111111111111";
|
||||
let c2 = "22222222222222222222222222222222";
|
||||
let c3 = "33333333333333333333333333333333";
|
||||
seed_committed(&store, c1, "d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1", "a.md", "en", &[], "primary");
|
||||
seed_committed(&store, c2, "d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2", "b.md", "en", &[], "primary");
|
||||
seed_committed(&store, c3, "d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3", "c.md", "en", &[], "primary");
|
||||
seed_committed(
|
||||
&store,
|
||||
c1,
|
||||
"d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1",
|
||||
"a.md",
|
||||
"en",
|
||||
&[],
|
||||
"primary",
|
||||
);
|
||||
seed_committed(
|
||||
&store,
|
||||
c2,
|
||||
"d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2",
|
||||
"b.md",
|
||||
"en",
|
||||
&[],
|
||||
"primary",
|
||||
);
|
||||
seed_committed(
|
||||
&store,
|
||||
c3,
|
||||
"d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3",
|
||||
"c.md",
|
||||
"en",
|
||||
&[],
|
||||
"primary",
|
||||
);
|
||||
|
||||
// Ask in the order c3, c1, c2; result must preserve that order.
|
||||
let out = store
|
||||
@@ -688,14 +722,24 @@ mod tests {
|
||||
let c1 = "11111111111111111111111111111111";
|
||||
let c2 = "22222222222222222222222222222222";
|
||||
seed_committed_full(
|
||||
&store, c1, "d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1",
|
||||
"notes/a.md", "en", &[], "primary",
|
||||
&store,
|
||||
c1,
|
||||
"d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1",
|
||||
"notes/a.md",
|
||||
"en",
|
||||
&[],
|
||||
"primary",
|
||||
r#""markdown""#,
|
||||
"1970-01-01T00:00:00Z",
|
||||
);
|
||||
seed_committed_full(
|
||||
&store, c2, "d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2",
|
||||
"notes/b.pdf", "en", &[], "primary",
|
||||
&store,
|
||||
c2,
|
||||
"d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2",
|
||||
"notes/b.pdf",
|
||||
"en",
|
||||
&[],
|
||||
"primary",
|
||||
r#""pdf""#,
|
||||
"1970-01-01T00:00:00Z",
|
||||
);
|
||||
@@ -704,10 +748,12 @@ mod tests {
|
||||
media: vec!["pdf".to_string()],
|
||||
..Default::default()
|
||||
};
|
||||
let out = store
|
||||
.filter_chunks(&[cid(c1), cid(c2)], &f)
|
||||
.unwrap();
|
||||
assert_eq!(out, vec![cid(c2)], "only pdf chunk should survive media filter");
|
||||
let out = store.filter_chunks(&[cid(c1), cid(c2)], &f).unwrap();
|
||||
assert_eq!(
|
||||
out,
|
||||
vec![cid(c2)],
|
||||
"only pdf chunk should survive media filter"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -718,14 +764,24 @@ mod tests {
|
||||
let c1 = "11111111111111111111111111111111";
|
||||
let c2 = "22222222222222222222222222222222";
|
||||
seed_committed_full(
|
||||
&store, c1, "d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1",
|
||||
"old.md", "en", &[], "primary",
|
||||
&store,
|
||||
c1,
|
||||
"d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1",
|
||||
"old.md",
|
||||
"en",
|
||||
&[],
|
||||
"primary",
|
||||
r#""markdown""#,
|
||||
"2020-01-01T00:00:00Z",
|
||||
);
|
||||
seed_committed_full(
|
||||
&store, c2, "d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2",
|
||||
"new.md", "en", &[], "primary",
|
||||
&store,
|
||||
c2,
|
||||
"d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2",
|
||||
"new.md",
|
||||
"en",
|
||||
&[],
|
||||
"primary",
|
||||
r#""markdown""#,
|
||||
"2026-01-01T00:00:00Z",
|
||||
);
|
||||
@@ -734,10 +790,12 @@ mod tests {
|
||||
ingested_after: Some(time::macros::datetime!(2025-01-01 00:00:00 UTC)),
|
||||
..Default::default()
|
||||
};
|
||||
let out = store
|
||||
.filter_chunks(&[cid(c1), cid(c2)], &f)
|
||||
.unwrap();
|
||||
assert_eq!(out, vec![cid(c2)], "only post-2025 chunk should survive ingested_after filter");
|
||||
let out = store.filter_chunks(&[cid(c1), cid(c2)], &f).unwrap();
|
||||
assert_eq!(
|
||||
out,
|
||||
vec![cid(c2)],
|
||||
"only post-2025 chunk should survive ingested_after filter"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -749,14 +807,24 @@ mod tests {
|
||||
let c2 = "22222222222222222222222222222222";
|
||||
let d1 = "d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1";
|
||||
seed_committed_full(
|
||||
&store, c1, d1,
|
||||
"a.md", "en", &[], "primary",
|
||||
&store,
|
||||
c1,
|
||||
d1,
|
||||
"a.md",
|
||||
"en",
|
||||
&[],
|
||||
"primary",
|
||||
r#""markdown""#,
|
||||
"1970-01-01T00:00:00Z",
|
||||
);
|
||||
seed_committed_full(
|
||||
&store, c2, "d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2",
|
||||
"b.md", "en", &[], "primary",
|
||||
&store,
|
||||
c2,
|
||||
"d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2",
|
||||
"b.md",
|
||||
"en",
|
||||
&[],
|
||||
"primary",
|
||||
r#""markdown""#,
|
||||
"1970-01-01T00:00:00Z",
|
||||
);
|
||||
@@ -765,10 +833,12 @@ mod tests {
|
||||
doc_id: Some(kebab_core::DocumentId(d1.to_string())),
|
||||
..Default::default()
|
||||
};
|
||||
let out = store
|
||||
.filter_chunks(&[cid(c1), cid(c2)], &f)
|
||||
.unwrap();
|
||||
assert_eq!(out, vec![cid(c1)], "doc_id filter must scope to the target doc only");
|
||||
let out = store.filter_chunks(&[cid(c1), cid(c2)], &f).unwrap();
|
||||
assert_eq!(
|
||||
out,
|
||||
vec![cid(c1)],
|
||||
"doc_id filter must scope to the target doc only"
|
||||
);
|
||||
}
|
||||
|
||||
// ── p10-1A-1 new filter arms ─────────────────────────────────────────
|
||||
@@ -783,18 +853,27 @@ mod tests {
|
||||
let c2 = "22222222222222222222222222222222";
|
||||
let c3 = "33333333333333333333333333333333";
|
||||
seed_committed_with_metadata(
|
||||
&store, c1, "d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1",
|
||||
"src/main.py", r#""code""#,
|
||||
&store,
|
||||
c1,
|
||||
"d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1",
|
||||
"src/main.py",
|
||||
r#""code""#,
|
||||
r#"{"code_lang":"python"}"#,
|
||||
);
|
||||
seed_committed_with_metadata(
|
||||
&store, c2, "d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2",
|
||||
"src/lib.rs", r#""code""#,
|
||||
&store,
|
||||
c2,
|
||||
"d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2",
|
||||
"src/lib.rs",
|
||||
r#""code""#,
|
||||
r#"{"code_lang":"rust"}"#,
|
||||
);
|
||||
seed_committed_with_metadata(
|
||||
&store, c3, "d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3",
|
||||
"README.md", r#""markdown""#,
|
||||
&store,
|
||||
c3,
|
||||
"d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3",
|
||||
"README.md",
|
||||
r#""markdown""#,
|
||||
r"{}",
|
||||
);
|
||||
|
||||
@@ -805,7 +884,11 @@ mod tests {
|
||||
let out = store
|
||||
.filter_chunks(&[cid(c1), cid(c2), cid(c3)], &f)
|
||||
.unwrap();
|
||||
assert_eq!(out, vec![cid(c1)], "only python chunk should survive code_lang filter");
|
||||
assert_eq!(
|
||||
out,
|
||||
vec![cid(c1)],
|
||||
"only python chunk should survive code_lang filter"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -818,18 +901,27 @@ mod tests {
|
||||
let c2 = "22222222222222222222222222222222";
|
||||
let c3 = "33333333333333333333333333333333";
|
||||
seed_committed_with_metadata(
|
||||
&store, c1, "d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1",
|
||||
"httpx/client.py", r#""code""#,
|
||||
&store,
|
||||
c1,
|
||||
"d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1",
|
||||
"httpx/client.py",
|
||||
r#""code""#,
|
||||
r#"{"repo":"httpx","code_lang":"python"}"#,
|
||||
);
|
||||
seed_committed_with_metadata(
|
||||
&store, c2, "d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2",
|
||||
"requests/api.py", r#""code""#,
|
||||
&store,
|
||||
c2,
|
||||
"d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2",
|
||||
"requests/api.py",
|
||||
r#""code""#,
|
||||
r#"{"repo":"requests","code_lang":"python"}"#,
|
||||
);
|
||||
seed_committed_with_metadata(
|
||||
&store, c3, "d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3",
|
||||
"standalone.py", r#""code""#,
|
||||
&store,
|
||||
c3,
|
||||
"d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3",
|
||||
"standalone.py",
|
||||
r#""code""#,
|
||||
r#"{"code_lang":"python"}"#,
|
||||
);
|
||||
|
||||
@@ -840,7 +932,11 @@ mod tests {
|
||||
let out = store
|
||||
.filter_chunks(&[cid(c1), cid(c2), cid(c3)], &f)
|
||||
.unwrap();
|
||||
assert_eq!(out, vec![cid(c1)], "only httpx chunk should survive repo filter");
|
||||
assert_eq!(
|
||||
out,
|
||||
vec![cid(c1)],
|
||||
"only httpx chunk should survive repo filter"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -865,8 +961,13 @@ mod tests {
|
||||
let store = open_store(&tmp);
|
||||
let c1 = "11111111111111111111111111111111";
|
||||
seed_committed_full(
|
||||
&store, c1, "d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1",
|
||||
"doc.md", "en", &[], "primary",
|
||||
&store,
|
||||
c1,
|
||||
"d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1",
|
||||
"doc.md",
|
||||
"en",
|
||||
&[],
|
||||
"primary",
|
||||
r#""markdown""#,
|
||||
"2026-04-01T01:00:00Z",
|
||||
);
|
||||
@@ -883,9 +984,7 @@ mod tests {
|
||||
ingested_after: Some(filter_instant),
|
||||
..Default::default()
|
||||
};
|
||||
let out = store
|
||||
.filter_chunks(&[cid(c1)], &f)
|
||||
.unwrap();
|
||||
let out = store.filter_chunks(&[cid(c1)], &f).unwrap();
|
||||
assert_eq!(
|
||||
out,
|
||||
vec![cid(c1)],
|
||||
|
||||
@@ -86,11 +86,7 @@ impl SqliteStore {
|
||||
}
|
||||
|
||||
impl kebab_core::JobRepo for SqliteStore {
|
||||
fn create(
|
||||
&self,
|
||||
kind: kebab_core::JobKind,
|
||||
payload: Value,
|
||||
) -> Result<kebab_core::JobId> {
|
||||
fn create(&self, kind: kebab_core::JobKind, payload: Value) -> Result<kebab_core::JobId> {
|
||||
let now_dt = OffsetDateTime::now_utc();
|
||||
let now = now_dt
|
||||
.format(&time::format_description::well_known::Rfc3339)
|
||||
@@ -100,8 +96,7 @@ impl kebab_core::JobRepo for SqliteStore {
|
||||
// identical `(kind, payload)` still get distinct IDs.
|
||||
let job_id = mint_job_id(&kind, &payload, now_dt);
|
||||
let kind_label = job_kind_label(&kind);
|
||||
let payload_json = serde_json::to_string(&payload)
|
||||
.context("serialize job payload")?;
|
||||
let payload_json = serde_json::to_string(&payload).context("serialize job payload")?;
|
||||
let conn = self.lock_conn();
|
||||
conn.execute(
|
||||
"INSERT INTO jobs (
|
||||
@@ -114,13 +109,8 @@ impl kebab_core::JobRepo for SqliteStore {
|
||||
Ok(job_id)
|
||||
}
|
||||
|
||||
fn update_progress(
|
||||
&self,
|
||||
id: &kebab_core::JobId,
|
||||
progress: Value,
|
||||
) -> Result<()> {
|
||||
let progress_json = serde_json::to_string(&progress)
|
||||
.context("serialize job progress")?;
|
||||
fn update_progress(&self, id: &kebab_core::JobId, progress: Value) -> Result<()> {
|
||||
let progress_json = serde_json::to_string(&progress).context("serialize job progress")?;
|
||||
let now = OffsetDateTime::now_utc()
|
||||
.format(&time::format_description::well_known::Rfc3339)
|
||||
.context("format job updated_at")?;
|
||||
@@ -167,10 +157,7 @@ impl kebab_core::JobRepo for SqliteStore {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn list(
|
||||
&self,
|
||||
filter: &kebab_core::JobFilter,
|
||||
) -> Result<Vec<kebab_core::JobRow>> {
|
||||
fn list(&self, filter: &kebab_core::JobFilter) -> Result<Vec<kebab_core::JobRow>> {
|
||||
let conn = self.lock_conn();
|
||||
let mut sql = String::from(
|
||||
"SELECT job_id, kind, status, payload_json, progress_json,
|
||||
@@ -259,11 +246,9 @@ fn job_row_from_sql(row: &rusqlite::Row<'_>) -> rusqlite::Result<kebab_core::Job
|
||||
let finished_at_raw: Option<String> = row.get(8)?;
|
||||
|
||||
let kind: kebab_core::JobKind =
|
||||
serde_json::from_value(serde_json::Value::String(kind_raw))
|
||||
.map_err(conv_err(1))?;
|
||||
serde_json::from_value(serde_json::Value::String(kind_raw)).map_err(conv_err(1))?;
|
||||
let status: kebab_core::JobStatus =
|
||||
serde_json::from_value(serde_json::Value::String(status_raw))
|
||||
.map_err(conv_err(2))?;
|
||||
serde_json::from_value(serde_json::Value::String(status_raw)).map_err(conv_err(2))?;
|
||||
let payload: Value = serde_json::from_str(&payload_json).map_err(conv_err(3))?;
|
||||
let progress: Option<Value> = match progress_json {
|
||||
Some(s) => Some(serde_json::from_str(&s).map_err(conv_err(4))?),
|
||||
|
||||
@@ -27,8 +27,8 @@ mod filters;
|
||||
mod fts;
|
||||
mod jobs;
|
||||
mod schema;
|
||||
mod store;
|
||||
pub mod stats_ext;
|
||||
mod store;
|
||||
|
||||
pub use embeddings::EmbeddingRecordRow;
|
||||
pub use error::StoreError;
|
||||
|
||||
@@ -19,10 +19,7 @@ pub struct Breakdowns {
|
||||
/// `lang` only contains observed languages; NULL lang is
|
||||
/// keyed as the literal string `"null"`. `stale_doc_count` is 0 when
|
||||
/// `threshold_days == 0` (mirrors fb-32 staleness disable semantics).
|
||||
pub fn breakdowns(
|
||||
conn: &Connection,
|
||||
threshold_days: u64,
|
||||
) -> rusqlite::Result<Breakdowns> {
|
||||
pub fn breakdowns(conn: &Connection, threshold_days: u64) -> rusqlite::Result<Breakdowns> {
|
||||
// media: dual JSON shape — text variant ("markdown") vs object
|
||||
// variant ({"image":{"format":"png"}}). Same CASE WHEN as fb-36.
|
||||
let mut media: BTreeMap<String, u64> = MEDIA_KINDS
|
||||
@@ -40,9 +37,7 @@ pub fn breakdowns(
|
||||
FROM documents d JOIN assets a ON a.asset_id = d.asset_id \
|
||||
GROUP BY kind",
|
||||
)?;
|
||||
let rows = stmt.query_map([], |r| {
|
||||
Ok((r.get::<_, String>(0)?, r.get::<_, u64>(1)?))
|
||||
})?;
|
||||
let rows = stmt.query_map([], |r| Ok((r.get::<_, String>(0)?, r.get::<_, u64>(1)?)))?;
|
||||
for row in rows {
|
||||
let (kind, n) = row?;
|
||||
media.insert(kind, n);
|
||||
@@ -53,9 +48,7 @@ pub fn breakdowns(
|
||||
"SELECT COALESCE(lang, 'null') AS l, COUNT(*) \
|
||||
FROM documents GROUP BY l",
|
||||
)?;
|
||||
let rows = stmt.query_map([], |r| {
|
||||
Ok((r.get::<_, String>(0)?, r.get::<_, u64>(1)?))
|
||||
})?;
|
||||
let rows = stmt.query_map([], |r| Ok((r.get::<_, String>(0)?, r.get::<_, u64>(1)?)))?;
|
||||
for row in rows {
|
||||
let (l, n) = row?;
|
||||
lang.insert(l, n);
|
||||
@@ -65,8 +58,7 @@ pub fn breakdowns(
|
||||
0
|
||||
} else {
|
||||
let secs = (threshold_days as i64) * 86_400;
|
||||
let cutoff = time::OffsetDateTime::now_utc()
|
||||
- time::Duration::seconds(secs);
|
||||
let cutoff = time::OffsetDateTime::now_utc() - time::Duration::seconds(secs);
|
||||
let cutoff_str = cutoff
|
||||
.format(&time::format_description::well_known::Rfc3339)
|
||||
.expect("RFC3339 format");
|
||||
@@ -148,7 +140,10 @@ mod tests {
|
||||
fn index_bytes_includes_sqlite_main() {
|
||||
let (dir, _store) = open_fresh();
|
||||
let b = index_bytes(dir.path()).unwrap();
|
||||
assert!(b.sqlite > 0, "main sqlite file should exist after migrations");
|
||||
assert!(
|
||||
b.sqlite > 0,
|
||||
"main sqlite file should exist after migrations"
|
||||
);
|
||||
assert_eq!(b.lancedb, 0);
|
||||
}
|
||||
|
||||
|
||||
@@ -163,7 +163,9 @@ impl SqliteStore {
|
||||
/// safe to reuse — we simply unwrap the inner guard rather than
|
||||
/// propagate the panic to every subsequent call.
|
||||
pub(crate) fn lock_conn(&self) -> MutexGuard<'_, Connection> {
|
||||
self.conn.lock().unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
self.conn
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
}
|
||||
|
||||
/// Read-only borrow of the connection.
|
||||
@@ -179,7 +181,9 @@ impl SqliteStore {
|
||||
///
|
||||
/// Poisoning is recovered the same way as [`Self::lock_conn`].
|
||||
pub fn read_conn(&self) -> MutexGuard<'_, Connection> {
|
||||
self.conn.lock().unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
self.conn
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
}
|
||||
|
||||
/// Persist a `RawAsset` *with its raw bytes*: row goes into `assets`,
|
||||
@@ -190,11 +194,7 @@ impl SqliteStore {
|
||||
/// In either branch, `blake3(bytes)` is recomputed and compared to
|
||||
/// `asset.checksum.0`. A mismatch returns
|
||||
/// `StoreError::Conflict` wrapped in `anyhow::Error`.
|
||||
pub fn put_asset_with_bytes(
|
||||
&self,
|
||||
asset: &kebab_core::RawAsset,
|
||||
bytes: &[u8],
|
||||
) -> Result<()> {
|
||||
pub fn put_asset_with_bytes(&self, asset: &kebab_core::RawAsset, bytes: &[u8]) -> Result<()> {
|
||||
// 0. Validate the AssetId shape before any I/O. `kebab_core::AssetId`
|
||||
// is a `pub String` newtype: `FromStr` enforces the 32-hex-char
|
||||
// invariant, but a hand-constructed `AssetId("../etc/passwd…")`
|
||||
@@ -229,9 +229,8 @@ impl SqliteStore {
|
||||
// of the temp file so we never leak bytes on disk.
|
||||
let dest = self.assets_path_for(&asset.asset_id);
|
||||
if let Some(parent) = dest.parent() {
|
||||
std::fs::create_dir_all(parent).with_context(|| {
|
||||
format!("create asset shard dir {}", parent.display())
|
||||
})?;
|
||||
std::fs::create_dir_all(parent)
|
||||
.with_context(|| format!("create asset shard dir {}", parent.display()))?;
|
||||
}
|
||||
let temp_path = temp_path_for(&dest);
|
||||
// Inline closure so any `?` in (a)/(b) cleans up the temp
|
||||
@@ -242,9 +241,8 @@ impl SqliteStore {
|
||||
format!("create temp asset file {}", temp_path.display())
|
||||
})?;
|
||||
use std::io::Write;
|
||||
f.write_all(bytes).with_context(|| {
|
||||
format!("write asset bytes to {}", temp_path.display())
|
||||
})?;
|
||||
f.write_all(bytes)
|
||||
.with_context(|| format!("write asset bytes to {}", temp_path.display()))?;
|
||||
f.sync_all().with_context(|| {
|
||||
format!("fsync temp asset file {}", temp_path.display())
|
||||
})?;
|
||||
@@ -255,9 +253,8 @@ impl SqliteStore {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let mut perms = std::fs::metadata(&temp_path)?.permissions();
|
||||
perms.set_mode(0o644);
|
||||
std::fs::set_permissions(&temp_path, perms).with_context(|| {
|
||||
format!("chmod 0o644 on {}", temp_path.display())
|
||||
})?;
|
||||
std::fs::set_permissions(&temp_path, perms)
|
||||
.with_context(|| format!("chmod 0o644 on {}", temp_path.display()))?;
|
||||
}
|
||||
// UPSERT the row first; only after a successful row write
|
||||
// do we publish the file via rename. A second
|
||||
@@ -270,12 +267,7 @@ impl SqliteStore {
|
||||
&asset.workspace_path.0,
|
||||
&asset.asset_id.0,
|
||||
)?;
|
||||
upsert_asset_row(
|
||||
&conn,
|
||||
asset,
|
||||
"copied",
|
||||
&dest.to_string_lossy(),
|
||||
)?;
|
||||
upsert_asset_row(&conn, asset, "copied", &dest.to_string_lossy())?;
|
||||
}
|
||||
std::fs::rename(&temp_path, &dest).with_context(|| {
|
||||
format!(
|
||||
@@ -305,11 +297,7 @@ impl SqliteStore {
|
||||
kebab_core::SourceUri::Kb(u) => u.clone(),
|
||||
};
|
||||
let conn = self.lock_conn();
|
||||
purge_orphan_at_workspace_path(
|
||||
&conn,
|
||||
&asset.workspace_path.0,
|
||||
&asset.asset_id.0,
|
||||
)?;
|
||||
purge_orphan_at_workspace_path(&conn, &asset.workspace_path.0, &asset.asset_id.0)?;
|
||||
upsert_asset_row(&conn, asset, "reference", &storage_path)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -338,9 +326,7 @@ impl SqliteStore {
|
||||
/// permits hand-construction, so any function that turns an `AssetId`
|
||||
/// into a filesystem path must call this first.
|
||||
pub(crate) fn validate_asset_id(asset_id: &kebab_core::AssetId) -> Result<()> {
|
||||
if asset_id.0.len() != ASSET_ID_HEX_LEN
|
||||
|| !asset_id.0.bytes().all(|b| b.is_ascii_hexdigit())
|
||||
{
|
||||
if asset_id.0.len() != ASSET_ID_HEX_LEN || !asset_id.0.bytes().all(|b| b.is_ascii_hexdigit()) {
|
||||
anyhow::bail!(
|
||||
"invalid AssetId shape (expected {} ASCII hex chars): {:?}",
|
||||
ASSET_ID_HEX_LEN,
|
||||
@@ -359,7 +345,8 @@ fn temp_path_for(dest: &Path) -> PathBuf {
|
||||
let n = TEMP_SUFFIX_COUNTER.fetch_add(1, Ordering::Relaxed);
|
||||
let parent = dest.parent().unwrap_or_else(|| Path::new("."));
|
||||
let file_name = dest
|
||||
.file_name().map_or_else(|| "asset".to_string(), |s| s.to_string_lossy().into_owned());
|
||||
.file_name()
|
||||
.map_or_else(|| "asset".to_string(), |s| s.to_string_lossy().into_owned());
|
||||
parent.join(format!("{file_name}.tmp.{pid}.{n}"))
|
||||
}
|
||||
|
||||
@@ -742,8 +729,7 @@ pub(crate) fn upsert_asset_row(
|
||||
kebab_core::SourceUri::File(p) => format!("file://{}", p.to_string_lossy()),
|
||||
kebab_core::SourceUri::Kb(u) => u.clone(),
|
||||
};
|
||||
let media_type = serde_json::to_string(&asset.media_type)
|
||||
.context("serialize media_type")?;
|
||||
let media_type = serde_json::to_string(&asset.media_type).context("serialize media_type")?;
|
||||
let discovered_at = asset
|
||||
.discovered_at
|
||||
.format(&time::format_description::well_known::Rfc3339)
|
||||
@@ -864,9 +850,7 @@ impl SqliteStore {
|
||||
/// skips rows where `code_lang` is NULL (i.e. non-code documents).
|
||||
/// Returns `BTreeMap<String, u32>` — key is the canonical lowercase
|
||||
/// language identifier (e.g. `"rust"`), value is the doc count.
|
||||
pub fn code_lang_breakdown(
|
||||
&self,
|
||||
) -> anyhow::Result<std::collections::BTreeMap<String, u32>> {
|
||||
pub fn code_lang_breakdown(&self) -> anyhow::Result<std::collections::BTreeMap<String, u32>> {
|
||||
use anyhow::Context;
|
||||
let conn = self.read_conn();
|
||||
let mut stmt = conn
|
||||
@@ -936,9 +920,7 @@ impl SqliteStore {
|
||||
/// where `repo` is NULL (documents without an explicit repo tag).
|
||||
/// Returns `BTreeMap<String, u32>` — key is the repo name as stored in
|
||||
/// frontmatter, value is the doc count.
|
||||
pub fn repo_breakdown(
|
||||
&self,
|
||||
) -> anyhow::Result<std::collections::BTreeMap<String, u32>> {
|
||||
pub fn repo_breakdown(&self) -> anyhow::Result<std::collections::BTreeMap<String, u32>> {
|
||||
use anyhow::Context;
|
||||
let conn = self.read_conn();
|
||||
let mut stmt = conn
|
||||
@@ -1299,4 +1281,3 @@ mod tests {
|
||||
assert_eq!(bd.len(), 1, "expected exactly 1 entry, got: {bd:?}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -44,8 +44,16 @@ fn copy_mode_writes_file_with_0o644_and_correct_bytes() {
|
||||
|
||||
// Path: data_dir/assets/aa/aaaaaa…aa
|
||||
let aa = &asset.asset_id.0[..2];
|
||||
let dest = env.data_dir().join("assets").join(aa).join(&asset.asset_id.0);
|
||||
assert!(dest.exists(), "asset file not written at {}", dest.display());
|
||||
let dest = env
|
||||
.data_dir()
|
||||
.join("assets")
|
||||
.join(aa)
|
||||
.join(&asset.asset_id.0);
|
||||
assert!(
|
||||
dest.exists(),
|
||||
"asset file not written at {}",
|
||||
dest.display()
|
||||
);
|
||||
let on_disk = std::fs::read(&dest).unwrap();
|
||||
assert_eq!(on_disk, bytes);
|
||||
|
||||
@@ -82,10 +90,16 @@ fn reference_mode_does_not_write_file_but_records_path() {
|
||||
let mut asset = fixed_asset(bytes, 1, &cs);
|
||||
asset.source_uri = SourceUri::File(PathBuf::from("/path/to/original.md"));
|
||||
|
||||
store.put_asset_with_bytes(&asset, bytes).expect("ref write");
|
||||
store
|
||||
.put_asset_with_bytes(&asset, bytes)
|
||||
.expect("ref write");
|
||||
|
||||
let aa = &asset.asset_id.0[..2];
|
||||
let dest = env.data_dir().join("assets").join(aa).join(&asset.asset_id.0);
|
||||
let dest = env
|
||||
.data_dir()
|
||||
.join("assets")
|
||||
.join(aa)
|
||||
.join(&asset.asset_id.0);
|
||||
assert!(!dest.exists(), "reference mode must not copy bytes");
|
||||
|
||||
let (storage_kind, storage_path): (String, String) = env.with_conn(|c| {
|
||||
@@ -161,11 +175,18 @@ fn put_asset_with_bytes_sweeps_workspace_path_orphan() {
|
||||
|row| row.get(0),
|
||||
)
|
||||
});
|
||||
assert_eq!(new_count, 1, "new asset_id must own the workspace_path slot");
|
||||
assert_eq!(
|
||||
new_count, 1,
|
||||
"new asset_id must own the workspace_path slot"
|
||||
);
|
||||
|
||||
// New asset's bytes published at the final destination.
|
||||
let aa = &asset.asset_id.0[..2];
|
||||
let dest = env.data_dir().join("assets").join(aa).join(&asset.asset_id.0);
|
||||
let dest = env
|
||||
.data_dir()
|
||||
.join("assets")
|
||||
.join(aa)
|
||||
.join(&asset.asset_id.0);
|
||||
assert!(
|
||||
dest.exists(),
|
||||
"new asset bytes must be visible at {}",
|
||||
@@ -185,7 +206,11 @@ fn put_asset_with_bytes_rejects_invalid_asset_id() {
|
||||
// 32 chars but contains a `/` — would let `assets_path_for` stitch
|
||||
// together a path outside the shard tree.
|
||||
let evil_id = "../etc/passwd_padded_to_xx_xxxxx".to_string();
|
||||
assert_eq!(evil_id.len(), 32, "test fixture must be 32 chars to exercise length-only checks");
|
||||
assert_eq!(
|
||||
evil_id.len(),
|
||||
32,
|
||||
"test fixture must be 32 chars to exercise length-only checks"
|
||||
);
|
||||
let mut asset = fixed_asset(b"x", 1, &b3_full_hex(b"x"));
|
||||
asset.asset_id = AssetId(evil_id.clone());
|
||||
|
||||
|
||||
@@ -49,7 +49,10 @@ fn create_get_roundtrip() {
|
||||
let store = open_store(&tmp);
|
||||
let session = make_session("sess-1");
|
||||
store.create_session(&session).unwrap();
|
||||
let fetched = store.get_session("sess-1").unwrap().expect("session present");
|
||||
let fetched = store
|
||||
.get_session("sess-1")
|
||||
.unwrap()
|
||||
.expect("session present");
|
||||
assert_eq!(fetched, session);
|
||||
}
|
||||
|
||||
@@ -112,20 +115,16 @@ fn append_turn_bumps_session_updated_at() {
|
||||
let store = open_store(&tmp);
|
||||
let session = make_session("bump");
|
||||
store.create_session(&session).unwrap();
|
||||
let pre = store
|
||||
.get_session("bump")
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.updated_at;
|
||||
let pre = store.get_session("bump").unwrap().unwrap().updated_at;
|
||||
let mut t = make_turn("bump", 0);
|
||||
t.created_at = pre + 100;
|
||||
store.append_turn(&t).unwrap();
|
||||
let post = store
|
||||
.get_session("bump")
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.updated_at;
|
||||
assert_eq!(post, pre + 100, "updated_at must follow latest turn's created_at");
|
||||
let post = store.get_session("bump").unwrap().unwrap().updated_at;
|
||||
assert_eq!(
|
||||
post,
|
||||
pre + 100,
|
||||
"updated_at must follow latest turn's created_at"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -168,7 +167,9 @@ fn list_sessions_respects_limit() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let store = open_store(&tmp);
|
||||
for i in 0..5 {
|
||||
store.create_session(&make_session(&format!("s{i}"))).unwrap();
|
||||
store
|
||||
.create_session(&make_session(&format!("s{i}")))
|
||||
.unwrap();
|
||||
}
|
||||
assert_eq!(store.list_sessions(2).unwrap().len(), 2);
|
||||
assert_eq!(store.list_sessions(100).unwrap().len(), 5);
|
||||
|
||||
@@ -10,7 +10,7 @@ use std::path::PathBuf;
|
||||
|
||||
use kebab_chunk::MdHeadingV1Chunker;
|
||||
use kebab_core::{
|
||||
AssetId, AssetStorage, Checksum, ChunkPolicy, ChunkerVersion, Chunker, DocumentStore,
|
||||
AssetId, AssetStorage, Checksum, ChunkPolicy, Chunker, ChunkerVersion, DocumentStore,
|
||||
MediaType, ParserVersion, RawAsset, SourceUri, WorkspacePath,
|
||||
};
|
||||
use kebab_parse_md::{BodyHints, build_canonical_document, parse_blocks, parse_frontmatter};
|
||||
@@ -58,8 +58,7 @@ fn document_and_chunks_round_trip_through_sqlite() {
|
||||
fs_mtime: asset.discovered_at,
|
||||
fallback_lang: Some("en".into()),
|
||||
};
|
||||
let (mut metadata, _fm_span, _fm_warns) =
|
||||
parse_frontmatter(&bytes, &hints).unwrap();
|
||||
let (mut metadata, _fm_span, _fm_warns) = parse_frontmatter(&bytes, &hints).unwrap();
|
||||
let (parsed_blocks, parse_warns) = parse_blocks(&bytes, 1).unwrap();
|
||||
|
||||
metadata.aliases.sort();
|
||||
@@ -91,9 +90,7 @@ fn document_and_chunks_round_trip_through_sqlite() {
|
||||
store
|
||||
.put_blocks(&doc.doc_id, &doc.blocks)
|
||||
.expect("put_blocks");
|
||||
store
|
||||
.put_chunks(&doc.doc_id, &chunks)
|
||||
.expect("put_chunks");
|
||||
store.put_chunks(&doc.doc_id, &chunks).expect("put_chunks");
|
||||
|
||||
// ── Read back ────────────────────────────────────────────────────
|
||||
let loaded = store
|
||||
|
||||
@@ -334,9 +334,7 @@ fn normalize_ws(s: &str) -> String {
|
||||
/// - no `CREATE VIRTUAL TABLE chunks_fts` inside that block
|
||||
/// - no `END;` after the virtual-table line
|
||||
fn extract_design_5_5_fts_block() -> String {
|
||||
let doc = include_str!(
|
||||
"../../../docs/superpowers/specs/2026-04-27-kebab-final-form-design.md"
|
||||
);
|
||||
let doc = include_str!("../../../docs/superpowers/specs/2026-04-27-kebab-final-form-design.md");
|
||||
let heading_idx = doc
|
||||
.find("### 5.5 Chunks + FTS5")
|
||||
.expect("design doc must contain `### 5.5 Chunks + FTS5` heading");
|
||||
@@ -394,9 +392,7 @@ fn extract_migration_5_5_verbatim_block() -> String {
|
||||
.expect("V007 must carry the `End §5.5 verbatim block` closing anchor")
|
||||
+ after_open_line;
|
||||
// Walk back from the close marker to the start of its comment line.
|
||||
let close_line_start = migration[..close_idx]
|
||||
.rfind('\n')
|
||||
.map_or(0, |n| n + 1);
|
||||
let close_line_start = migration[..close_idx].rfind('\n').map_or(0, |n| n + 1);
|
||||
|
||||
migration[after_open_line..close_line_start].to_string()
|
||||
}
|
||||
@@ -476,8 +472,7 @@ fn fts_store_drop_releases_wal_files() {
|
||||
}
|
||||
// The main DB file should likewise be removable.
|
||||
if db_path.exists() {
|
||||
std::fs::remove_file(&db_path)
|
||||
.expect("main DB file should be removable after store drop");
|
||||
std::fs::remove_file(&db_path).expect("main DB file should be removable after store drop");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -584,11 +579,7 @@ fn fts_trigram_english_substring_hits() {
|
||||
1,
|
||||
"substring of 'tokenizer' — trigram recall"
|
||||
);
|
||||
assert_eq!(
|
||||
count_match(&conn, "izer"),
|
||||
1,
|
||||
"substring of 'tokenizer'"
|
||||
);
|
||||
assert_eq!(count_match(&conn, "izer"), 1, "substring of 'tokenizer'");
|
||||
// 3-char-minimum applies to English too.
|
||||
assert_eq!(count_match(&conn, "to"), 0, "2-char English query");
|
||||
}
|
||||
|
||||
@@ -5,10 +5,9 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use kebab_core::{
|
||||
AssetId, AssetStorage, Block, CanonicalDocument, Checksum, Chunk, ChunkerVersion,
|
||||
CommonBlock, DocumentId, DocumentStore, HeadingBlock, Lang, MediaType, Metadata,
|
||||
ParserVersion, Provenance, RawAsset, SourceSpan, SourceType, SourceUri, TextBlock,
|
||||
TrustLevel, WorkspacePath,
|
||||
AssetId, AssetStorage, Block, CanonicalDocument, Checksum, Chunk, ChunkerVersion, CommonBlock,
|
||||
DocumentId, DocumentStore, HeadingBlock, Lang, MediaType, Metadata, ParserVersion, Provenance,
|
||||
RawAsset, SourceSpan, SourceType, SourceUri, TextBlock, TrustLevel, WorkspacePath,
|
||||
};
|
||||
use kebab_store_sqlite::SqliteStore;
|
||||
use time::OffsetDateTime;
|
||||
@@ -66,7 +65,7 @@ fn make_doc() -> CanonicalDocument {
|
||||
block_id: kebab_core::BlockId("c".repeat(32)),
|
||||
heading_path: vec!["Title".into()],
|
||||
source_span: span,
|
||||
},
|
||||
},
|
||||
text: "body".into(),
|
||||
inlines: vec![],
|
||||
});
|
||||
@@ -138,8 +137,7 @@ fn put_document_idempotent_bumps_doc_version() {
|
||||
|
||||
// Tags were re-derived: still exactly the two original tags.
|
||||
let tags: Vec<String> = env.with_conn(|c| {
|
||||
let mut stmt =
|
||||
c.prepare("SELECT tag FROM document_tags WHERE doc_id = ? ORDER BY tag")?;
|
||||
let mut stmt = c.prepare("SELECT tag FROM document_tags WHERE doc_id = ? ORDER BY tag")?;
|
||||
let rows = stmt.query_map([&doc.doc_id.0], |r| r.get::<_, String>(0))?;
|
||||
rows.collect::<rusqlite::Result<Vec<String>>>()
|
||||
});
|
||||
@@ -158,7 +156,9 @@ fn put_blocks_and_put_chunks_replace_not_duplicate() {
|
||||
store.put_document(&doc).unwrap();
|
||||
|
||||
store.put_blocks(&doc.doc_id, &doc.blocks).unwrap();
|
||||
store.put_chunks(&doc.doc_id, &make_chunks(&doc.doc_id)).unwrap();
|
||||
store
|
||||
.put_chunks(&doc.doc_id, &make_chunks(&doc.doc_id))
|
||||
.unwrap();
|
||||
|
||||
let (b1, ch1): (i64, i64) = env.with_conn(|c| {
|
||||
Ok((
|
||||
@@ -179,7 +179,9 @@ fn put_blocks_and_put_chunks_replace_not_duplicate() {
|
||||
|
||||
// Re-put same data → counts unchanged (DELETE-then-INSERT).
|
||||
store.put_blocks(&doc.doc_id, &doc.blocks).unwrap();
|
||||
store.put_chunks(&doc.doc_id, &make_chunks(&doc.doc_id)).unwrap();
|
||||
store
|
||||
.put_chunks(&doc.doc_id, &make_chunks(&doc.doc_id))
|
||||
.unwrap();
|
||||
let (b2, ch2): (i64, i64) = env.with_conn(|c| {
|
||||
Ok((
|
||||
c.query_row(
|
||||
@@ -214,9 +216,8 @@ fn put_blocks_transactional_rollback_on_fk_violation() {
|
||||
store.put_document(&doc).unwrap();
|
||||
// Establish a baseline row in `blocks`.
|
||||
store.put_blocks(&doc.doc_id, &doc.blocks).unwrap();
|
||||
let baseline: i64 = env.with_conn(|c| {
|
||||
c.query_row("SELECT COUNT(*) FROM blocks", [], |r| r.get(0))
|
||||
});
|
||||
let baseline: i64 =
|
||||
env.with_conn(|c| c.query_row("SELECT COUNT(*) FROM blocks", [], |r| r.get(0)));
|
||||
assert_eq!(baseline, 2);
|
||||
|
||||
// Now ask put_blocks to write to a doc_id that does NOT exist.
|
||||
@@ -237,9 +238,8 @@ fn put_blocks_transactional_rollback_on_fk_violation() {
|
||||
let res = store.put_blocks(&phantom, &phantom_blocks);
|
||||
assert!(res.is_err(), "FK violation must surface as Err");
|
||||
|
||||
let after: i64 = env.with_conn(|c| {
|
||||
c.query_row("SELECT COUNT(*) FROM blocks", [], |r| r.get(0))
|
||||
});
|
||||
let after: i64 =
|
||||
env.with_conn(|c| c.query_row("SELECT COUNT(*) FROM blocks", [], |r| r.get(0)));
|
||||
assert_eq!(
|
||||
after, baseline,
|
||||
"transaction must roll back; blocks count must be unchanged"
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use kebab_core::{
|
||||
AssetId, ChunkerVersion, DocumentId, IngestItem, IngestItemKind, IngestReport,
|
||||
ParserVersion, SourceScope, WorkspacePath,
|
||||
AssetId, ChunkerVersion, DocumentId, IngestItem, IngestItemKind, IngestReport, ParserVersion,
|
||||
SourceScope, WorkspacePath,
|
||||
};
|
||||
use serde_json::Value;
|
||||
|
||||
|
||||
@@ -29,9 +29,7 @@ fn create_then_progress_then_finish() {
|
||||
assert_eq!(row[0].progress.as_ref().unwrap()["total"], json!(10));
|
||||
|
||||
// Finish with success.
|
||||
store
|
||||
.finish(&id, JobStatus::Succeeded, None)
|
||||
.unwrap();
|
||||
store.finish(&id, JobStatus::Succeeded, None).unwrap();
|
||||
let row = store.list(&JobFilter::default()).unwrap();
|
||||
assert_eq!(row[0].status, JobStatus::Succeeded);
|
||||
assert!(row[0].finished_at.is_some());
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use kebab_core::{
|
||||
AssetId, AssetStorage, Block, CanonicalDocument, Checksum, CommonBlock, DocFilter,
|
||||
DocumentId, DocumentStore, HeadingBlock, Lang, MediaType, Metadata, ParserVersion,
|
||||
Provenance, RawAsset, SourceSpan, SourceType, SourceUri, TrustLevel, WorkspacePath,
|
||||
AssetId, AssetStorage, Block, CanonicalDocument, Checksum, CommonBlock, DocFilter, DocumentId,
|
||||
DocumentStore, HeadingBlock, Lang, MediaType, Metadata, ParserVersion, Provenance, RawAsset,
|
||||
SourceSpan, SourceType, SourceUri, TrustLevel, WorkspacePath,
|
||||
};
|
||||
use kebab_store_sqlite::SqliteStore;
|
||||
use time::OffsetDateTime;
|
||||
@@ -84,7 +84,13 @@ fn list_documents_filters_lang_and_tags() {
|
||||
store.run_migrations().unwrap();
|
||||
|
||||
for (asset, doc) in [
|
||||
make_doc('a', "notes/a.md", "en", vec!["rust", "kb"], TrustLevel::Primary),
|
||||
make_doc(
|
||||
'a',
|
||||
"notes/a.md",
|
||||
"en",
|
||||
vec!["rust", "kb"],
|
||||
TrustLevel::Primary,
|
||||
),
|
||||
make_doc('b', "notes/b.md", "ko", vec!["rust"], TrustLevel::Secondary),
|
||||
make_doc('c', "papers/c.md", "en", vec!["bio"], TrustLevel::Generated),
|
||||
] {
|
||||
|
||||
@@ -23,5 +23,8 @@ fn open_existing_does_not_create_missing_db() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let nonexistent_db = dir.path().join("does-not-exist.sqlite");
|
||||
let _ = SqliteStore::open_existing(&nonexistent_db);
|
||||
assert!(!nonexistent_db.exists(), "open_existing must NOT create the file");
|
||||
assert!(
|
||||
!nonexistent_db.exists(),
|
||||
"open_existing must NOT create the file"
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user