Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/batch-pearl-labels.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@smooai/smooth': patch
---

smooth-pearls: kill the N+1 label query in list-style reads. `ready`/`list`/
`blocked`/`search`/`due_scheduled` each fetched labels with one Dolt query
**per pearl** — every `th prime` / `th pearls ready` at session start cold-booted
Dolt ~40 times (~5.7s). A single `WHERE pearl_id IN (…)` batch collapses that to
2 queries: `th pearls ready` against a 1200-pearl store drops from 5.7s to ~0.7s.
100 changes: 64 additions & 36 deletions crates/smooth-pearls/src/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,36 @@ impl PearlStore {
Ok(pearl)
}

/// Populate `.labels` for a whole batch of pearls in a SINGLE Dolt query.
///
/// The per-pearl [`load_pearl_with_labels`](Self::load_pearl_with_labels)
/// path is an N+1: every list-style query (`ready`/`list`/`blocked`/…) then
/// cold-boots Dolt once per row just to fetch labels, which dominated
/// session-start latency (`th prime` → `th pearls ready` ≈ 5.7s for ~40
/// open pearls). One `WHERE pearl_id IN (…)` collapses that to 2 queries
/// total. Order of `pearls` is preserved; labels come back label-sorted.
fn attach_labels(&self, mut pearls: Vec<Pearl>) -> Result<Vec<Pearl>> {
if pearls.is_empty() {
return Ok(pearls);
}
let in_list = pearls.iter().map(|p| format!("'{}'", sql_escape(&p.id))).collect::<Vec<_>>().join(",");
let rows = self.dolt.sql(&format!(
"SELECT pearl_id, label FROM pearl_labels WHERE pearl_id IN ({in_list}) ORDER BY label"
))?;
let mut by_id: std::collections::HashMap<String, Vec<String>> = std::collections::HashMap::new();
for r in &rows {
if let (Some(pid), Some(label)) = (r["pearl_id"].as_str(), r["label"].as_str()) {
by_id.entry(pid.to_string()).or_default().push(label.to_string());
}
}
for p in &mut pearls {
if let Some(labels) = by_id.remove(&p.id) {
p.labels = labels;
}
}
Ok(pearls)
}

// ── Dolt version control ────────────────────────────────────────────

/// View the Dolt commit log.
Expand Down Expand Up @@ -576,12 +606,8 @@ impl PearlStore {
}

let rows = self.dolt.sql(&sql)?;
let mut result = Vec::with_capacity(rows.len());
for row in &rows {
let pearl = Self::parse_pearl(row)?;
result.push(self.load_pearl_with_labels(pearl)?);
}
Ok(result)
let pearls = rows.iter().map(Self::parse_pearl).collect::<Result<Vec<_>>>()?;
self.attach_labels(pearls)
}

/// Update a pearl with partial changes. Records history for each changed field.
Expand Down Expand Up @@ -800,12 +826,8 @@ impl PearlStore {
WHERE d.pearl_id = '{}' AND d.dep_type = 'blocks' AND p.status != 'closed'",
sql_escape(id),
))?;
let mut result = Vec::with_capacity(rows.len());
for row in &rows {
let pearl = Self::parse_pearl(row)?;
result.push(self.load_pearl_with_labels(pearl)?);
}
Ok(result)
let pearls = rows.iter().map(Self::parse_pearl).collect::<Result<Vec<_>>>()?;
self.attach_labels(pearls)
}

/// Get all dependencies for a pearl.
Expand Down Expand Up @@ -907,12 +929,8 @@ impl PearlStore {
) \
ORDER BY p.priority ASC, p.created_at DESC",
)?;
let mut result = Vec::with_capacity(rows.len());
for row in &rows {
let pearl = Self::parse_pearl(row)?;
result.push(self.load_pearl_with_labels(pearl)?);
}
Ok(result)
let pearls = rows.iter().map(Self::parse_pearl).collect::<Result<Vec<_>>>()?;
self.attach_labels(pearls)
}

/// Scheduled pearls whose time has arrived: `scheduled_at <= now` and not
Expand All @@ -929,12 +947,8 @@ impl PearlStore {
WHERE p.scheduled_at IS NOT NULL AND p.scheduled_at <= '{now}' AND p.status != 'closed' \
ORDER BY p.scheduled_at ASC",
))?;
let mut result = Vec::with_capacity(rows.len());
for row in &rows {
let pearl = Self::parse_pearl(row)?;
result.push(self.load_pearl_with_labels(pearl)?);
}
Ok(result)
let pearls = rows.iter().map(Self::parse_pearl).collect::<Result<Vec<_>>>()?;
self.attach_labels(pearls)
}

/// Pearls that have unresolved blocking dependencies.
Expand All @@ -946,12 +960,8 @@ impl PearlStore {
WHERE d.dep_type = 'blocks' AND blocker.status != 'closed' AND p.status != 'closed' \
ORDER BY p.priority ASC",
)?;
let mut result = Vec::with_capacity(rows.len());
for row in &rows {
let pearl = Self::parse_pearl(row)?;
result.push(self.load_pearl_with_labels(pearl)?);
}
Ok(result)
let pearls = rows.iter().map(Self::parse_pearl).collect::<Result<Vec<_>>>()?;
self.attach_labels(pearls)
}

/// Full-text search on title and description (LIKE-based).
Expand All @@ -960,12 +970,8 @@ impl PearlStore {
let rows = self.dolt.sql(&format!(
"SELECT * FROM pearls WHERE title LIKE '%{pattern}%' OR description LIKE '%{pattern}%' ORDER BY priority ASC, created_at DESC",
))?;
let mut result = Vec::with_capacity(rows.len());
for row in &rows {
let pearl = Self::parse_pearl(row)?;
result.push(self.load_pearl_with_labels(pearl)?);
}
Ok(result)
let pearls = rows.iter().map(Self::parse_pearl).collect::<Result<Vec<_>>>()?;
self.attach_labels(pearls)
}

/// Aggregate stats across all pearls.
Expand Down Expand Up @@ -1498,4 +1504,26 @@ mod tests {
PearlStore::migrate_schema(&store.dolt).expect("migrate idempotent on healed store");
assert!(PearlStore::column_exists(&store.dolt, "pearls", "scheduled_at").expect("still present"));
}

#[test]
fn list_batch_loads_labels_per_pearl() {
// Regression for th-2e1ad2: list-style queries batch-load labels in one
// query instead of N+1. Verify each pearl gets exactly its own labels
// (no cross-contamination) and a label-less pearl stays empty.
let Some(store) = test_store() else { return };
let a = store.create(&new_task("alpha")).unwrap();
let b = store.create(&new_task("bravo")).unwrap();
let _c = store.create(&new_task("charlie")).unwrap(); // no labels
store.add_label(&a.id, "backend").unwrap();
store.add_label(&a.id, "auth").unwrap();
store.add_label(&b.id, "frontend").unwrap();

let all = store.list(&PearlQuery::new()).unwrap();
let get = |id: &str| all.iter().find(|p| p.id == id).unwrap().labels.clone();

// add_label uses ORDER BY label → alphabetical.
assert_eq!(get(&a.id), vec!["auth".to_string(), "backend".to_string()]);
assert_eq!(get(&b.id), vec!["frontend".to_string()]);
assert!(get(&_c.id).is_empty(), "unlabelled pearl must have no labels");
}
}
Loading