1use rusqlite::{params, Connection};
22use std::io::Read;
23use std::io::Write as _;
24use std::io::Write;
25use std::io::{Seek, SeekFrom};
26use std::path::PathBuf;
27use std::time::{SystemTime, UNIX_EPOCH};
28
29pub struct HistoryEngine {
35 conn: Connection,
37}
38
39#[derive(Debug, Clone)]
45pub struct HistoryEntry {
46 pub id: i64,
48 pub command: String,
50 pub timestamp: i64,
52 pub duration_ms: Option<i64>,
54 pub exit_code: Option<i32>,
56 pub cwd: Option<String>,
58 pub frequency: u32,
60}
61
62impl HistoryEngine {
63 pub fn new() -> rusqlite::Result<Self> {
65 let path = Self::db_path();
66 if let Some(parent) = path.parent() {
67 std::fs::create_dir_all(parent).ok();
68 }
69
70 let _lowfd = crate::lowfd::LowFdGuard::new();
75 let conn = Connection::open(&path)?;
76 let engine = Self { conn };
77 engine.init_schema()?;
78 let count = engine.count().unwrap_or(0);
79 let db_size = std::fs::metadata(&path).map(|m| m.len()).unwrap_or(0);
80 tracing::info!(
81 entries = count,
82 db_bytes = db_size,
83 path = %path.display(),
84 "history: sqlite opened"
85 );
86
87 if let Err(e) = engine.rehydrate_text_if_stale() {
91 tracing::warn!(?e, "history: failed to rehydrate text mirror; continuing");
92 }
93 Ok(engine)
94 }
95 pub fn in_memory() -> rusqlite::Result<Self> {
97 let conn = Connection::open_in_memory()?;
98 let engine = Self { conn };
99 engine.init_schema()?;
100 Ok(engine)
101 }
102
103 fn db_path() -> PathBuf {
115 Self::root().join("zshrs_history.db")
116 }
117
118 pub fn text_path() -> PathBuf {
134 Self::root().join("zshrs_history")
135 }
136
137 fn root() -> PathBuf {
138 if let Some(custom) = std::env::var_os("ZSHRS_HOME") {
139 PathBuf::from(custom)
140 } else {
141 dirs::home_dir()
142 .unwrap_or_else(|| PathBuf::from("."))
143 .join(".zshrs")
144 }
145 }
146
147 fn init_schema(&self) -> rusqlite::Result<()> {
148 self.conn.execute_batch(r#"
149 CREATE TABLE IF NOT EXISTS history (
150 id INTEGER PRIMARY KEY,
151 command TEXT NOT NULL,
152 timestamp INTEGER NOT NULL,
153 duration_ms INTEGER,
154 exit_code INTEGER,
155 cwd TEXT,
156 frequency INTEGER DEFAULT 1
157 );
158
159 CREATE INDEX IF NOT EXISTS idx_history_timestamp ON history(timestamp DESC);
160 CREATE INDEX IF NOT EXISTS idx_history_cwd ON history(cwd);
161 CREATE UNIQUE INDEX IF NOT EXISTS idx_history_command ON history(command);
162
163 CREATE VIRTUAL TABLE IF NOT EXISTS history_fts USING fts5(
164 command,
165 content='history',
166 content_rowid='id',
167 tokenize='trigram'
168 );
169
170 CREATE TRIGGER IF NOT EXISTS history_ai AFTER INSERT ON history BEGIN
171 INSERT INTO history_fts(rowid, command) VALUES (new.id, new.command);
172 END;
173
174 CREATE TRIGGER IF NOT EXISTS history_ad AFTER DELETE ON history BEGIN
175 INSERT INTO history_fts(history_fts, rowid, command) VALUES('delete', old.id, old.command);
176 END;
177
178 CREATE TRIGGER IF NOT EXISTS history_au AFTER UPDATE ON history BEGIN
179 INSERT INTO history_fts(history_fts, rowid, command) VALUES('delete', old.id, old.command);
180 INSERT INTO history_fts(rowid, command) VALUES (new.id, new.command);
181 END;
182 "#)?;
183 Ok(())
184 }
185
186 fn now() -> i64 {
187 SystemTime::now()
188 .duration_since(UNIX_EPOCH)
189 .map(|d| d.as_secs() as i64)
190 .unwrap_or(0)
191 }
192
193 pub fn add(&self, command: &str, cwd: Option<&str>) -> rusqlite::Result<i64> {
195 let command = command.trim();
196 if command.is_empty() || command.starts_with(' ') {
197 return Ok(0);
198 }
199
200 let now = Self::now();
201
202 let updated = self.conn.execute(
204 "UPDATE history SET timestamp = ?1, frequency = frequency + 1, cwd = COALESCE(?2, cwd)
205 WHERE command = ?3",
206 params![now, cwd, command],
207 )?;
208
209 if updated > 0 {
210 let id: i64 = self.conn.query_row(
212 "SELECT id FROM history WHERE command = ?1",
213 params![command],
214 |row| row.get(0),
215 )?;
216 return Ok(id);
217 }
218
219 self.conn.execute(
221 "INSERT INTO history (command, timestamp, cwd) VALUES (?1, ?2, ?3)",
222 params![command, now, cwd],
223 )?;
224
225 let id = self.conn.last_insert_rowid();
226
227 if let Err(e) = append_text_line(now, 0, command) {
233 tracing::warn!(?e, "history: text mirror append failed");
234 }
235
236 Ok(id)
237 }
238
239 pub fn update_last(&self, id: i64, duration_ms: i64, exit_code: i32) -> rusqlite::Result<()> {
241 self.conn.execute(
242 "UPDATE history SET duration_ms = ?1, exit_code = ?2 WHERE id = ?3",
243 params![duration_ms, exit_code, id],
244 )?;
245
246 if let Ok((ts, command)) = self.conn.query_row(
250 "SELECT timestamp, command FROM history WHERE id = ?1",
251 params![id],
252 |row| Ok((row.get::<_, i64>(0)?, row.get::<_, String>(1)?)),
253 ) {
254 let duration_secs = (duration_ms / 1000).max(0);
255 if let Err(e) = rewrite_last_text_line(ts, duration_secs, &command) {
256 tracing::warn!(?e, "history: text mirror update failed");
257 }
258 }
259 Ok(())
260 }
261
262 fn rehydrate_text_if_stale(&self) -> rusqlite::Result<()> {
267 let text = Self::text_path();
268 let text_size = std::fs::metadata(&text).map(|m| m.len()).unwrap_or(0);
269 if text_size > 0 {
270 return Ok(());
271 }
272 let count: i64 = self
273 .conn
274 .query_row("SELECT COUNT(*) FROM history", [], |r| r.get(0))?;
275 if count == 0 {
276 return Ok(());
277 }
278 let mut stmt = self.conn.prepare(
279 "SELECT timestamp, COALESCE(duration_ms, 0), command \
280 FROM history ORDER BY timestamp ASC, id ASC",
281 )?;
282 let rows = stmt.query_map([], |r| {
283 Ok((
284 r.get::<_, i64>(0)?,
285 r.get::<_, i64>(1)?,
286 r.get::<_, String>(2)?,
287 ))
288 })?;
289 let file = std::fs::OpenOptions::new()
290 .create(true)
291 .truncate(true)
292 .write(true)
293 .open(&text)
294 .map_err(|e| rusqlite::Error::ToSqlConversionFailure(Box::new(e)))?;
295 let mut w = std::io::BufWriter::new(file);
296 let mut written: u64 = 0;
297 for row in rows {
298 let (ts, dur_ms, cmd) = row?;
299 let line = format_text_line(ts, (dur_ms / 1000).max(0), &cmd);
300 w.write_all(line.as_bytes())
301 .map_err(|e| rusqlite::Error::ToSqlConversionFailure(Box::new(e)))?;
302 written += 1;
303 }
304 w.flush()
305 .map_err(|e| rusqlite::Error::ToSqlConversionFailure(Box::new(e)))?;
306 tracing::info!(
307 entries = written,
308 path = %text.display(),
309 "history: rehydrated text mirror from sqlite index"
310 );
311 Ok(())
312 }
313
314 pub fn search(&self, query: &str, limit: usize) -> rusqlite::Result<Vec<HistoryEntry>> {
316 if query.is_empty() {
317 return self.recent(limit);
318 }
319
320 let escaped = query.replace('"', "\"\"");
322 let fts_query = format!("\"{}\"*", escaped);
323
324 let mut stmt = self.conn.prepare(
325 r#"SELECT h.id, h.command, h.timestamp, h.duration_ms, h.exit_code, h.cwd, h.frequency
326 FROM history h
327 JOIN history_fts f ON h.id = f.rowid
328 WHERE history_fts MATCH ?1
329 ORDER BY h.frequency DESC, h.timestamp DESC
330 LIMIT ?2"#,
331 )?;
332
333 let entries = stmt.query_map(params![fts_query, limit as i64], |row| {
334 Ok(HistoryEntry {
335 id: row.get(0)?,
336 command: row.get(1)?,
337 timestamp: row.get(2)?,
338 duration_ms: row.get(3)?,
339 exit_code: row.get(4)?,
340 cwd: row.get(5)?,
341 frequency: row.get(6)?,
342 })
343 })?;
344
345 entries.collect()
346 }
347
348 pub fn search_prefix(&self, prefix: &str, limit: usize) -> rusqlite::Result<Vec<HistoryEntry>> {
350 if prefix.is_empty() {
351 return self.recent(limit);
352 }
353
354 let mut stmt = self.conn.prepare(
355 r#"SELECT id, command, timestamp, duration_ms, exit_code, cwd, frequency
356 FROM history
357 WHERE command LIKE ?1 || '%' ESCAPE '\'
358 ORDER BY timestamp DESC
359 LIMIT ?2"#,
360 )?;
361
362 let escaped = prefix
364 .replace('\\', "\\\\")
365 .replace('%', "\\%")
366 .replace('_', "\\_");
367
368 let entries = stmt.query_map(params![escaped, limit as i64], |row| {
369 Ok(HistoryEntry {
370 id: row.get(0)?,
371 command: row.get(1)?,
372 timestamp: row.get(2)?,
373 duration_ms: row.get(3)?,
374 exit_code: row.get(4)?,
375 cwd: row.get(5)?,
376 frequency: row.get(6)?,
377 })
378 })?;
379
380 entries.collect()
381 }
382
383 pub fn search_prefix_cs(
390 &self,
391 prefix: &str,
392 limit: usize,
393 ) -> rusqlite::Result<Vec<HistoryEntry>> {
394 if prefix.is_empty() {
395 return self.recent(limit);
396 }
397
398 let mut stmt = self.conn.prepare_cached(
399 r#"SELECT id, command, timestamp, duration_ms, exit_code, cwd, frequency
400 FROM history
401 WHERE command GLOB ?1
402 ORDER BY timestamp DESC
403 LIMIT ?2"#,
404 )?;
405
406 let escaped = prefix
409 .replace('[', "[[]")
410 .replace('*', "[*]")
411 .replace('?', "[?]");
412 let pattern = format!("{escaped}*");
413
414 let entries = stmt.query_map(params![pattern, limit as i64], |row| {
415 Ok(HistoryEntry {
416 id: row.get(0)?,
417 command: row.get(1)?,
418 timestamp: row.get(2)?,
419 duration_ms: row.get(3)?,
420 exit_code: row.get(4)?,
421 cwd: row.get(5)?,
422 frequency: row.get(6)?,
423 })
424 })?;
425
426 entries.collect()
427 }
428
429 pub fn recent(&self, limit: usize) -> rusqlite::Result<Vec<HistoryEntry>> {
431 let mut stmt = self.conn.prepare(
432 r#"SELECT id, command, timestamp, duration_ms, exit_code, cwd, frequency
433 FROM history
434 ORDER BY timestamp DESC
435 LIMIT ?1"#,
436 )?;
437
438 let entries = stmt.query_map(params![limit as i64], |row| {
439 Ok(HistoryEntry {
440 id: row.get(0)?,
441 command: row.get(1)?,
442 timestamp: row.get(2)?,
443 duration_ms: row.get(3)?,
444 exit_code: row.get(4)?,
445 cwd: row.get(5)?,
446 frequency: row.get(6)?,
447 })
448 })?;
449
450 entries.collect()
451 }
452
453 pub fn for_directory(&self, cwd: &str, limit: usize) -> rusqlite::Result<Vec<HistoryEntry>> {
455 let mut stmt = self.conn.prepare(
456 r#"SELECT id, command, timestamp, duration_ms, exit_code, cwd, frequency
457 FROM history
458 WHERE cwd = ?1
459 ORDER BY frequency DESC, timestamp DESC
460 LIMIT ?2"#,
461 )?;
462
463 let entries = stmt.query_map(params![cwd, limit as i64], |row| {
464 Ok(HistoryEntry {
465 id: row.get(0)?,
466 command: row.get(1)?,
467 timestamp: row.get(2)?,
468 duration_ms: row.get(3)?,
469 exit_code: row.get(4)?,
470 cwd: row.get(5)?,
471 frequency: row.get(6)?,
472 })
473 })?;
474
475 entries.collect()
476 }
477
478 pub fn delete(&self, id: i64) -> rusqlite::Result<()> {
480 self.conn
481 .execute("DELETE FROM history WHERE id = ?1", params![id])?;
482 Ok(())
483 }
484
485 pub fn clear(&self) -> rusqlite::Result<()> {
487 self.conn.execute("DELETE FROM history", [])?;
488 Ok(())
489 }
490
491 pub fn count(&self) -> rusqlite::Result<i64> {
493 self.conn
494 .query_row("SELECT COUNT(*) FROM history", [], |row| row.get(0))
495 }
496
497 pub fn get_by_offset(&self, offset: usize) -> rusqlite::Result<Option<HistoryEntry>> {
499 let mut stmt = self.conn.prepare(
500 r#"SELECT id, command, timestamp, duration_ms, exit_code, cwd, frequency
501 FROM history
502 ORDER BY timestamp DESC
503 LIMIT 1 OFFSET ?1"#,
504 )?;
505
506 let mut rows = stmt.query(params![offset as i64])?;
507 if let Some(row) = rows.next()? {
508 Ok(Some(HistoryEntry {
509 id: row.get(0)?,
510 command: row.get(1)?,
511 timestamp: row.get(2)?,
512 duration_ms: row.get(3)?,
513 exit_code: row.get(4)?,
514 cwd: row.get(5)?,
515 frequency: row.get(6)?,
516 }))
517 } else {
518 Ok(None)
519 }
520 }
521
522 pub fn get_by_number(&self, num: i64) -> rusqlite::Result<Option<HistoryEntry>> {
524 let mut stmt = self.conn.prepare(
525 r#"SELECT id, command, timestamp, duration_ms, exit_code, cwd, frequency
526 FROM history
527 WHERE id = ?1"#,
528 )?;
529
530 let mut rows = stmt.query(params![num])?;
531 if let Some(row) = rows.next()? {
532 Ok(Some(HistoryEntry {
533 id: row.get(0)?,
534 command: row.get(1)?,
535 timestamp: row.get(2)?,
536 duration_ms: row.get(3)?,
537 exit_code: row.get(4)?,
538 cwd: row.get(5)?,
539 frequency: row.get(6)?,
540 }))
541 } else {
542 Ok(None)
543 }
544 }
545}
546
547thread_local! {
563 static SESSION_HISTORY: std::cell::RefCell<Option<Option<HistoryEngine>>> =
567 const { std::cell::RefCell::new(None) };
568 static HIST_PENDING: std::cell::Cell<Option<(i64, std::time::Instant)>> =
572 const { std::cell::Cell::new(None) };
573}
574
575pub fn with_session_engine<R>(f: impl FnOnce(&HistoryEngine) -> R) -> Option<R> {
576 SESSION_HISTORY.with(|cell| {
577 let mut slot = cell.borrow_mut();
578 if slot.is_none() {
579 *slot = Some(HistoryEngine::new().ok());
580 }
581 slot.as_ref().unwrap().as_ref().map(f)
582 })
583}
584
585pub fn history_sqlite_add(line: &str) {
588 let cwd = std::env::current_dir()
589 .ok()
590 .map(|p| p.to_string_lossy().to_string());
591 let id = with_session_engine(|e| e.add(line, cwd.as_deref()).ok()).flatten();
592 if let Some(id) = id {
593 HIST_PENDING.with(|p| p.set(Some((id, std::time::Instant::now()))));
594 }
595}
596
597pub fn history_sqlite_finish(exit_code: i32) {
600 let Some((id, start)) = HIST_PENDING.with(|p| p.take()) else {
601 return;
602 };
603 let dur = start.elapsed().as_millis() as i64;
604 let _ = with_session_engine(|e| e.update_last(id, dur, exit_code));
605}
606
607fn is_sqlite_file(path: &std::path::Path) -> bool {
612 let mut f = match std::fs::File::open(path) {
613 Ok(f) => f,
614 Err(_) => return false,
615 };
616 let mut header = [0u8; 16];
617 if f.read_exact(&mut header).is_err() {
618 return false;
619 }
620 &header == b"SQLite format 3\0"
621}
622
623fn format_text_line(ts: i64, duration_secs: i64, command: &str) -> String {
634 let escaped = command.replace('\\', "\\\\").replace('\n', "\\\n");
635 format!(": {}:{};{}\n", ts, duration_secs, escaped)
636}
637
638fn append_text_line(ts: i64, duration_secs: i64, command: &str) -> std::io::Result<()> {
640 let path = HistoryEngine::text_path();
641 if let Some(parent) = path.parent() {
642 std::fs::create_dir_all(parent).ok();
643 }
644 let line = format_text_line(ts, duration_secs, command);
645 let mut f = std::fs::OpenOptions::new()
646 .create(true)
647 .append(true)
648 .open(&path)?;
649 f.write_all(line.as_bytes())
650}
651
652fn rewrite_last_text_line(ts: i64, duration_secs: i64, command: &str) -> std::io::Result<()> {
659 let path = HistoryEngine::text_path();
660 let mut f = std::fs::OpenOptions::new()
661 .read(true)
662 .write(true)
663 .open(&path)?;
664 let len = f.metadata()?.len();
665 let max_tail = 65_536u64.min(len);
669 let read_from = len - max_tail;
670 f.seek(SeekFrom::Start(read_from))?;
671 let mut tail = Vec::with_capacity(max_tail as usize);
672 f.read_to_end(&mut tail)?;
673 let mut last_record_start = 0usize;
677 let mut nl_count = 0;
678 for (i, b) in tail.iter().enumerate().rev() {
679 if *b == b'\n' {
680 nl_count += 1;
681 if nl_count == 2 {
682 last_record_start = i + 1;
683 break;
684 }
685 }
686 }
687 let new_record = format_text_line(ts, duration_secs, command);
688 let new_abs = read_from + last_record_start as u64;
689 f.seek(SeekFrom::Start(new_abs))?;
690 f.write_all(new_record.as_bytes())?;
691 let new_len = new_abs + new_record.len() as u64;
692 if new_len < len {
693 f.set_len(new_len)?;
694 }
695 Ok(())
696}
697
698#[cfg(test)]
699mod tests {
700 use super::*;
701
702 #[test]
703 fn test_add_and_search() {
704 let _g = crate::test_util::global_state_lock();
705 let engine = HistoryEngine::in_memory().unwrap();
706
707 engine.add("ls -la", Some("/home/user")).unwrap();
708 engine.add("cd /tmp", Some("/home/user")).unwrap();
709 engine.add("echo hello", Some("/tmp")).unwrap();
710
711 let results = engine.search_prefix("ls", 10).unwrap();
713 assert_eq!(results.len(), 1);
714 assert_eq!(results[0].command, "ls -la");
715 }
716
717 #[test]
718 fn test_frequency_tracking() {
719 let _g = crate::test_util::global_state_lock();
720 let engine = HistoryEngine::in_memory().unwrap();
721
722 engine.add("git status", None).unwrap();
723 engine.add("git status", None).unwrap();
724 engine.add("git status", None).unwrap();
725
726 let results = engine.recent(10).unwrap();
727 assert_eq!(results.len(), 1);
728 assert_eq!(results[0].frequency, 3);
729 }
730
731 #[test]
732 fn test_prefix_search() {
733 let _g = crate::test_util::global_state_lock();
734 let engine = HistoryEngine::in_memory().unwrap();
735
736 engine.add("git status", None).unwrap();
737 engine.add("git commit -m 'test'", None).unwrap();
738 engine.add("grep foo bar", None).unwrap();
739
740 let results = engine.search_prefix("git", 10).unwrap();
741 assert_eq!(results.len(), 2);
742 }
743
744 #[test]
745 fn test_directory_history() {
746 let _g = crate::test_util::global_state_lock();
747 let engine = HistoryEngine::in_memory().unwrap();
748
749 engine.add("make build", Some("/project")).unwrap();
750 engine.add("cargo test", Some("/project")).unwrap();
751 engine.add("ls", Some("/tmp")).unwrap();
752
753 let results = engine.for_directory("/project", 10).unwrap();
754 assert_eq!(results.len(), 2);
755 }
756
757 #[test]
762 fn format_text_line_emits_canonical_prefix() {
763 let line = format_text_line(1_700_000_000, 5, "echo hi");
764 assert_eq!(line, ": 1700000000:5;echo hi\n");
765 }
766
767 #[test]
768 fn format_text_line_zero_duration_still_renders() {
769 let line = format_text_line(0, 0, "true");
770 assert_eq!(line, ": 0:0;true\n");
771 }
772
773 #[test]
774 fn format_text_line_escapes_literal_backslash() {
775 let line = format_text_line(1, 0, r"\n");
777 assert!(
779 line.ends_with(";\\\\n\n"),
780 "expected backslash-escape, got: {:?}",
781 line
782 );
783 }
784
785 #[test]
786 fn format_text_line_escapes_embedded_newline_to_backslash_newline() {
787 let line = format_text_line(2, 1, "line1\nline2");
790 assert!(
792 line.contains("line1\\\nline2"),
793 "expected escaped newline, got: {:?}",
794 line
795 );
796 assert!(line.ends_with('\n'));
798 }
799
800 #[test]
801 fn format_text_line_handles_empty_command() {
802 let line = format_text_line(42, 7, "");
803 assert_eq!(line, ": 42:7;\n");
804 }
805
806 #[test]
807 fn format_text_line_negative_duration_round_trips() {
808 let line = format_text_line(100, -1, "foo");
811 assert_eq!(line, ": 100:-1;foo\n");
812 }
813
814 #[test]
815 fn format_text_line_only_terminating_newline_present() {
816 let line = format_text_line(1, 1, "abc");
818 let nls = line.matches('\n').count();
819 assert_eq!(nls, 1);
820 }
821
822 #[test]
827 fn is_sqlite_file_true_for_real_header() {
828 let _g = crate::test_util::global_state_lock();
829 let tmp = std::env::temp_dir().join("zshrs_history_sqlite_magic.bin");
830 std::fs::write(&tmp, b"SQLite format 3\0extra junk after").unwrap();
831 assert!(is_sqlite_file(&tmp));
832 let _ = std::fs::remove_file(&tmp);
833 }
834
835 #[test]
836 fn is_sqlite_file_false_for_plain_text() {
837 let _g = crate::test_util::global_state_lock();
838 let tmp = std::env::temp_dir().join("zshrs_history_text_not_db.txt");
839 std::fs::write(&tmp, b": 1700000000:5;ls -la\n").unwrap();
840 assert!(!is_sqlite_file(&tmp));
841 let _ = std::fs::remove_file(&tmp);
842 }
843
844 #[test]
845 fn is_sqlite_file_false_for_short_file() {
846 let _g = crate::test_util::global_state_lock();
847 let tmp = std::env::temp_dir().join("zshrs_history_short.bin");
848 std::fs::write(&tmp, b"abc").unwrap();
849 assert!(!is_sqlite_file(&tmp));
851 let _ = std::fs::remove_file(&tmp);
852 }
853
854 #[test]
855 fn is_sqlite_file_false_for_missing_path() {
856 let _g = crate::test_util::global_state_lock();
857 assert!(!is_sqlite_file(std::path::Path::new(
858 "/nonexistent/zshrs/sqlite/magic.db"
859 )));
860 }
861
862 #[test]
863 fn is_sqlite_file_false_for_almost_matching_header() {
864 let _g = crate::test_util::global_state_lock();
866 let tmp = std::env::temp_dir().join("zshrs_history_fake_magic.bin");
867 std::fs::write(&tmp, b"SQLite format 4\0").unwrap();
868 assert!(!is_sqlite_file(&tmp));
869 let _ = std::fs::remove_file(&tmp);
870 }
871
872 #[test]
877 fn search_prefix_respects_limit() {
878 let _g = crate::test_util::global_state_lock();
879 let engine = HistoryEngine::in_memory().unwrap();
880 for n in 0..5 {
881 engine.add(&format!("git-{}", n), None).unwrap();
882 }
883 let results = engine.search_prefix("git-", 2).unwrap();
884 assert!(results.len() <= 2, "limit not honored: {}", results.len());
885 }
886}