1use std::collections::{HashMap, HashSet};
35use std::fs;
36use std::path::{Path, PathBuf};
37use std::sync::{Arc, Mutex, OnceLock};
38
39use regex::Regex;
40use rusqlite::{params_from_iter, Connection, OpenFlags, Result as SqliteResult, ToSql};
41use serde_json::Value;
42
43use crate::error::{Error, Result};
44
45pub(crate) const METADATA_DB_NAME: &str = "metadata.db";
47const SQLITE_PARAM_LIMIT: usize = 900;
48
49pub(crate) const SUBSET_COLUMN: &str = "_subset_";
51
52const SUBSET_INDEX_NAME: &str = "idx_metadata_subset";
54
55const METADATA_SCHEMA_V1: i64 = 1;
64
65pub(crate) const METADATA_SCHEMA_V2: i64 = 2;
70
71pub(crate) const CONTENT_TABLE: &str = "METADATA_CONTENT";
73
74pub(crate) const CONTENT_ID_COLUMN: &str = "_content_id_";
76
77const THIN_COLUMNS: &[&str] = &[
80 "file",
81 "name",
82 "qualified_name",
83 "line",
84 "end_line",
85 "language",
86 "unit_type",
87 "complexity",
88 "has_loops",
89 "has_branches",
90 "has_error_handling",
91];
92
93fn is_valid_column_name(name: &str) -> bool {
98 lazy_static_regex().is_match(name)
99}
100
101fn lazy_static_regex() -> &'static Regex {
102 use std::sync::OnceLock;
103 static REGEX: OnceLock<Regex> = OnceLock::new();
104 REGEX.get_or_init(|| Regex::new(r"^[a-zA-Z_][a-zA-Z0-9_]*$").unwrap())
105}
106
107#[derive(Debug, Clone, PartialEq)]
117enum Token {
118 Identifier(String),
119 Placeholder, Eq, Ne, Lt, Le, Gt, Ge, Like,
129 Regexp,
130 Between,
131 In,
132 And,
133 Or,
134 Not,
135 Is,
136 Null,
137 LParen,
139 RParen,
140 Comma,
141 Eof,
143}
144
145fn quick_safety_check(condition: &str) -> Result<()> {
147 let upper = condition.to_uppercase();
148
149 if condition.contains("--") || condition.contains("/*") || condition.contains("*/") {
151 return Err(Error::Filtering(
152 "SQL comments are not allowed in conditions".into(),
153 ));
154 }
155
156 if condition.contains(';') {
158 return Err(Error::Filtering(
159 "Semicolons are not allowed in conditions".into(),
160 ));
161 }
162
163 let dangerous_keywords = [
165 "SELECT", "UNION", "INSERT", "UPDATE", "DELETE", "DROP", "CREATE", "ALTER", "TRUNCATE",
166 "EXEC", "EXECUTE", "GRANT", "REVOKE",
167 ];
168
169 for keyword in dangerous_keywords {
170 let pattern = format!(r"\b{}\b", keyword);
172 if Regex::new(&pattern).unwrap().is_match(&upper) {
173 return Err(Error::Filtering(format!(
174 "SQL keyword '{}' is not allowed in conditions",
175 keyword
176 )));
177 }
178 }
179
180 Ok(())
181}
182
183fn tokenize(input: &str) -> Result<Vec<Token>> {
185 let mut tokens = Vec::new();
186 let chars: Vec<char> = input.chars().collect();
187 let mut pos = 0;
188
189 while pos < chars.len() {
190 if chars[pos].is_whitespace() {
192 pos += 1;
193 continue;
194 }
195
196 match chars[pos] {
198 '?' => {
199 tokens.push(Token::Placeholder);
200 pos += 1;
201 continue;
202 }
203 '(' => {
204 tokens.push(Token::LParen);
205 pos += 1;
206 continue;
207 }
208 ')' => {
209 tokens.push(Token::RParen);
210 pos += 1;
211 continue;
212 }
213 ',' => {
214 tokens.push(Token::Comma);
215 pos += 1;
216 continue;
217 }
218 '=' => {
219 tokens.push(Token::Eq);
220 pos += 1;
221 continue;
222 }
223 _ => {}
224 }
225
226 if pos + 1 < chars.len() {
228 let two_chars: String = chars[pos..pos + 2].iter().collect();
229 match two_chars.as_str() {
230 "!=" => {
231 tokens.push(Token::Ne);
232 pos += 2;
233 continue;
234 }
235 "<>" => {
236 tokens.push(Token::Ne);
237 pos += 2;
238 continue;
239 }
240 "<=" => {
241 tokens.push(Token::Le);
242 pos += 2;
243 continue;
244 }
245 ">=" => {
246 tokens.push(Token::Ge);
247 pos += 2;
248 continue;
249 }
250 _ => {}
251 }
252 }
253
254 match chars[pos] {
256 '<' => {
257 tokens.push(Token::Lt);
258 pos += 1;
259 continue;
260 }
261 '>' => {
262 tokens.push(Token::Gt);
263 pos += 1;
264 continue;
265 }
266 _ => {}
267 }
268
269 if chars[pos].is_alphabetic() || chars[pos] == '_' {
271 let start = pos;
272 while pos < chars.len() && (chars[pos].is_alphanumeric() || chars[pos] == '_') {
273 pos += 1;
274 }
275 let word: String = chars[start..pos].iter().collect();
276 let upper = word.to_uppercase();
277
278 let token = match upper.as_str() {
279 "AND" => Token::And,
280 "OR" => Token::Or,
281 "NOT" => Token::Not,
282 "IS" => Token::Is,
283 "NULL" => Token::Null,
284 "LIKE" => Token::Like,
285 "REGEXP" => Token::Regexp,
286 "BETWEEN" => Token::Between,
287 "IN" => Token::In,
288 _ => Token::Identifier(word),
289 };
290 tokens.push(token);
291 continue;
292 }
293
294 if chars[pos] == '"' {
296 pos += 1; let start = pos;
298 while pos < chars.len() && chars[pos] != '"' {
299 pos += 1;
300 }
301 if pos >= chars.len() {
302 return Err(Error::Filtering("Unterminated quoted identifier".into()));
303 }
304 let word: String = chars[start..pos].iter().collect();
305 tokens.push(Token::Identifier(word));
306 pos += 1; continue;
308 }
309
310 return Err(Error::Filtering(format!(
312 "Unexpected character '{}' in condition",
313 chars[pos]
314 )));
315 }
316
317 tokens.push(Token::Eof);
318 Ok(tokens)
319}
320
321struct ConditionValidator<'a> {
323 tokens: &'a [Token],
324 pos: usize,
325 valid_columns: &'a HashSet<String>,
326 columns_used: Vec<String>,
327}
328
329impl<'a> ConditionValidator<'a> {
330 fn new(tokens: &'a [Token], valid_columns: &'a HashSet<String>) -> Self {
331 Self {
332 tokens,
333 pos: 0,
334 valid_columns,
335 columns_used: Vec::new(),
336 }
337 }
338
339 fn current(&self) -> &Token {
340 self.tokens.get(self.pos).unwrap_or(&Token::Eof)
341 }
342
343 fn advance(&mut self) {
344 if self.pos < self.tokens.len() {
345 self.pos += 1;
346 }
347 }
348
349 fn expect(&mut self, expected: &Token) -> Result<()> {
350 if self.current() == expected {
351 self.advance();
352 Ok(())
353 } else {
354 Err(Error::Filtering(format!(
355 "Expected {:?}, found {:?}",
356 expected,
357 self.current()
358 )))
359 }
360 }
361
362 fn validate(&mut self) -> Result<()> {
364 self.parse_expr()?;
365 if *self.current() != Token::Eof {
366 return Err(Error::Filtering(format!(
367 "Unexpected token {:?} after expression",
368 self.current()
369 )));
370 }
371 Ok(())
372 }
373
374 fn parse_expr(&mut self) -> Result<()> {
376 self.parse_and_expr()?;
377 while *self.current() == Token::Or {
378 self.advance();
379 self.parse_and_expr()?;
380 }
381 Ok(())
382 }
383
384 fn parse_and_expr(&mut self) -> Result<()> {
386 self.parse_unary_expr()?;
387 while *self.current() == Token::And {
388 self.advance();
389 self.parse_unary_expr()?;
390 }
391 Ok(())
392 }
393
394 fn parse_unary_expr(&mut self) -> Result<()> {
396 if *self.current() == Token::Not {
397 self.advance();
398 }
399 self.parse_primary_expr()
400 }
401
402 fn parse_primary_expr(&mut self) -> Result<()> {
404 if *self.current() == Token::LParen {
406 self.advance();
407 self.parse_expr()?;
408 self.expect(&Token::RParen)?;
409 return Ok(());
410 }
411
412 let col_name = match self.current().clone() {
414 Token::Identifier(name) => name,
415 other => {
416 return Err(Error::Filtering(format!(
417 "Expected column name, found {:?}",
418 other
419 )))
420 }
421 };
422
423 let col_lower = col_name.to_lowercase();
426 let valid = self
427 .valid_columns
428 .iter()
429 .any(|c| c.to_lowercase() == col_lower);
430 if !valid {
431 return Err(Error::Filtering(format!(
432 "Unknown column '{}' in condition",
433 col_name
434 )));
435 }
436 self.columns_used.push(col_name);
437 self.advance();
438
439 match self.current() {
441 Token::Is => {
443 self.advance();
444 if *self.current() == Token::Not {
445 self.advance();
446 }
447 self.expect(&Token::Null)?;
448 }
449
450 Token::Not => {
452 self.advance();
453 match self.current() {
454 Token::Between => {
455 self.advance();
456 self.expect(&Token::Placeholder)?;
457 self.expect(&Token::And)?;
458 self.expect(&Token::Placeholder)?;
459 }
460 Token::In => {
461 self.advance();
462 self.parse_in_list()?;
463 }
464 Token::Like => {
465 self.advance();
466 self.expect(&Token::Placeholder)?;
467 }
468 Token::Regexp => {
469 self.advance();
470 self.expect(&Token::Placeholder)?;
471 }
472 _ => {
473 return Err(Error::Filtering(format!(
474 "Expected BETWEEN, IN, LIKE, or REGEXP after NOT, found {:?}",
475 self.current()
476 )));
477 }
478 }
479 }
480
481 Token::Between => {
482 self.advance();
483 self.expect(&Token::Placeholder)?;
484 self.expect(&Token::And)?;
485 self.expect(&Token::Placeholder)?;
486 }
487
488 Token::In => {
490 self.advance();
491 self.parse_in_list()?;
492 }
493
494 Token::Like => {
496 self.advance();
497 self.expect(&Token::Placeholder)?;
498 }
499
500 Token::Regexp => {
502 self.advance();
503 self.expect(&Token::Placeholder)?;
504 }
505
506 Token::Eq | Token::Ne | Token::Lt | Token::Le | Token::Gt | Token::Ge => {
508 self.advance();
509 self.expect(&Token::Placeholder)?;
510 }
511
512 other => {
513 return Err(Error::Filtering(format!(
514 "Expected operator after column name, found {:?}",
515 other
516 )));
517 }
518 }
519
520 Ok(())
521 }
522
523 fn parse_in_list(&mut self) -> Result<()> {
525 self.expect(&Token::LParen)?;
526 self.expect(&Token::Placeholder)?;
527 while *self.current() == Token::Comma {
528 self.advance();
529 self.expect(&Token::Placeholder)?;
530 }
531 self.expect(&Token::RParen)?;
532 Ok(())
533 }
534}
535
536fn get_schema_columns(conn: &Connection) -> Result<HashSet<String>> {
538 let mut columns = HashSet::new();
539 let split = is_split_schema(conn);
540 let mut stmt = conn.prepare("PRAGMA table_info(METADATA)")?;
541 let rows = stmt.query_map([], |row| row.get::<_, String>(1))?;
542 for row in rows {
543 let col = row?;
544 if split && col == CONTENT_ID_COLUMN {
545 continue;
546 }
547 columns.insert(col);
548 }
549 if split {
551 let mut stmt = conn.prepare(&format!("PRAGMA table_info({})", CONTENT_TABLE))?;
552 let rows = stmt.query_map([], |row| row.get::<_, String>(1))?;
553 for row in rows {
554 let col = row?;
555 if col != CONTENT_ID_COLUMN {
556 columns.insert(col);
557 }
558 }
559 }
560 Ok(columns)
561}
562
563fn is_numeric_equality(condition: &str) -> bool {
587 lazy_static_numeric_eq_regex().is_match(condition.trim())
588}
589
590fn lazy_static_numeric_eq_regex() -> &'static Regex {
591 use std::sync::OnceLock;
592 static REGEX: OnceLock<Regex> = OnceLock::new();
593 REGEX.get_or_init(|| Regex::new(r"^(\d+)\s*=\s*(\d+)$").unwrap())
594}
595
596fn validate_condition(condition: &str, valid_columns: &HashSet<String>) -> Result<()> {
597 if is_numeric_equality(condition) {
600 return Ok(());
601 }
602
603 quick_safety_check(condition)?;
605
606 let tokens = tokenize(condition)?;
608
609 let mut validator = ConditionValidator::new(&tokens, valid_columns);
611 validator.validate()?;
612
613 Ok(())
614}
615
616fn infer_sql_type(value: &Value) -> &'static str {
618 match value {
619 Value::Number(n) => {
620 if n.is_i64() || n.is_u64() {
621 "INTEGER"
622 } else {
623 "REAL"
624 }
625 }
626 Value::Bool(_) => "INTEGER",
627 Value::String(_) => "TEXT",
628 Value::Null => "TEXT",
629 Value::Array(_) | Value::Object(_) => "BLOB",
630 }
631}
632
633fn json_to_sql(value: &Value) -> Box<dyn ToSql> {
635 match value {
636 Value::Null => Box::new(None::<String>),
637 Value::Bool(b) => Box::new(if *b { 1i64 } else { 0i64 }),
638 Value::Number(n) => {
639 if let Some(i) = n.as_i64() {
640 Box::new(i)
641 } else if let Some(f) = n.as_f64() {
642 Box::new(f)
643 } else {
644 Box::new(n.to_string())
645 }
646 }
647 Value::String(s) => Box::new(s.clone()),
648 Value::Array(_) | Value::Object(_) => Box::new(serde_json::to_string(value).unwrap()),
649 }
650}
651
652pub(crate) fn get_db_path(index_path: &str) -> std::path::PathBuf {
654 Path::new(index_path).join(METADATA_DB_NAME)
655}
656
657static DB_READ_CONNECTIONS: OnceLock<Mutex<HashMap<PathBuf, Arc<Mutex<Connection>>>>> =
658 OnceLock::new();
659
660fn open_db_read_uncached(db_path: &std::path::Path) -> Result<Connection> {
661 let conn = Connection::open_with_flags(
662 db_path,
663 OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_NO_MUTEX,
664 )?;
665 conn.execute_batch(
666 "PRAGMA busy_timeout=5000;
667 PRAGMA temp_store=MEMORY;
668 PRAGMA query_only=ON;",
669 )?;
670 Ok(conn)
671}
672
673fn read_connection(db_path: &Path) -> Result<Arc<Mutex<Connection>>> {
674 let key = db_path.to_path_buf();
675 let connections = DB_READ_CONNECTIONS.get_or_init(|| Mutex::new(HashMap::new()));
676
677 if let Some(conn) = connections
678 .lock()
679 .expect("DB_READ_CONNECTIONS mutex poisoned while reading metadata DB cache")
680 .get(&key)
681 .cloned()
682 {
683 return Ok(conn);
684 }
685
686 let new_conn = Arc::new(Mutex::new(open_db_read_uncached(db_path)?));
687 let mut map = connections
688 .lock()
689 .expect("DB_READ_CONNECTIONS mutex poisoned while updating metadata DB cache");
690 Ok(map.entry(key).or_insert_with(|| new_conn).clone())
691}
692
693fn invalidate_read_connection(db_path: &Path) {
694 if let Some(connections) = DB_READ_CONNECTIONS.get() {
695 connections
696 .lock()
697 .expect("DB_READ_CONNECTIONS mutex poisoned while invalidating metadata DB cache")
698 .remove(db_path);
699 }
700}
701
702pub(crate) fn with_db_read<T>(
708 db_path: &std::path::Path,
709 f: impl FnOnce(&Connection) -> Result<T>,
710) -> Result<T> {
711 let conn = read_connection(db_path)?;
712 let guard = conn
713 .lock()
714 .expect("cached metadata read connection mutex poisoned");
715 f(&guard)
716}
717
718pub(crate) fn open_db_write(db_path: &std::path::Path) -> Result<Connection> {
723 let conn = Connection::open(db_path)?;
724 conn.execute_batch(
725 "PRAGMA busy_timeout=5000;
726 PRAGMA journal_mode=WAL;
727 PRAGMA synchronous=NORMAL;
728 PRAGMA temp_store=MEMORY;",
729 )?;
730 Ok(conn)
731}
732
733fn validate_fixed_columns(columns: &[(&str, &str)]) -> Result<()> {
734 for (name, _) in columns {
735 if !is_valid_column_name(name) {
736 return Err(Error::Filtering(format!(
737 "Invalid column name '{}'. Column names must start with a letter or \
738 underscore, followed by letters, digits, or underscores.",
739 name
740 )));
741 }
742 }
743 Ok(())
744}
745
746pub(crate) fn metadata_schema_version(conn: &Connection) -> i64 {
748 conn.query_row("PRAGMA user_version", [], |row| row.get(0))
749 .unwrap_or(0)
750}
751
752pub(crate) fn is_split_schema(conn: &Connection) -> bool {
754 metadata_schema_version(conn) >= METADATA_SCHEMA_V2
755}
756
757fn is_thin_column(col: &str) -> bool {
759 col == SUBSET_COLUMN || col == CONTENT_ID_COLUMN || THIN_COLUMNS.contains(&col)
760}
761
762fn create_subset_index(conn: &Connection) -> Result<()> {
770 conn.execute(
771 &format!(
772 "CREATE INDEX IF NOT EXISTS \"{}\" ON METADATA (\"{}\")",
773 SUBSET_INDEX_NAME, SUBSET_COLUMN
774 ),
775 [],
776 )?;
777 Ok(())
778}
779
780fn ensure_fast_subset_schema(conn: &Connection) -> Result<()> {
793 if metadata_schema_version(conn) >= METADATA_SCHEMA_V1 {
794 return Ok(());
795 }
796
797 let has_table: i64 = conn
798 .query_row(
799 "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='METADATA'",
800 [],
801 |row| row.get(0),
802 )
803 .unwrap_or(0);
804 if has_table == 0 {
805 return Ok(());
807 }
808
809 let mut cols: Vec<(String, String)> = Vec::new(); let mut subset_is_pk = false;
812 {
813 let mut stmt = conn.prepare("PRAGMA table_info(METADATA)")?;
814 let rows = stmt.query_map([], |row| {
815 Ok((
816 row.get::<_, String>(1)?, row.get::<_, String>(2)?, row.get::<_, i64>(5)?, ))
820 })?;
821 for r in rows {
822 let (name, ty, pk) = r?;
823 if name == SUBSET_COLUMN && pk > 0 {
824 subset_is_pk = true;
825 }
826 cols.push((name, ty));
827 }
828 }
829
830 if !subset_is_pk {
831 create_subset_index(conn)?;
833 conn.execute_batch(&format!("PRAGMA user_version={}", METADATA_SCHEMA_V1))?;
834 return Ok(());
835 }
836
837 let col_defs: Vec<String> = cols
839 .iter()
840 .map(|(name, ty)| {
841 if name == SUBSET_COLUMN {
842 format!("\"{}\" INTEGER NOT NULL", SUBSET_COLUMN)
843 } else {
844 let ty = if ty.trim().is_empty() {
845 "TEXT"
846 } else {
847 ty.as_str()
848 };
849 format!("\"{}\" {}", name, ty)
850 }
851 })
852 .collect();
853 let all_cols = cols
854 .iter()
855 .map(|(name, _)| format!("\"{}\"", name))
856 .collect::<Vec<_>>()
857 .join(", ");
858
859 conn.execute_batch("BEGIN")?;
860 conn.execute("ALTER TABLE METADATA RENAME TO _METADATA_V0", [])?;
861 conn.execute(
862 &format!("CREATE TABLE METADATA ({})", col_defs.join(", ")),
863 [],
864 )?;
865 conn.execute(
866 &format!(
867 "INSERT INTO METADATA ({0}) SELECT {0} FROM _METADATA_V0",
868 all_cols
869 ),
870 [],
871 )?;
872 create_subset_index(conn)?;
873 conn.execute("DROP TABLE _METADATA_V0", [])?;
874 conn.execute_batch(&format!("PRAGMA user_version={}", METADATA_SCHEMA_V1))?;
875 conn.execute_batch("COMMIT")?;
876 Ok(())
877}
878
879fn create_fixed_metadata_table(conn: &Connection, columns: &[(&str, &str)]) -> Result<()> {
880 let mut thin_col_defs = vec![
883 format!("\"{}\" INTEGER NOT NULL", SUBSET_COLUMN),
884 format!("\"{}\" INTEGER NOT NULL", CONTENT_ID_COLUMN),
885 ];
886 let mut fat_col_defs = vec![format!("\"{}\" INTEGER PRIMARY KEY", CONTENT_ID_COLUMN)];
887
888 for (name, sql_type) in columns {
889 if is_thin_column(name) {
890 thin_col_defs.push(format!("\"{}\" {}", name, sql_type));
891 } else {
892 fat_col_defs.push(format!("\"{}\" {}", name, sql_type));
893 }
894 }
895
896 conn.execute(
897 &format!("CREATE TABLE METADATA ({})", thin_col_defs.join(", ")),
898 [],
899 )?;
900 conn.execute(
901 &format!(
902 "CREATE TABLE {} ({})",
903 CONTENT_TABLE,
904 fat_col_defs.join(", ")
905 ),
906 [],
907 )?;
908 create_subset_index(conn)?;
909 conn.execute_batch(&format!("PRAGMA user_version={}", METADATA_SCHEMA_V2))?;
910 Ok(())
911}
912
913fn insert_fixed_metadata_rows(
914 conn: &mut Connection,
915 columns: &[(&str, &str)],
916 metadata: &[Value],
917 doc_ids: &[i64],
918) -> Result<usize> {
919 if is_split_schema(conn) {
920 return insert_fixed_metadata_rows_v2(conn, columns, metadata, doc_ids);
921 }
922 let txn = conn.transaction()?;
923 let mut column_names = vec![format!("\"{}\"", SUBSET_COLUMN)];
924 column_names.extend(columns.iter().map(|(name, _)| format!("\"{}\"", name)));
925 let placeholders: Vec<&str> = std::iter::repeat_n("?", columns.len() + 1).collect();
926 let insert_sql = format!(
927 "INSERT INTO METADATA ({}) VALUES ({})",
928 column_names.join(", "),
929 placeholders.join(", ")
930 );
931 {
932 let mut stmt = txn.prepare_cached(&insert_sql)?;
933 for (i, item) in metadata.iter().enumerate() {
934 let obj = item.as_object().ok_or_else(|| {
935 Error::Filtering("Expected metadata rows to be JSON objects".into())
936 })?;
937 let mut values: Vec<Box<dyn ToSql>> = vec![Box::new(doc_ids[i])];
938 for (column_name, _) in columns {
939 let value = obj.get(*column_name).unwrap_or(&Value::Null);
940 values.push(json_to_sql(value));
941 }
942 let params: Vec<&dyn ToSql> = values.iter().map(|value| value.as_ref()).collect();
943 stmt.execute(params_from_iter(params))?;
944 }
945 }
946 txn.commit()?;
947 Ok(metadata.len())
948}
949
950fn insert_fixed_metadata_rows_v2(
951 conn: &mut Connection,
952 columns: &[(&str, &str)],
953 metadata: &[Value],
954 doc_ids: &[i64],
955) -> Result<usize> {
956 let thin_cols: Vec<&(&str, &str)> = columns.iter().filter(|(n, _)| is_thin_column(n)).collect();
957 let fat_cols: Vec<&(&str, &str)> = columns.iter().filter(|(n, _)| !is_thin_column(n)).collect();
958
959 let next_content_id: i64 = conn
960 .query_row(
961 &format!(
962 "SELECT COALESCE(MAX(\"{}\"), -1) + 1 FROM {}",
963 CONTENT_ID_COLUMN, CONTENT_TABLE
964 ),
965 [],
966 |row| row.get(0),
967 )
968 .unwrap_or(0);
969
970 let txn = conn.transaction()?;
971
972 let mut fat_col_names = vec![format!("\"{}\"", CONTENT_ID_COLUMN)];
974 fat_col_names.extend(fat_cols.iter().map(|(name, _)| format!("\"{}\"", name)));
975 let fat_placeholders: Vec<&str> = std::iter::repeat_n("?", fat_cols.len() + 1).collect();
976 let fat_insert_sql = format!(
977 "INSERT INTO {} ({}) VALUES ({})",
978 CONTENT_TABLE,
979 fat_col_names.join(", "),
980 fat_placeholders.join(", ")
981 );
982
983 let mut thin_col_names = vec![
985 format!("\"{}\"", SUBSET_COLUMN),
986 format!("\"{}\"", CONTENT_ID_COLUMN),
987 ];
988 thin_col_names.extend(thin_cols.iter().map(|(name, _)| format!("\"{}\"", name)));
989 let thin_placeholders: Vec<&str> = std::iter::repeat_n("?", thin_cols.len() + 2).collect();
990 let thin_insert_sql = format!(
991 "INSERT INTO METADATA ({}) VALUES ({})",
992 thin_col_names.join(", "),
993 thin_placeholders.join(", ")
994 );
995
996 {
997 let mut fat_stmt = txn.prepare_cached(&fat_insert_sql)?;
998 let mut thin_stmt = txn.prepare_cached(&thin_insert_sql)?;
999
1000 for (i, item) in metadata.iter().enumerate() {
1001 let obj = item.as_object().ok_or_else(|| {
1002 Error::Filtering("Expected metadata rows to be JSON objects".into())
1003 })?;
1004 let content_id = next_content_id + i as i64;
1005
1006 let mut fat_values: Vec<Box<dyn ToSql>> = vec![Box::new(content_id)];
1008 for (column_name, _) in &fat_cols {
1009 let value = obj.get(*column_name).unwrap_or(&Value::Null);
1010 fat_values.push(json_to_sql(value));
1011 }
1012 let fat_params: Vec<&dyn ToSql> = fat_values.iter().map(|v| v.as_ref()).collect();
1013 fat_stmt.execute(params_from_iter(fat_params))?;
1014
1015 let mut thin_values: Vec<Box<dyn ToSql>> =
1017 vec![Box::new(doc_ids[i]), Box::new(content_id)];
1018 for (column_name, _) in &thin_cols {
1019 let value = obj.get(*column_name).unwrap_or(&Value::Null);
1020 thin_values.push(json_to_sql(value));
1021 }
1022 let thin_params: Vec<&dyn ToSql> = thin_values.iter().map(|v| v.as_ref()).collect();
1023 thin_stmt.execute(params_from_iter(thin_params))?;
1024 }
1025 }
1026 txn.commit()?;
1027 Ok(metadata.len())
1028}
1029
1030fn try_fixed_schema_from_first_row(
1031 metadata: &[Value],
1032) -> Result<Option<Vec<(&str, &'static str)>>> {
1033 let Some(Value::Object(first_obj)) = metadata.first() else {
1034 return Ok(None);
1035 };
1036
1037 let mut columns = Vec::with_capacity(first_obj.len());
1038 let mut seen = HashSet::with_capacity(first_obj.len());
1039 for (key, value) in first_obj {
1040 if !is_valid_column_name(key) {
1041 return Err(Error::Filtering(format!(
1042 "Invalid column name '{}'. Column names must start with a letter or \
1043 underscore, followed by letters, digits, or underscores.",
1044 key
1045 )));
1046 }
1047 seen.insert(key.as_str());
1048 columns.push((key.as_str(), infer_sql_type(value)));
1049 }
1050
1051 for item in &metadata[1..] {
1054 if let Value::Object(obj) = item {
1055 for key in obj.keys() {
1056 if !seen.contains(key.as_str()) {
1057 return Ok(None);
1058 }
1059 }
1060 }
1061 }
1062
1063 Ok(Some(columns))
1064}
1065
1066pub fn exists(index_path: &str) -> bool {
1068 get_db_path(index_path).exists()
1069}
1070
1071fn create_with_fixed_columns(
1072 index_path: &str,
1073 columns: &[(&str, &str)],
1074 metadata: &[Value],
1075 doc_ids: &[i64],
1076) -> Result<usize> {
1077 if metadata.len() != doc_ids.len() {
1078 return Err(Error::Filtering(format!(
1079 "Metadata length ({}) must match doc_ids length ({})",
1080 metadata.len(),
1081 doc_ids.len()
1082 )));
1083 }
1084 validate_fixed_columns(columns)?;
1085
1086 let index_dir = Path::new(index_path);
1087 if !index_dir.exists() {
1088 fs::create_dir_all(index_dir)?;
1089 }
1090
1091 let db_path = get_db_path(index_path);
1092 if db_path.exists() {
1093 invalidate_read_connection(&db_path);
1094 fs::remove_file(&db_path)?;
1095 }
1096
1097 if metadata.is_empty() {
1098 return Ok(0);
1099 }
1100
1101 let mut conn = open_db_write(&db_path)?;
1102 create_fixed_metadata_table(&conn, columns)?;
1103 insert_fixed_metadata_rows(&mut conn, columns, metadata, doc_ids)
1104}
1105
1106pub fn create(index_path: &str, metadata: &[Value], doc_ids: &[i64]) -> Result<usize> {
1142 if metadata.len() != doc_ids.len() {
1144 return Err(Error::Filtering(format!(
1145 "Metadata length ({}) must match doc_ids length ({})",
1146 metadata.len(),
1147 doc_ids.len()
1148 )));
1149 }
1150
1151 let index_dir = Path::new(index_path);
1153 if !index_dir.exists() {
1154 fs::create_dir_all(index_dir)?;
1155 }
1156
1157 let db_path = get_db_path(index_path);
1159 if db_path.exists() {
1160 invalidate_read_connection(&db_path);
1161 fs::remove_file(&db_path)?;
1162 }
1163
1164 if metadata.is_empty() {
1165 return Ok(0);
1166 }
1167
1168 if let Some(columns) = try_fixed_schema_from_first_row(metadata)? {
1172 return create_with_fixed_columns(index_path, &columns, metadata, doc_ids);
1173 }
1174
1175 let mut columns: Vec<String> = Vec::new();
1177 let mut column_types: HashMap<String, &'static str> = HashMap::new();
1178
1179 for item in metadata {
1180 if let Value::Object(obj) = item {
1181 for (key, value) in obj {
1182 if !columns.contains(key) {
1183 if !is_valid_column_name(key) {
1184 return Err(Error::Filtering(format!(
1185 "Invalid column name '{}'. Column names must start with a letter or \
1186 underscore, followed by letters, digits, or underscores.",
1187 key
1188 )));
1189 }
1190 columns.push(key.clone());
1191 }
1192 if !value.is_null() && !column_types.contains_key(key) {
1193 column_types.insert(key.clone(), infer_sql_type(value));
1194 }
1195 }
1196 }
1197 }
1198
1199 let mut conn = open_db_write(&db_path)?;
1201
1202 let thin_columns: Vec<&String> = columns.iter().filter(|c| is_thin_column(c)).collect();
1204 let fat_columns: Vec<&String> = columns.iter().filter(|c| !is_thin_column(c)).collect();
1205
1206 let mut thin_col_defs = vec![
1207 format!("\"{}\" INTEGER NOT NULL", SUBSET_COLUMN),
1208 format!("\"{}\" INTEGER NOT NULL", CONTENT_ID_COLUMN),
1209 ];
1210 for col in &thin_columns {
1211 let sql_type = column_types.get(col.as_str()).copied().unwrap_or("TEXT");
1212 thin_col_defs.push(format!("\"{}\" {}", col, sql_type));
1213 }
1214
1215 let mut fat_col_defs = vec![format!("\"{}\" INTEGER PRIMARY KEY", CONTENT_ID_COLUMN)];
1216 for col in &fat_columns {
1217 let sql_type = column_types.get(col.as_str()).copied().unwrap_or("TEXT");
1218 fat_col_defs.push(format!("\"{}\" {}", col, sql_type));
1219 }
1220
1221 let txn = conn.transaction()?;
1222 txn.execute(
1223 &format!("CREATE TABLE METADATA ({})", thin_col_defs.join(", ")),
1224 [],
1225 )?;
1226 txn.execute(
1227 &format!(
1228 "CREATE TABLE {} ({})",
1229 CONTENT_TABLE,
1230 fat_col_defs.join(", ")
1231 ),
1232 [],
1233 )?;
1234 txn.execute(
1235 &format!(
1236 "CREATE INDEX IF NOT EXISTS \"{}\" ON METADATA (\"{}\")",
1237 SUBSET_INDEX_NAME, SUBSET_COLUMN
1238 ),
1239 [],
1240 )?;
1241
1242 let thin_col_names: Vec<String> = std::iter::once(format!("\"{}\"", SUBSET_COLUMN))
1244 .chain(std::iter::once(format!("\"{}\"", CONTENT_ID_COLUMN)))
1245 .chain(thin_columns.iter().map(|c| format!("\"{}\"", c)))
1246 .collect();
1247 let thin_placeholders: Vec<&str> = std::iter::repeat_n("?", thin_columns.len() + 2).collect();
1248 let thin_insert_sql = format!(
1249 "INSERT INTO METADATA ({}) VALUES ({})",
1250 thin_col_names.join(", "),
1251 thin_placeholders.join(", ")
1252 );
1253
1254 let fat_col_names: Vec<String> = std::iter::once(format!("\"{}\"", CONTENT_ID_COLUMN))
1255 .chain(fat_columns.iter().map(|c| format!("\"{}\"", c)))
1256 .collect();
1257 let fat_placeholders: Vec<&str> = std::iter::repeat_n("?", fat_columns.len() + 1).collect();
1258 let fat_insert_sql = format!(
1259 "INSERT INTO {} ({}) VALUES ({})",
1260 CONTENT_TABLE,
1261 fat_col_names.join(", "),
1262 fat_placeholders.join(", ")
1263 );
1264
1265 {
1266 let mut thin_stmt = txn.prepare(&thin_insert_sql)?;
1267 let mut fat_stmt = txn.prepare(&fat_insert_sql)?;
1268 for (i, item) in metadata.iter().enumerate() {
1269 let content_id = doc_ids[i];
1270
1271 let mut fat_values: Vec<Box<dyn ToSql>> = vec![Box::new(content_id)];
1273 if let Value::Object(obj) = item {
1274 for col in &fat_columns {
1275 let value = obj.get(col.as_str()).unwrap_or(&Value::Null);
1276 fat_values.push(json_to_sql(value));
1277 }
1278 } else {
1279 for _ in &fat_columns {
1280 fat_values.push(Box::new(None::<String>));
1281 }
1282 }
1283 let fat_params: Vec<&dyn ToSql> = fat_values.iter().map(|v| v.as_ref()).collect();
1284 fat_stmt.execute(params_from_iter(fat_params))?;
1285
1286 let mut thin_values: Vec<Box<dyn ToSql>> =
1288 vec![Box::new(doc_ids[i]), Box::new(content_id)];
1289 if let Value::Object(obj) = item {
1290 for col in &thin_columns {
1291 let value = obj.get(col.as_str()).unwrap_or(&Value::Null);
1292 thin_values.push(json_to_sql(value));
1293 }
1294 } else {
1295 for _ in &thin_columns {
1296 thin_values.push(Box::new(None::<String>));
1297 }
1298 }
1299 let thin_params: Vec<&dyn ToSql> = thin_values.iter().map(|v| v.as_ref()).collect();
1300 thin_stmt.execute(params_from_iter(thin_params))?;
1301 }
1302 }
1303 txn.commit()?;
1304
1305 conn.execute_batch(&format!("PRAGMA user_version={}", METADATA_SCHEMA_V2))?;
1306
1307 Ok(metadata.len())
1308}
1309
1310pub fn update(index_path: &str, metadata: &[Value], doc_ids: &[i64]) -> Result<usize> {
1333 if metadata.is_empty() {
1334 return Ok(0);
1335 }
1336
1337 if metadata.len() != doc_ids.len() {
1339 return Err(Error::Filtering(format!(
1340 "Metadata length ({}) must match doc_ids length ({})",
1341 metadata.len(),
1342 doc_ids.len()
1343 )));
1344 }
1345
1346 let db_path = get_db_path(index_path);
1347 if !db_path.exists() {
1348 return Err(Error::Filtering(
1349 "Metadata database does not exist. Use create() first.".into(),
1350 ));
1351 }
1352
1353 let mut conn = open_db_write(&db_path)?;
1354
1355 if is_split_schema(&conn) {
1356 return update_v2(&mut conn, metadata, doc_ids);
1357 }
1358
1359 let mut existing_columns: Vec<String> = Vec::new();
1361 {
1362 let mut stmt = conn.prepare("PRAGMA table_info(METADATA)")?;
1363 let rows = stmt.query_map([], |row| row.get::<_, String>(1))?;
1364 for row in rows {
1365 let col = row?;
1366 if col != SUBSET_COLUMN {
1367 existing_columns.push(col);
1368 }
1369 }
1370 }
1371
1372 let existing_column_set: HashSet<&str> = existing_columns
1373 .iter()
1374 .map(|column| column.as_str())
1375 .collect();
1376 let has_new_columns = metadata.iter().any(|item| {
1377 item.as_object().is_some_and(|obj| {
1378 obj.keys()
1379 .any(|key| !existing_column_set.contains(key.as_str()))
1380 })
1381 });
1382 if !has_new_columns {
1383 let fixed_columns: Vec<(&str, &str)> = existing_columns
1387 .iter()
1388 .map(|column| (column.as_str(), "TEXT"))
1389 .collect();
1390 return insert_fixed_metadata_rows(&mut conn, &fixed_columns, metadata, doc_ids);
1391 }
1392
1393 let mut new_columns: Vec<String> = Vec::new();
1395 let mut column_types: HashMap<String, &'static str> = HashMap::new();
1396
1397 for item in metadata {
1398 if let Value::Object(obj) = item {
1399 for (key, value) in obj {
1400 if !existing_columns.contains(key) && !new_columns.contains(key) {
1401 if !is_valid_column_name(key) {
1402 return Err(Error::Filtering(format!(
1403 "Invalid column name '{}'. Column names must start with a letter or \
1404 underscore, followed by letters, digits, or underscores.",
1405 key
1406 )));
1407 }
1408 new_columns.push(key.clone());
1409 }
1410 if !value.is_null() && !column_types.contains_key(key) {
1411 column_types.insert(key.clone(), infer_sql_type(value));
1412 }
1413 }
1414 }
1415 }
1416
1417 let txn = conn.transaction()?;
1418 for col in &new_columns {
1420 let sql_type = column_types.get(col).copied().unwrap_or("TEXT");
1421 let alter_sql = format!("ALTER TABLE METADATA ADD COLUMN \"{}\" {}", col, sql_type);
1422 txn.execute(&alter_sql, [])?;
1423 }
1424
1425 let all_columns: Vec<String> = existing_columns.into_iter().chain(new_columns).collect();
1427
1428 let placeholders: Vec<&str> = std::iter::repeat_n("?", all_columns.len() + 1).collect();
1430 let insert_sql = if all_columns.is_empty() {
1431 format!("INSERT INTO METADATA (\"{}\") VALUES (?)", SUBSET_COLUMN,)
1432 } else {
1433 let col_names: Vec<String> = all_columns.iter().map(|c| format!("\"{}\"", c)).collect();
1434 format!(
1435 "INSERT INTO METADATA (\"{}\", {}) VALUES ({})",
1436 SUBSET_COLUMN,
1437 col_names.join(", "),
1438 placeholders.join(", ")
1439 )
1440 };
1441
1442 {
1443 let mut stmt = txn.prepare(&insert_sql)?;
1444 for (i, item) in metadata.iter().enumerate() {
1445 let mut values: Vec<Box<dyn ToSql>> = vec![Box::new(doc_ids[i])];
1446 if let Value::Object(obj) = item {
1447 for col in &all_columns {
1448 let value = obj.get(col).unwrap_or(&Value::Null);
1449 values.push(json_to_sql(value));
1450 }
1451 } else {
1452 for _ in &all_columns {
1453 values.push(Box::new(None::<String>));
1454 }
1455 }
1456 let params: Vec<&dyn ToSql> = values.iter().map(|v| v.as_ref()).collect();
1457 stmt.execute(params_from_iter(params))?;
1458 }
1459 }
1460 txn.commit()?;
1461
1462 Ok(metadata.len())
1463}
1464
1465fn update_v2(conn: &mut Connection, metadata: &[Value], doc_ids: &[i64]) -> Result<usize> {
1466 let mut thin_existing: Vec<String> = Vec::new();
1468 {
1469 let mut stmt = conn.prepare("PRAGMA table_info(METADATA)")?;
1470 let rows = stmt.query_map([], |row| row.get::<_, String>(1))?;
1471 for row in rows {
1472 let col = row?;
1473 if col != SUBSET_COLUMN && col != CONTENT_ID_COLUMN {
1474 thin_existing.push(col);
1475 }
1476 }
1477 }
1478 let mut fat_existing: Vec<String> = Vec::new();
1479 {
1480 let mut stmt = conn.prepare(&format!("PRAGMA table_info({})", CONTENT_TABLE))?;
1481 let rows = stmt.query_map([], |row| row.get::<_, String>(1))?;
1482 for row in rows {
1483 let col = row?;
1484 if col != CONTENT_ID_COLUMN {
1485 fat_existing.push(col);
1486 }
1487 }
1488 }
1489
1490 let mut all_existing: HashSet<&str> = HashSet::new();
1491 for c in &thin_existing {
1492 all_existing.insert(c.as_str());
1493 }
1494 for c in &fat_existing {
1495 all_existing.insert(c.as_str());
1496 }
1497
1498 let mut column_types: HashMap<String, &'static str> = HashMap::new();
1500 let mut new_thin: Vec<String> = Vec::new();
1501 let mut new_fat: Vec<String> = Vec::new();
1502
1503 for item in metadata {
1504 if let Value::Object(obj) = item {
1505 for (key, value) in obj {
1506 if !all_existing.contains(key.as_str())
1507 && !new_thin.contains(key)
1508 && !new_fat.contains(key)
1509 {
1510 if !is_valid_column_name(key) {
1511 return Err(Error::Filtering(format!(
1512 "Invalid column name '{}'. Column names must start with a letter or \
1513 underscore, followed by letters, digits, or underscores.",
1514 key
1515 )));
1516 }
1517 if is_thin_column(key) {
1518 new_thin.push(key.clone());
1519 } else {
1520 new_fat.push(key.clone());
1521 }
1522 }
1523 if !value.is_null() && !column_types.contains_key(key) {
1524 column_types.insert(key.clone(), infer_sql_type(value));
1525 }
1526 }
1527 }
1528 }
1529
1530 let txn = conn.transaction()?;
1531 for col in &new_thin {
1532 let sql_type = column_types.get(col).copied().unwrap_or("TEXT");
1533 txn.execute(
1534 &format!("ALTER TABLE METADATA ADD COLUMN \"{}\" {}", col, sql_type),
1535 [],
1536 )?;
1537 }
1538 for col in &new_fat {
1539 let sql_type = column_types.get(col).copied().unwrap_or("TEXT");
1540 txn.execute(
1541 &format!(
1542 "ALTER TABLE {} ADD COLUMN \"{}\" {}",
1543 CONTENT_TABLE, col, sql_type
1544 ),
1545 [],
1546 )?;
1547 }
1548
1549 let all_thin: Vec<String> = thin_existing.into_iter().chain(new_thin).collect();
1550 let all_fat: Vec<String> = fat_existing.into_iter().chain(new_fat).collect();
1551
1552 let next_content_id: i64 = txn
1554 .query_row(
1555 &format!(
1556 "SELECT COALESCE(MAX(\"{}\"), -1) + 1 FROM {}",
1557 CONTENT_ID_COLUMN, CONTENT_TABLE
1558 ),
1559 [],
1560 |row| row.get(0),
1561 )
1562 .unwrap_or(0);
1563
1564 let mut fat_col_names = vec![format!("\"{}\"", CONTENT_ID_COLUMN)];
1566 fat_col_names.extend(all_fat.iter().map(|c| format!("\"{}\"", c)));
1567 let fat_placeholders: Vec<&str> = std::iter::repeat_n("?", all_fat.len() + 1).collect();
1568 let fat_insert_sql = format!(
1569 "INSERT INTO {} ({}) VALUES ({})",
1570 CONTENT_TABLE,
1571 fat_col_names.join(", "),
1572 fat_placeholders.join(", ")
1573 );
1574
1575 let mut thin_col_names = vec![
1577 format!("\"{}\"", SUBSET_COLUMN),
1578 format!("\"{}\"", CONTENT_ID_COLUMN),
1579 ];
1580 thin_col_names.extend(all_thin.iter().map(|c| format!("\"{}\"", c)));
1581 let thin_placeholders: Vec<&str> = std::iter::repeat_n("?", all_thin.len() + 2).collect();
1582 let thin_insert_sql = format!(
1583 "INSERT INTO METADATA ({}) VALUES ({})",
1584 thin_col_names.join(", "),
1585 thin_placeholders.join(", ")
1586 );
1587
1588 {
1589 let mut fat_stmt = txn.prepare(&fat_insert_sql)?;
1590 let mut thin_stmt = txn.prepare(&thin_insert_sql)?;
1591
1592 for (i, item) in metadata.iter().enumerate() {
1593 let content_id = next_content_id + i as i64;
1594
1595 let mut fat_values: Vec<Box<dyn ToSql>> = vec![Box::new(content_id)];
1596 if let Value::Object(obj) = item {
1597 for col in &all_fat {
1598 let value = obj.get(col).unwrap_or(&Value::Null);
1599 fat_values.push(json_to_sql(value));
1600 }
1601 } else {
1602 for _ in &all_fat {
1603 fat_values.push(Box::new(None::<String>));
1604 }
1605 }
1606 let fat_params: Vec<&dyn ToSql> = fat_values.iter().map(|v| v.as_ref()).collect();
1607 fat_stmt.execute(params_from_iter(fat_params))?;
1608
1609 let mut thin_values: Vec<Box<dyn ToSql>> =
1610 vec![Box::new(doc_ids[i]), Box::new(content_id)];
1611 if let Value::Object(obj) = item {
1612 for col in &all_thin {
1613 let value = obj.get(col).unwrap_or(&Value::Null);
1614 thin_values.push(json_to_sql(value));
1615 }
1616 } else {
1617 for _ in &all_thin {
1618 thin_values.push(Box::new(None::<String>));
1619 }
1620 }
1621 let thin_params: Vec<&dyn ToSql> = thin_values.iter().map(|v| v.as_ref()).collect();
1622 thin_stmt.execute(params_from_iter(thin_params))?;
1623 }
1624 }
1625 txn.commit()?;
1626 Ok(metadata.len())
1627}
1628
1629pub fn delete(index_path: &str, subset: &[i64]) -> Result<usize> {
1647 if subset.is_empty() {
1648 return Ok(0);
1649 }
1650
1651 let db_path = get_db_path(index_path);
1652 if !db_path.exists() {
1653 return Ok(0);
1654 }
1655
1656 let conn = open_db_write(&db_path)?;
1657
1658 if is_split_schema(&conn) {
1659 return delete_v2(&conn, subset);
1660 }
1661
1662 ensure_fast_subset_schema(&conn)?;
1666
1667 conn.execute("BEGIN", [])?;
1669
1670 let original_count: i64 = conn
1675 .query_row(
1676 &format!(
1677 "SELECT COALESCE(MAX(\"{}\"), -1) FROM METADATA",
1678 SUBSET_COLUMN
1679 ),
1680 [],
1681 |row| row.get::<_, i64>(0),
1682 )
1683 .unwrap_or(-1)
1684 + 1;
1685
1686 let (in_clause, in_params, temp_table) = crate::text_search::build_in_clause(&conn, subset)?;
1688 let delete_sql = format!(
1689 "DELETE FROM METADATA WHERE \"{}\" {}",
1690 SUBSET_COLUMN, in_clause
1691 );
1692 let param_refs: Vec<&dyn ToSql> = in_params.iter().map(|v| v.as_ref()).collect();
1693 let deleted = conn.execute(&delete_sql, params_from_iter(param_refs))?;
1694 if let Some(ref name) = temp_table {
1695 crate::text_search::drop_temp_table(&conn, name);
1696 }
1697
1698 let mut sorted_ids: Vec<i64> = subset.to_vec();
1705 sorted_ids.sort_unstable();
1706 sorted_ids.dedup();
1707 sorted_ids.retain(|&id| id >= 0 && id < original_count);
1708
1709 let max_id: i64 = conn
1710 .query_row(
1711 &format!(
1712 "SELECT COALESCE(MAX(\"{}\"), -1) FROM METADATA",
1713 SUBSET_COLUMN
1714 ),
1715 [],
1716 |row| row.get(0),
1717 )
1718 .unwrap_or(-1);
1719
1720 if max_id >= 0 && !sorted_ids.is_empty() {
1721 let mut updates: Vec<(i64, i64, i64)> = Vec::new();
1729 let mut i = 0;
1730 while i < sorted_ids.len() {
1731 let mut j = i + 1;
1733 while j < sorted_ids.len() && sorted_ids[j] == sorted_ids[j - 1] + 1 {
1734 j += 1;
1735 }
1736 let range_start = sorted_ids[j - 1] + 1;
1739 let range_end = if j < sorted_ids.len() {
1740 sorted_ids[j]
1741 } else {
1742 max_id + sorted_ids.len() as i64 + 1
1743 };
1744 if range_start < range_end {
1745 updates.push((range_start, range_end, j as i64));
1746 }
1747 i = j;
1748 }
1749
1750 for (from, to_excl, shift) in &updates {
1751 conn.execute(
1752 &format!(
1753 "UPDATE METADATA SET \"{}\" = \"{}\" - ?1 WHERE \"{}\" >= ?2 AND \"{}\" < ?3",
1754 SUBSET_COLUMN, SUBSET_COLUMN, SUBSET_COLUMN, SUBSET_COLUMN
1755 ),
1756 rusqlite::params![shift, from, to_excl],
1757 )?;
1758 }
1759 }
1760
1761 conn.execute("COMMIT", [])?;
1763
1764 Ok(deleted)
1765}
1766
1767fn delete_v2(conn: &Connection, subset: &[i64]) -> Result<usize> {
1768 conn.execute("BEGIN", [])?;
1769
1770 let original_count: i64 = conn
1771 .query_row(
1772 &format!(
1773 "SELECT COALESCE(MAX(\"{}\"), -1) FROM METADATA",
1774 SUBSET_COLUMN
1775 ),
1776 [],
1777 |row| row.get::<_, i64>(0),
1778 )
1779 .unwrap_or(-1)
1780 + 1;
1781
1782 let (in_clause, in_params, temp_table) = crate::text_search::build_in_clause(conn, subset)?;
1784 let delete_sql = format!(
1785 "DELETE FROM METADATA WHERE \"{}\" {}",
1786 SUBSET_COLUMN, in_clause
1787 );
1788 let param_refs: Vec<&dyn ToSql> = in_params.iter().map(|v| v.as_ref()).collect();
1789 let deleted = conn.execute(&delete_sql, params_from_iter(param_refs))?;
1790 if let Some(ref name) = temp_table {
1791 crate::text_search::drop_temp_table(conn, name);
1792 }
1793
1794 crate::text_search::delete_fts_rows_for_orphaned_content(conn)?;
1801
1802 conn.execute(
1804 &format!(
1805 "DELETE FROM {} WHERE \"{}\" NOT IN (SELECT \"{}\" FROM METADATA)",
1806 CONTENT_TABLE, CONTENT_ID_COLUMN, CONTENT_ID_COLUMN
1807 ),
1808 [],
1809 )?;
1810
1811 let mut sorted_ids: Vec<i64> = subset.to_vec();
1814 sorted_ids.sort_unstable();
1815 sorted_ids.dedup();
1816 sorted_ids.retain(|&id| id >= 0 && id < original_count);
1817
1818 let max_id: i64 = conn
1819 .query_row(
1820 &format!(
1821 "SELECT COALESCE(MAX(\"{}\"), -1) FROM METADATA",
1822 SUBSET_COLUMN
1823 ),
1824 [],
1825 |row| row.get(0),
1826 )
1827 .unwrap_or(-1);
1828
1829 if max_id >= 0 && !sorted_ids.is_empty() {
1830 let mut updates: Vec<(i64, i64, i64)> = Vec::new();
1831 let mut i = 0;
1832 while i < sorted_ids.len() {
1833 let mut j = i + 1;
1834 while j < sorted_ids.len() && sorted_ids[j] == sorted_ids[j - 1] + 1 {
1835 j += 1;
1836 }
1837 let range_start = sorted_ids[j - 1] + 1;
1838 let range_end = if j < sorted_ids.len() {
1839 sorted_ids[j]
1840 } else {
1841 max_id + sorted_ids.len() as i64 + 1
1842 };
1843 if range_start < range_end {
1844 updates.push((range_start, range_end, j as i64));
1845 }
1846 i = j;
1847 }
1848
1849 for (from, to_excl, shift) in &updates {
1850 conn.execute(
1851 &format!(
1852 "UPDATE METADATA SET \"{}\" = \"{}\" - ?1 WHERE \"{}\" >= ?2 AND \"{}\" < ?3",
1853 SUBSET_COLUMN, SUBSET_COLUMN, SUBSET_COLUMN, SUBSET_COLUMN
1854 ),
1855 rusqlite::params![shift, from, to_excl],
1856 )?;
1857 }
1858 }
1859
1860 conn.execute("COMMIT", [])?;
1861 Ok(deleted)
1862}
1863
1864pub fn where_condition(
1889 index_path: &str,
1890 condition: &str,
1891 parameters: &[Value],
1892) -> Result<Vec<i64>> {
1893 let db_path = get_db_path(index_path);
1894 if !db_path.exists() {
1895 return Err(Error::Filtering(
1896 "No metadata database found. Create it first by adding metadata during index creation."
1897 .into(),
1898 ));
1899 }
1900
1901 with_db_read(&db_path, |conn| {
1902 let valid_columns = get_schema_columns(conn)?;
1904 validate_condition(condition, &valid_columns)?;
1905
1906 let query = if is_split_schema(conn) && condition_references_fat_column(condition, conn) {
1907 format!(
1908 "SELECT M.\"{}\" FROM METADATA M JOIN {} C ON M.\"{}\" = C.\"{}\" WHERE {}",
1909 SUBSET_COLUMN, CONTENT_TABLE, CONTENT_ID_COLUMN, CONTENT_ID_COLUMN, condition
1910 )
1911 } else {
1912 format!(
1913 "SELECT \"{}\" FROM METADATA WHERE {}",
1914 SUBSET_COLUMN, condition
1915 )
1916 };
1917
1918 let params: Vec<Box<dyn ToSql>> = parameters.iter().map(json_to_sql).collect();
1919 let param_refs: Vec<&dyn ToSql> = params.iter().map(|v| v.as_ref()).collect();
1920
1921 let mut stmt = conn.prepare(&query)?;
1922 let rows = stmt.query_map(params_from_iter(param_refs), |row| row.get::<_, i64>(0))?;
1923
1924 let mut result = Vec::new();
1925 for row in rows {
1926 result.push(row?);
1927 }
1928
1929 Ok(result)
1930 })
1931}
1932
1933fn condition_references_fat_column(condition: &str, conn: &Connection) -> bool {
1935 let mut fat_columns: Vec<String> = Vec::new();
1936 if let Ok(mut stmt) = conn.prepare(&format!("PRAGMA table_info({})", CONTENT_TABLE)) {
1937 if let Ok(rows) = stmt.query_map([], |row| row.get::<_, String>(1)) {
1938 for row in rows.flatten() {
1939 if row != CONTENT_ID_COLUMN {
1940 fat_columns.push(row);
1941 }
1942 }
1943 }
1944 }
1945 let cond_upper = condition.to_uppercase();
1946 fat_columns
1947 .iter()
1948 .any(|col| cond_upper.contains(&col.to_uppercase()))
1949}
1950
1951pub fn where_condition_regexp(
1978 index_path: &str,
1979 condition: &str,
1980 parameters: &[Value],
1981) -> Result<Vec<i64>> {
1982 let db_path = get_db_path(index_path);
1983 if !db_path.exists() {
1984 return Err(Error::Filtering(
1985 "No metadata database found. Create it first by adding metadata during index creation."
1986 .into(),
1987 ));
1988 }
1989
1990 let regex_pattern = parameters
1993 .first()
1994 .and_then(|v| v.as_str())
1995 .ok_or_else(|| Error::Filtering("REGEXP requires a pattern parameter".into()))?;
1996
1997 let compiled_regex = std::sync::Arc::new(
2010 fancy_regex::RegexBuilder::new(regex_pattern)
2011 .build()
2012 .map_err(|e| {
2013 Error::Filtering(format!("Invalid regex pattern '{}': {}", regex_pattern, e))
2014 })?,
2015 );
2016
2017 with_db_read(&db_path, |conn| {
2018 let valid_columns = get_schema_columns(conn)?;
2020 validate_condition(condition, &valid_columns)?;
2021
2022 let re = compiled_regex.clone();
2024 conn.create_scalar_function(
2025 "regexp",
2026 2,
2027 rusqlite::functions::FunctionFlags::SQLITE_UTF8
2028 | rusqlite::functions::FunctionFlags::SQLITE_DETERMINISTIC,
2029 move |ctx| {
2030 let _pattern: String = ctx.get(0)?;
2032 let text: String = ctx.get(1)?;
2033
2034 Ok(re.is_match(&text).unwrap_or(false))
2039 },
2040 )?;
2041
2042 let query = if is_split_schema(conn) && condition_references_fat_column(condition, conn) {
2043 format!(
2044 "SELECT M.\"{}\" FROM METADATA M JOIN {} C ON M.\"{}\" = C.\"{}\" WHERE {}",
2045 SUBSET_COLUMN, CONTENT_TABLE, CONTENT_ID_COLUMN, CONTENT_ID_COLUMN, condition
2046 )
2047 } else {
2048 format!(
2049 "SELECT \"{}\" FROM METADATA WHERE {}",
2050 SUBSET_COLUMN, condition
2051 )
2052 };
2053
2054 let params: Vec<Box<dyn ToSql>> = parameters.iter().map(json_to_sql).collect();
2055 let param_refs: Vec<&dyn ToSql> = params.iter().map(|v| v.as_ref()).collect();
2056
2057 let mut stmt = conn.prepare(&query)?;
2058 let rows = stmt.query_map(params_from_iter(param_refs), |row| row.get::<_, i64>(0))?;
2059
2060 let mut result = Vec::new();
2061 for row in rows {
2062 result.push(row?);
2063 }
2064
2065 Ok(result)
2066 })
2067}
2068
2069pub fn get_distinct_strings(index_path: &str, column: &str) -> Result<Vec<String>> {
2087 let db_path = get_db_path(index_path);
2088 if !db_path.exists() {
2089 return Ok(Vec::new());
2090 }
2091
2092 if !is_valid_column_name(column) {
2095 return Err(Error::Filtering(format!(
2096 "Invalid column name: '{}'",
2097 column
2098 )));
2099 }
2100
2101 with_db_read(&db_path, |conn| {
2102 let columns = get_schema_columns(conn)?;
2103 if !columns.contains(column) {
2104 return Ok(Vec::new());
2105 }
2106
2107 let query = if is_split_schema(conn) && !is_thin_column(column) {
2108 format!(
2109 "SELECT DISTINCT \"{0}\" FROM {1} WHERE \"{0}\" IS NOT NULL",
2110 column, CONTENT_TABLE
2111 )
2112 } else {
2113 format!(
2114 "SELECT DISTINCT \"{0}\" FROM METADATA WHERE \"{0}\" IS NOT NULL",
2115 column
2116 )
2117 };
2118 let mut stmt = conn.prepare(&query)?;
2119 let rows = stmt.query_map([], |row| row.get::<_, Option<String>>(0))?;
2120
2121 let mut values: Vec<String> = Vec::new();
2122 for row in rows {
2123 if let Some(value) = row? {
2124 values.push(value);
2125 }
2126 }
2127
2128 Ok(values)
2129 })
2130}
2131
2132pub fn get(
2152 index_path: &str,
2153 condition: Option<&str>,
2154 parameters: &[Value],
2155 subset: Option<&[i64]>,
2156) -> Result<Vec<Value>> {
2157 if condition.is_some() && subset.is_some() {
2158 return Err(Error::Filtering(
2159 "Please provide either a 'condition' or a 'subset', not both.".into(),
2160 ));
2161 }
2162
2163 let db_path = get_db_path(index_path);
2164 if !db_path.exists() {
2165 return Ok(Vec::new());
2166 }
2167
2168 with_db_read(&db_path, |conn| {
2169 if let Some(cond) = condition {
2171 let valid_columns = get_schema_columns(conn)?;
2172 validate_condition(cond, &valid_columns)?;
2173 }
2174
2175 if is_split_schema(conn) {
2176 return get_v2(conn, condition, parameters, subset);
2177 }
2178
2179 let mut columns: Vec<String> = Vec::new();
2181 {
2182 let mut stmt = conn.prepare("PRAGMA table_info(METADATA)")?;
2183 let rows = stmt.query_map([], |row| row.get::<_, String>(1))?;
2184 for row in rows {
2185 columns.push(row?);
2186 }
2187 }
2188
2189 if let Some(ids) = subset {
2190 if ids.is_empty() {
2191 return Ok(Vec::new());
2192 }
2193
2194 let mut results: Vec<Value> = Vec::new();
2195 for chunk in ids.chunks(SQLITE_PARAM_LIMIT) {
2196 let placeholders: Vec<&str> = std::iter::repeat_n("?", chunk.len()).collect();
2197 let query = format!(
2198 "SELECT * FROM METADATA WHERE \"{}\" IN ({})",
2199 SUBSET_COLUMN,
2200 placeholders.join(", ")
2201 );
2202 let params: Vec<Box<dyn ToSql>> = chunk
2203 .iter()
2204 .map(|&id| Box::new(id) as Box<dyn ToSql>)
2205 .collect();
2206 let param_refs: Vec<&dyn ToSql> = params.iter().map(|v| v.as_ref()).collect();
2207 let mut stmt = conn.prepare(&query)?;
2208 let mut rows = stmt.query(params_from_iter(param_refs))?;
2209
2210 while let Some(row) = rows.next()? {
2211 let mut obj = serde_json::Map::new();
2212 for (i, col) in columns.iter().enumerate() {
2213 let value = row_to_json_value(row, i)?;
2214 obj.insert(col.clone(), value);
2215 }
2216 results.push(Value::Object(obj));
2217 }
2218 }
2219
2220 let mut results_map: HashMap<i64, Value> = HashMap::new();
2221 for result in results {
2222 if let Some(id) = result.get(SUBSET_COLUMN).and_then(|v| v.as_i64()) {
2223 results_map.insert(id, result);
2224 }
2225 }
2226 return Ok(ids.iter().filter_map(|id| results_map.remove(id)).collect());
2227 }
2228
2229 let (query, params): (String, Vec<Box<dyn ToSql>>) = if let Some(cond) = condition {
2231 let query = format!(
2232 "SELECT * FROM METADATA WHERE {} ORDER BY \"{}\"",
2233 cond, SUBSET_COLUMN
2234 );
2235 let params = parameters.iter().map(json_to_sql).collect();
2236 (query, params)
2237 } else {
2238 let query = format!("SELECT * FROM METADATA ORDER BY \"{}\"", SUBSET_COLUMN);
2239 (query, Vec::new())
2240 };
2241
2242 let param_refs: Vec<&dyn ToSql> = params.iter().map(|v| v.as_ref()).collect();
2243 let mut stmt = conn.prepare(&query)?;
2244 let mut rows = stmt.query(params_from_iter(param_refs))?;
2245
2246 let mut results: Vec<Value> = Vec::new();
2247 while let Some(row) = rows.next()? {
2248 let mut obj = serde_json::Map::new();
2249 for (i, col) in columns.iter().enumerate() {
2250 let value = row_to_json_value(row, i)?;
2251 obj.insert(col.clone(), value);
2252 }
2253 results.push(Value::Object(obj));
2254 }
2255
2256 Ok(results)
2257 })
2258}
2259
2260fn get_v2(
2261 conn: &Connection,
2262 condition: Option<&str>,
2263 parameters: &[Value],
2264 subset: Option<&[i64]>,
2265) -> Result<Vec<Value>> {
2266 let mut thin_cols: Vec<String> = Vec::new();
2268 {
2269 let mut stmt = conn.prepare("PRAGMA table_info(METADATA)")?;
2270 let rows = stmt.query_map([], |row| row.get::<_, String>(1))?;
2271 for row in rows {
2272 let col = row?;
2273 if col != CONTENT_ID_COLUMN {
2274 thin_cols.push(col);
2275 }
2276 }
2277 }
2278 let mut fat_cols: Vec<String> = Vec::new();
2279 {
2280 let mut stmt = conn.prepare(&format!("PRAGMA table_info({})", CONTENT_TABLE))?;
2281 let rows = stmt.query_map([], |row| row.get::<_, String>(1))?;
2282 for row in rows {
2283 let col = row?;
2284 if col != CONTENT_ID_COLUMN {
2285 fat_cols.push(col);
2286 }
2287 }
2288 }
2289
2290 let mut select_parts: Vec<String> = Vec::new();
2292 for col in &thin_cols {
2293 select_parts.push(format!("M.\"{}\"", col));
2294 }
2295 for col in &fat_cols {
2296 select_parts.push(format!("C.\"{}\"", col));
2297 }
2298 let select_clause = select_parts.join(", ");
2299
2300 let from_clause = format!(
2301 "METADATA M JOIN {} C ON M.\"{}\" = C.\"{}\"",
2302 CONTENT_TABLE, CONTENT_ID_COLUMN, CONTENT_ID_COLUMN
2303 );
2304
2305 let all_cols: Vec<&String> = thin_cols.iter().chain(fat_cols.iter()).collect();
2307
2308 if let Some(ids) = subset {
2309 if ids.is_empty() {
2310 return Ok(Vec::new());
2311 }
2312
2313 let mut results: Vec<Value> = Vec::new();
2314 for chunk in ids.chunks(SQLITE_PARAM_LIMIT) {
2315 let placeholders: Vec<&str> = std::iter::repeat_n("?", chunk.len()).collect();
2316 let query = format!(
2317 "SELECT {} FROM {} WHERE M.\"{}\" IN ({})",
2318 select_clause,
2319 from_clause,
2320 SUBSET_COLUMN,
2321 placeholders.join(", ")
2322 );
2323 let params: Vec<Box<dyn ToSql>> = chunk
2324 .iter()
2325 .map(|&id| Box::new(id) as Box<dyn ToSql>)
2326 .collect();
2327 let param_refs: Vec<&dyn ToSql> = params.iter().map(|v| v.as_ref()).collect();
2328 let mut stmt = conn.prepare(&query)?;
2329 let mut rows = stmt.query(params_from_iter(param_refs))?;
2330
2331 while let Some(row) = rows.next()? {
2332 let mut obj = serde_json::Map::new();
2333 for (i, col) in all_cols.iter().enumerate() {
2334 let value = row_to_json_value(row, i)?;
2335 obj.insert((*col).clone(), value);
2336 }
2337 results.push(Value::Object(obj));
2338 }
2339 }
2340
2341 let mut results_map: HashMap<i64, Value> = HashMap::new();
2342 for result in results {
2343 if let Some(id) = result.get(SUBSET_COLUMN).and_then(|v| v.as_i64()) {
2344 results_map.insert(id, result);
2345 }
2346 }
2347 return Ok(ids.iter().filter_map(|id| results_map.remove(id)).collect());
2348 }
2349
2350 let (query, params): (String, Vec<Box<dyn ToSql>>) = if let Some(cond) = condition {
2351 let query = format!(
2352 "SELECT {} FROM {} WHERE {} ORDER BY M.\"{}\"",
2353 select_clause, from_clause, cond, SUBSET_COLUMN
2354 );
2355 let params = parameters.iter().map(json_to_sql).collect();
2356 (query, params)
2357 } else {
2358 let query = format!(
2359 "SELECT {} FROM {} ORDER BY M.\"{}\"",
2360 select_clause, from_clause, SUBSET_COLUMN
2361 );
2362 (query, Vec::new())
2363 };
2364
2365 let param_refs: Vec<&dyn ToSql> = params.iter().map(|v| v.as_ref()).collect();
2366 let mut stmt = conn.prepare(&query)?;
2367 let mut rows = stmt.query(params_from_iter(param_refs))?;
2368
2369 let mut results: Vec<Value> = Vec::new();
2370 while let Some(row) = rows.next()? {
2371 let mut obj = serde_json::Map::new();
2372 for (i, col) in all_cols.iter().enumerate() {
2373 let value = row_to_json_value(row, i)?;
2374 obj.insert((*col).clone(), value);
2375 }
2376 results.push(Value::Object(obj));
2377 }
2378
2379 Ok(results)
2380}
2381
2382fn row_to_json_value(row: &rusqlite::Row, idx: usize) -> SqliteResult<Value> {
2384 if let Ok(i) = row.get::<_, i64>(idx) {
2386 return Ok(Value::Number(i.into()));
2387 }
2388 if let Ok(f) = row.get::<_, f64>(idx) {
2389 return Ok(serde_json::Number::from_f64(f)
2390 .map(Value::Number)
2391 .unwrap_or(Value::Null));
2392 }
2393 if let Ok(s) = row.get::<_, String>(idx) {
2394 return Ok(Value::String(s));
2395 }
2396 if let Ok(b) = row.get::<_, Vec<u8>>(idx) {
2397 if let Ok(v) = serde_json::from_slice(&b) {
2399 return Ok(v);
2400 }
2401 return Ok(Value::String(base64_encode(&b)));
2403 }
2404 Ok(Value::Null)
2405}
2406
2407fn base64_encode(data: &[u8]) -> String {
2408 let mut result = String::with_capacity(data.len() * 4 / 3 + 4);
2409 const ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
2410
2411 for chunk in data.chunks(3) {
2412 let b0 = chunk[0] as usize;
2413 let b1 = chunk.get(1).copied().unwrap_or(0) as usize;
2414 let b2 = chunk.get(2).copied().unwrap_or(0) as usize;
2415
2416 result.push(ALPHABET[b0 >> 2] as char);
2417 result.push(ALPHABET[((b0 & 0x03) << 4) | (b1 >> 4)] as char);
2418
2419 if chunk.len() > 1 {
2420 result.push(ALPHABET[((b1 & 0x0f) << 2) | (b2 >> 6)] as char);
2421 } else {
2422 result.push('=');
2423 }
2424
2425 if chunk.len() > 2 {
2426 result.push(ALPHABET[b2 & 0x3f] as char);
2427 } else {
2428 result.push('=');
2429 }
2430 }
2431
2432 result
2433}
2434
2435pub fn update_where(
2466 index_path: &str,
2467 condition: &str,
2468 parameters: &[Value],
2469 updates: &Value,
2470) -> Result<usize> {
2471 let db_path = get_db_path(index_path);
2472 if !db_path.exists() {
2473 return Err(Error::Filtering(
2474 "No metadata database found. Create it first by adding metadata during index creation."
2475 .into(),
2476 ));
2477 }
2478
2479 let updates_obj = match updates {
2481 Value::Object(obj) => obj,
2482 _ => {
2483 return Err(Error::Filtering("Updates must be a JSON object".into()));
2484 }
2485 };
2486
2487 if updates_obj.is_empty() {
2488 return Ok(0);
2489 }
2490
2491 let conn = open_db_write(&db_path)?;
2492
2493 let valid_columns = get_schema_columns(&conn)?;
2495 validate_condition(condition, &valid_columns)?;
2496
2497 for col_name in updates_obj.keys() {
2499 if col_name == SUBSET_COLUMN {
2500 return Err(Error::Filtering("Cannot update the _subset_ column".into()));
2501 }
2502 if !is_valid_column_name(col_name) {
2503 return Err(Error::Filtering(format!(
2504 "Invalid column name '{}'. Column names must start with a letter or \
2505 underscore, followed by letters, digits, or underscores.",
2506 col_name
2507 )));
2508 }
2509 let col_lower = col_name.to_lowercase();
2511 let exists = valid_columns.iter().any(|c| c.to_lowercase() == col_lower);
2512 if !exists {
2513 return Err(Error::Filtering(format!(
2514 "Unknown column '{}' in updates",
2515 col_name
2516 )));
2517 }
2518 }
2519
2520 let affected_query =
2522 if is_split_schema(&conn) && condition_references_fat_column(condition, &conn) {
2523 format!(
2524 "SELECT M.\"{}\" FROM METADATA M JOIN {} C ON M.\"{}\" = C.\"{}\" WHERE {}",
2525 SUBSET_COLUMN, CONTENT_TABLE, CONTENT_ID_COLUMN, CONTENT_ID_COLUMN, condition
2526 )
2527 } else {
2528 format!(
2529 "SELECT \"{}\" FROM METADATA WHERE {}",
2530 SUBSET_COLUMN, condition
2531 )
2532 };
2533 let affected_ids: Vec<i64> = {
2534 let cond_params: Vec<Box<dyn ToSql>> = parameters.iter().map(json_to_sql).collect();
2535 let cond_param_refs: Vec<&dyn ToSql> = cond_params.iter().map(|v| v.as_ref()).collect();
2536
2537 let mut affected_stmt = conn.prepare(&affected_query)?;
2538 let rows = affected_stmt.query_map(params_from_iter(cond_param_refs), |row| {
2539 row.get::<_, i64>(0)
2540 })?;
2541 rows.filter_map(|row| row.ok()).collect()
2542 };
2543
2544 if is_split_schema(&conn) {
2545 return update_where_v2(
2546 &conn,
2547 index_path,
2548 updates_obj,
2549 &affected_ids,
2550 condition,
2551 parameters,
2552 );
2553 }
2554
2555 let set_parts: Vec<String> = updates_obj
2557 .keys()
2558 .map(|col| format!("\"{}\" = ?", col))
2559 .collect();
2560 let set_clause = set_parts.join(", ");
2561
2562 let query = format!("UPDATE METADATA SET {} WHERE {}", set_clause, condition);
2564
2565 let mut all_params: Vec<Box<dyn ToSql>> = updates_obj.values().map(json_to_sql).collect();
2567 all_params.extend(parameters.iter().map(json_to_sql));
2568
2569 let param_refs: Vec<&dyn ToSql> = all_params.iter().map(|v| v.as_ref()).collect();
2570
2571 let updated = conn.execute(&query, params_from_iter(param_refs))?;
2572
2573 if updated > 0 && !affected_ids.is_empty() {
2574 crate::text_search::update_rows(index_path, &affected_ids)?;
2575 }
2576
2577 Ok(updated)
2578}
2579
2580fn update_where_v2(
2581 conn: &Connection,
2582 index_path: &str,
2583 updates_obj: &serde_json::Map<String, Value>,
2584 affected_ids: &[i64],
2585 _condition: &str,
2586 _parameters: &[Value],
2587) -> Result<usize> {
2588 if affected_ids.is_empty() {
2589 return Ok(0);
2590 }
2591
2592 let thin_updates: Vec<(&String, &Value)> = updates_obj
2594 .iter()
2595 .filter(|(k, _)| is_thin_column(k))
2596 .collect();
2597 let fat_updates: Vec<(&String, &Value)> = updates_obj
2598 .iter()
2599 .filter(|(k, _)| !is_thin_column(k))
2600 .collect();
2601
2602 conn.execute("BEGIN", [])?;
2603
2604 let mut total_updated = 0usize;
2605
2606 if !thin_updates.is_empty() {
2608 let set_parts: Vec<String> = thin_updates
2609 .iter()
2610 .map(|(col, _)| format!("\"{}\" = ?", col))
2611 .collect();
2612 let set_clause = set_parts.join(", ");
2613
2614 for chunk in affected_ids.chunks(SQLITE_PARAM_LIMIT) {
2615 let placeholders: Vec<&str> = std::iter::repeat_n("?", chunk.len()).collect();
2616 let query = format!(
2617 "UPDATE METADATA SET {} WHERE \"{}\" IN ({})",
2618 set_clause,
2619 SUBSET_COLUMN,
2620 placeholders.join(", ")
2621 );
2622 let mut params: Vec<Box<dyn ToSql>> =
2623 thin_updates.iter().map(|(_, v)| json_to_sql(v)).collect();
2624 params.extend(chunk.iter().map(|&id| Box::new(id) as Box<dyn ToSql>));
2625 let param_refs: Vec<&dyn ToSql> = params.iter().map(|v| v.as_ref()).collect();
2626 total_updated += conn.execute(&query, params_from_iter(param_refs))?;
2627 }
2628 }
2629
2630 if !fat_updates.is_empty() {
2632 let set_parts: Vec<String> = fat_updates
2633 .iter()
2634 .map(|(col, _)| format!("\"{}\" = ?", col))
2635 .collect();
2636 let set_clause = set_parts.join(", ");
2637
2638 for chunk in affected_ids.chunks(SQLITE_PARAM_LIMIT) {
2640 let placeholders: Vec<&str> = std::iter::repeat_n("?", chunk.len()).collect();
2641 let content_ids_query = format!(
2642 "SELECT \"{}\" FROM METADATA WHERE \"{}\" IN ({})",
2643 CONTENT_ID_COLUMN,
2644 SUBSET_COLUMN,
2645 placeholders.join(", ")
2646 );
2647 let id_params: Vec<Box<dyn ToSql>> = chunk
2648 .iter()
2649 .map(|&id| Box::new(id) as Box<dyn ToSql>)
2650 .collect();
2651 let id_param_refs: Vec<&dyn ToSql> = id_params.iter().map(|v| v.as_ref()).collect();
2652 let mut stmt = conn.prepare(&content_ids_query)?;
2653 let content_ids: Vec<i64> = stmt
2654 .query_map(params_from_iter(id_param_refs), |row| row.get::<_, i64>(0))?
2655 .filter_map(|r| r.ok())
2656 .collect();
2657
2658 if !content_ids.is_empty() {
2659 let c_placeholders: Vec<&str> =
2660 std::iter::repeat_n("?", content_ids.len()).collect();
2661 let query = format!(
2662 "UPDATE {} SET {} WHERE \"{}\" IN ({})",
2663 CONTENT_TABLE,
2664 set_clause,
2665 CONTENT_ID_COLUMN,
2666 c_placeholders.join(", ")
2667 );
2668 let mut params: Vec<Box<dyn ToSql>> =
2669 fat_updates.iter().map(|(_, v)| json_to_sql(v)).collect();
2670 params.extend(content_ids.iter().map(|&id| Box::new(id) as Box<dyn ToSql>));
2671 let param_refs: Vec<&dyn ToSql> = params.iter().map(|v| v.as_ref()).collect();
2672 total_updated += conn.execute(&query, params_from_iter(param_refs))?;
2673 }
2674 }
2675 }
2676
2677 conn.execute("COMMIT", [])?;
2678
2679 if total_updated > 0 && !affected_ids.is_empty() {
2680 crate::text_search::update_rows(index_path, affected_ids)?;
2681 }
2682
2683 Ok(affected_ids.len())
2684}
2685
2686pub fn count(index_path: &str) -> Result<usize> {
2688 let db_path = get_db_path(index_path);
2689 if !db_path.exists() {
2690 return Ok(0);
2691 }
2692
2693 with_db_read(&db_path, |conn| {
2694 let count: i64 = conn.query_row("SELECT COUNT(*) FROM METADATA", [], |row| row.get(0))?;
2695 Ok(count as usize)
2696 })
2697}
2698
2699#[cfg(test)]
2700mod tests {
2701 use super::*;
2702 use serde_json::json;
2703 use tempfile::TempDir;
2704
2705 fn setup_test_dir() -> TempDir {
2706 TempDir::new().unwrap()
2707 }
2708
2709 #[test]
2710 fn test_create_empty() {
2711 let dir = setup_test_dir();
2712 let path = dir.path().to_str().unwrap();
2713
2714 let result = create(path, &[], &[]).unwrap();
2715 assert_eq!(result, 0);
2716 assert!(!exists(path));
2717 }
2718
2719 #[test]
2720 fn test_create_with_metadata() {
2721 let dir = setup_test_dir();
2722 let path = dir.path().to_str().unwrap();
2723
2724 let metadata = vec![
2725 json!({"name": "Alice", "age": 30, "score": 95.5}),
2726 json!({"name": "Bob", "age": 25, "score": 87.0}),
2727 json!({"name": "Charlie", "age": 35}),
2728 ];
2729 let doc_ids: Vec<i64> = (0..3).collect();
2730
2731 let result = create(path, &metadata, &doc_ids).unwrap();
2732 assert_eq!(result, 3);
2733 assert!(exists(path));
2734
2735 assert_eq!(count(path).unwrap(), 3);
2737 }
2738
2739 #[test]
2740 fn test_create_invalid_column_name() {
2741 let dir = setup_test_dir();
2742 let path = dir.path().to_str().unwrap();
2743
2744 let metadata = vec![json!({"valid_name": "Alice", "invalid name": 30})];
2745 let doc_ids = vec![0];
2746
2747 let result = create(path, &metadata, &doc_ids);
2748 assert!(result.is_err());
2749 }
2750
2751 #[test]
2752 fn test_where_condition() {
2753 let dir = setup_test_dir();
2754 let path = dir.path().to_str().unwrap();
2755
2756 let metadata = vec![
2757 json!({"name": "Alice", "category": "A", "score": 95}),
2758 json!({"name": "Bob", "category": "B", "score": 87}),
2759 json!({"name": "Charlie", "category": "A", "score": 92}),
2760 ];
2761 let doc_ids: Vec<i64> = (0..3).collect();
2762
2763 create(path, &metadata, &doc_ids).unwrap();
2764
2765 let subset = where_condition(path, "category = ?", &[json!("A")]).unwrap();
2767 assert_eq!(subset, vec![0, 2]);
2768
2769 let subset =
2771 where_condition(path, "category = ? AND score > ?", &[json!("A"), json!(93)]).unwrap();
2772 assert_eq!(subset, vec![0]);
2773 }
2774
2775 #[test]
2776 fn test_get_distinct_strings_returns_unique_values() {
2777 let dir = setup_test_dir();
2778 let path = dir.path().to_str().unwrap();
2779
2780 let metadata = vec![
2781 json!({"file": "src/a.rs", "code": "x"}),
2782 json!({"file": "src/a.rs", "code": "y"}),
2783 json!({"file": "src/b.rs", "code": "z"}),
2784 ];
2785 let doc_ids: Vec<i64> = (0..3).collect();
2786 create(path, &metadata, &doc_ids).unwrap();
2787
2788 let mut files = get_distinct_strings(path, "file").unwrap();
2789 files.sort();
2790 assert_eq!(files, vec!["src/a.rs".to_string(), "src/b.rs".to_string()]);
2791 }
2792
2793 #[test]
2794 fn test_get_distinct_strings_missing_db_returns_empty() {
2795 let dir = setup_test_dir();
2796 let path = dir.path().to_str().unwrap();
2797 let files = get_distinct_strings(path, "file").unwrap();
2799 assert!(files.is_empty());
2800 }
2801
2802 #[test]
2803 fn test_get_distinct_strings_unknown_column_returns_empty() {
2804 let dir = setup_test_dir();
2805 let path = dir.path().to_str().unwrap();
2806
2807 let metadata = vec![json!({"file": "src/a.rs"})];
2808 create(path, &metadata, &[0]).unwrap();
2809
2810 let values = get_distinct_strings(path, "not_a_column").unwrap();
2811 assert!(values.is_empty());
2812 }
2813
2814 #[test]
2815 fn test_get_distinct_strings_rejects_invalid_column_name() {
2816 let dir = setup_test_dir();
2817 let path = dir.path().to_str().unwrap();
2818
2819 let metadata = vec![json!({"file": "src/a.rs"})];
2820 create(path, &metadata, &[0]).unwrap();
2821
2822 let result = get_distinct_strings(path, "file; DROP TABLE METADATA --");
2823 assert!(result.is_err());
2824 }
2825
2826 #[test]
2827 fn test_get_all() {
2828 let dir = setup_test_dir();
2829 let path = dir.path().to_str().unwrap();
2830
2831 let metadata = vec![
2832 json!({"name": "Alice", "age": 30}),
2833 json!({"name": "Bob", "age": 25}),
2834 ];
2835 let doc_ids: Vec<i64> = (0..2).collect();
2836
2837 create(path, &metadata, &doc_ids).unwrap();
2838
2839 let results = get(path, None, &[], None).unwrap();
2840 assert_eq!(results.len(), 2);
2841 assert_eq!(results[0]["name"], "Alice");
2842 assert_eq!(results[1]["name"], "Bob");
2843 }
2844
2845 #[test]
2846 fn test_get_by_subset() {
2847 let dir = setup_test_dir();
2848 let path = dir.path().to_str().unwrap();
2849
2850 let metadata = vec![
2851 json!({"name": "Alice"}),
2852 json!({"name": "Bob"}),
2853 json!({"name": "Charlie"}),
2854 ];
2855 let doc_ids: Vec<i64> = (0..3).collect();
2856
2857 create(path, &metadata, &doc_ids).unwrap();
2858
2859 let results = get(path, None, &[], Some(&[2, 0])).unwrap();
2861 assert_eq!(results.len(), 2);
2862 assert_eq!(results[0]["name"], "Charlie");
2863 assert_eq!(results[1]["name"], "Alice");
2864 }
2865
2866 #[test]
2867 fn test_update_adds_rows() {
2868 let dir = setup_test_dir();
2869 let path = dir.path().to_str().unwrap();
2870
2871 let metadata1 = vec![json!({"name": "Alice"}), json!({"name": "Bob"})];
2872 let doc_ids1: Vec<i64> = (0..2).collect();
2873
2874 create(path, &metadata1, &doc_ids1).unwrap();
2875 assert_eq!(count(path).unwrap(), 2);
2876
2877 let metadata2 = vec![json!({"name": "Charlie"})];
2878 let doc_ids2 = vec![2]; update(path, &metadata2, &doc_ids2).unwrap();
2881 assert_eq!(count(path).unwrap(), 3);
2882
2883 let results = get(path, None, &[], None).unwrap();
2885 assert_eq!(results[2]["_subset_"], 2);
2886 assert_eq!(results[2]["name"], "Charlie");
2887 }
2888
2889 #[test]
2890 fn test_update_adds_columns() {
2891 let dir = setup_test_dir();
2892 let path = dir.path().to_str().unwrap();
2893
2894 let metadata1 = vec![json!({"name": "Alice"})];
2895 let doc_ids1 = vec![0];
2896
2897 create(path, &metadata1, &doc_ids1).unwrap();
2898
2899 let metadata2 = vec![json!({"name": "Bob", "age": 25, "city": "NYC"})];
2900 let doc_ids2 = vec![1];
2901
2902 update(path, &metadata2, &doc_ids2).unwrap();
2903
2904 let results = get(path, None, &[], None).unwrap();
2906 assert_eq!(results[0]["name"], "Alice");
2907 assert!(results[0]["age"].is_null()); assert_eq!(results[1]["age"], 25);
2909 assert_eq!(results[1]["city"], "NYC");
2910 }
2911
2912 #[test]
2913 fn test_delete_and_reindex() {
2914 let dir = setup_test_dir();
2915 let path = dir.path().to_str().unwrap();
2916
2917 let metadata = vec![
2918 json!({"name": "Alice"}),
2919 json!({"name": "Bob"}),
2920 json!({"name": "Charlie"}),
2921 json!({"name": "Diana"}),
2922 ];
2923 let doc_ids: Vec<i64> = (0..4).collect();
2924
2925 create(path, &metadata, &doc_ids).unwrap();
2926
2927 let deleted = delete(path, &[1, 2]).unwrap();
2929 assert_eq!(deleted, 2);
2930 assert_eq!(count(path).unwrap(), 2);
2931
2932 let results = get(path, None, &[], None).unwrap();
2934 assert_eq!(results.len(), 2);
2935 assert_eq!(results[0]["_subset_"], 0);
2936 assert_eq!(results[0]["name"], "Alice");
2937 assert_eq!(results[1]["_subset_"], 1);
2938 assert_eq!(results[1]["name"], "Diana");
2939 }
2940
2941 #[test]
2947 fn test_delete_resequence_ignores_out_of_range_and_negative_ids() {
2948 let dir = setup_test_dir();
2949 let path = dir.path().to_str().unwrap();
2950
2951 let metadata = vec![
2952 json!({"name": "Alice"}), json!({"name": "Bob"}), json!({"name": "Charlie"}), json!({"name": "Diana"}), json!({"name": "Eve"}), json!({"name": "Frank"}), ];
2959 let doc_ids: Vec<i64> = (0..6).collect();
2960 create(path, &metadata, &doc_ids).unwrap();
2961
2962 let deleted = delete(path, &[1, 3, -5, 999]).unwrap();
2964 assert_eq!(deleted, 2, "only the two present ids are removed");
2965 assert_eq!(count(path).unwrap(), 4);
2966
2967 let rows = get(path, None, &[], None).unwrap();
2969 let got: Vec<(i64, String)> = rows
2970 .iter()
2971 .map(|r| {
2972 (
2973 r["_subset_"].as_i64().unwrap(),
2974 r["name"].as_str().unwrap().to_string(),
2975 )
2976 })
2977 .collect();
2978 assert_eq!(
2979 got,
2980 vec![
2981 (0, "Alice".into()),
2982 (1, "Charlie".into()),
2983 (2, "Eve".into()),
2984 (3, "Frank".into()),
2985 ]
2986 );
2987 }
2988
2989 #[test]
2990 fn test_where_with_like() {
2991 let dir = setup_test_dir();
2992 let path = dir.path().to_str().unwrap();
2993
2994 let metadata = vec![
2995 json!({"name": "Alice"}),
2996 json!({"name": "Alex"}),
2997 json!({"name": "Bob"}),
2998 ];
2999 let doc_ids: Vec<i64> = (0..3).collect();
3000
3001 create(path, &metadata, &doc_ids).unwrap();
3002
3003 let subset = where_condition(path, "name LIKE ?", &[json!("Al%")]).unwrap();
3004 assert_eq!(subset, vec![0, 1]);
3005 }
3006
3007 #[test]
3008 fn test_is_valid_column_name() {
3009 assert!(is_valid_column_name("name"));
3010 assert!(is_valid_column_name("_private"));
3011 assert!(is_valid_column_name("column123"));
3012 assert!(is_valid_column_name("Col_Name_2"));
3013
3014 assert!(!is_valid_column_name("123column")); assert!(!is_valid_column_name("column name")); assert!(!is_valid_column_name("column-name")); assert!(!is_valid_column_name("")); assert!(!is_valid_column_name("col;drop")); }
3020
3021 #[test]
3022 fn test_type_inference() {
3023 let dir = setup_test_dir();
3024 let path = dir.path().to_str().unwrap();
3025
3026 let metadata = vec![json!({
3027 "int_val": 42,
3028 "float_val": 3.125,
3029 "str_val": "hello",
3030 "bool_val": true,
3031 "null_val": null
3032 })];
3033 let doc_ids = vec![0];
3034
3035 create(path, &metadata, &doc_ids).unwrap();
3036
3037 let results = get(path, None, &[], None).unwrap();
3038 assert_eq!(results[0]["int_val"], 42);
3039 assert!((results[0]["float_val"].as_f64().unwrap() - 3.125).abs() < 0.001);
3040 assert_eq!(results[0]["str_val"], "hello");
3041 assert_eq!(results[0]["bool_val"], 1); assert!(results[0]["null_val"].is_null());
3043 }
3044
3045 fn test_columns() -> HashSet<String> {
3050 ["name", "category", "score", "status", "_subset_"]
3051 .iter()
3052 .map(|s| s.to_string())
3053 .collect()
3054 }
3055
3056 #[test]
3057 fn test_validator_simple_equality() {
3058 let cols = test_columns();
3059 assert!(validate_condition("name = ?", &cols).is_ok());
3060 assert!(validate_condition("score = ?", &cols).is_ok());
3061 }
3062
3063 #[test]
3064 fn test_validator_comparison_operators() {
3065 let cols = test_columns();
3066 assert!(validate_condition("score > ?", &cols).is_ok());
3067 assert!(validate_condition("score >= ?", &cols).is_ok());
3068 assert!(validate_condition("score < ?", &cols).is_ok());
3069 assert!(validate_condition("score <= ?", &cols).is_ok());
3070 assert!(validate_condition("score != ?", &cols).is_ok());
3071 assert!(validate_condition("score <> ?", &cols).is_ok());
3072 }
3073
3074 #[test]
3075 fn test_validator_and_or() {
3076 let cols = test_columns();
3077 assert!(validate_condition("name = ? AND score > ?", &cols).is_ok());
3078 assert!(validate_condition("category = ? OR status = ?", &cols).is_ok());
3079 assert!(validate_condition("name = ? AND score > ? OR category = ?", &cols).is_ok());
3080 }
3081
3082 #[test]
3083 fn test_validator_like() {
3084 let cols = test_columns();
3085 assert!(validate_condition("name LIKE ?", &cols).is_ok());
3086 assert!(validate_condition("name NOT LIKE ?", &cols).is_ok());
3087 }
3088
3089 #[test]
3090 fn test_validator_regexp() {
3091 let cols = test_columns();
3092 assert!(validate_condition("name REGEXP ?", &cols).is_ok());
3093 assert!(validate_condition("name NOT REGEXP ?", &cols).is_ok());
3094 }
3095
3096 #[test]
3097 fn test_validator_between() {
3098 let cols = test_columns();
3099 assert!(validate_condition("score BETWEEN ? AND ?", &cols).is_ok());
3100 assert!(validate_condition("score NOT BETWEEN ? AND ?", &cols).is_ok());
3101 }
3102
3103 #[test]
3104 fn test_validator_in() {
3105 let cols = test_columns();
3106 assert!(validate_condition("category IN (?)", &cols).is_ok());
3107 assert!(validate_condition("category IN (?, ?)", &cols).is_ok());
3108 assert!(validate_condition("category IN (?, ?, ?)", &cols).is_ok());
3109 assert!(validate_condition("category NOT IN (?, ?)", &cols).is_ok());
3110 }
3111
3112 #[test]
3113 fn test_validator_is_null() {
3114 let cols = test_columns();
3115 assert!(validate_condition("name IS NULL", &cols).is_ok());
3116 assert!(validate_condition("name IS NOT NULL", &cols).is_ok());
3117 }
3118
3119 #[test]
3120 fn test_validator_parentheses() {
3121 let cols = test_columns();
3122 assert!(validate_condition("(name = ?)", &cols).is_ok());
3123 assert!(validate_condition("(name = ? AND score > ?)", &cols).is_ok());
3124 assert!(validate_condition("(name = ? OR category = ?) AND score > ?", &cols).is_ok());
3125 assert!(validate_condition("name = ? AND (category = ? OR status = ?)", &cols).is_ok());
3126 }
3127
3128 #[test]
3129 fn test_validator_not() {
3130 let cols = test_columns();
3131 assert!(validate_condition("NOT name = ?", &cols).is_ok());
3132 assert!(validate_condition("NOT (name = ? AND score > ?)", &cols).is_ok());
3133 }
3134
3135 #[test]
3136 fn test_validator_quoted_identifiers() {
3137 let cols = test_columns();
3138 assert!(validate_condition("\"name\" = ?", &cols).is_ok());
3139 assert!(validate_condition("\"score\" > ?", &cols).is_ok());
3140 }
3141
3142 #[test]
3143 fn test_validator_case_insensitive_keywords() {
3144 let cols = test_columns();
3145 assert!(validate_condition("name = ? and score > ?", &cols).is_ok());
3146 assert!(validate_condition("name = ? AND score > ?", &cols).is_ok());
3147 assert!(validate_condition("name LIKE ? or category = ?", &cols).is_ok());
3148 assert!(validate_condition("score between ? and ?", &cols).is_ok());
3149 }
3150
3151 #[test]
3152 fn test_validator_allows_numeric_equality() {
3153 let cols = test_columns();
3156 assert!(validate_condition("1=1", &cols).is_ok());
3157 assert!(validate_condition(" 1=1 ", &cols).is_ok()); assert!(validate_condition("0=0", &cols).is_ok());
3159 assert!(validate_condition("1 = 1", &cols).is_ok()); assert!(validate_condition("42=42", &cols).is_ok());
3161 assert!(validate_condition("1=0", &cols).is_ok()); }
3163
3164 #[test]
3167 fn test_validator_rejects_semicolon() {
3168 let cols = test_columns();
3169 let result = validate_condition("name = ?; DROP TABLE METADATA", &cols);
3170 assert!(result.is_err());
3171 assert!(result.unwrap_err().to_string().contains("Semicolon"));
3172 }
3173
3174 #[test]
3175 fn test_validator_rejects_comments() {
3176 let cols = test_columns();
3177 assert!(validate_condition("name = ? -- comment", &cols).is_err());
3178 assert!(validate_condition("name = ? /* comment */", &cols).is_err());
3179 }
3180
3181 #[test]
3182 fn test_validator_rejects_union() {
3183 let cols = test_columns();
3184 let result = validate_condition("name = ? UNION SELECT * FROM users", &cols);
3186 assert!(result.is_err());
3187 let err_msg = result.unwrap_err().to_string();
3189 assert!(
3190 err_msg.contains("UNION") || err_msg.contains("SELECT"),
3191 "Expected error about UNION or SELECT, got: {}",
3192 err_msg
3193 );
3194 }
3195
3196 #[test]
3197 fn test_validator_rejects_subqueries() {
3198 let cols = test_columns();
3199 let result = validate_condition("name = (SELECT name FROM users)", &cols);
3201 assert!(result.is_err());
3202 }
3203
3204 #[test]
3205 fn test_validator_rejects_ddl_keywords() {
3206 let cols = test_columns();
3207 assert!(validate_condition("DROP TABLE METADATA", &cols).is_err());
3208 assert!(validate_condition("DELETE FROM METADATA", &cols).is_err());
3209 assert!(validate_condition("INSERT INTO METADATA VALUES (?)", &cols).is_err());
3210 assert!(validate_condition("UPDATE METADATA SET name = ?", &cols).is_err());
3211 assert!(validate_condition("CREATE TABLE foo (id INT)", &cols).is_err());
3212 assert!(validate_condition("ALTER TABLE METADATA ADD x INT", &cols).is_err());
3213 assert!(validate_condition("TRUNCATE TABLE METADATA", &cols).is_err());
3214 }
3215
3216 #[test]
3217 fn test_validator_rejects_unknown_columns() {
3218 let cols = test_columns();
3219 let result = validate_condition("unknown_column = ?", &cols);
3220 assert!(result.is_err());
3221 assert!(result.unwrap_err().to_string().contains("Unknown column"));
3222 }
3223
3224 #[test]
3225 fn test_validator_rejects_string_literals() {
3226 let cols = test_columns();
3227 let result = validate_condition("name = 'Alice'", &cols);
3229 assert!(result.is_err());
3230 }
3231
3232 #[test]
3233 fn test_validator_rejects_malformed_syntax() {
3234 let cols = test_columns();
3235 assert!(validate_condition("name =", &cols).is_err());
3237 assert!(validate_condition("(name = ?", &cols).is_err());
3239 assert!(validate_condition("name = ?)", &cols).is_err());
3240 assert!(validate_condition("name = = ?", &cols).is_err());
3242 assert!(validate_condition("= ?", &cols).is_err());
3244 }
3245
3246 #[test]
3247 fn test_validator_rejects_function_calls() {
3248 let cols = test_columns();
3249 let result = validate_condition("LENGTH(name) > ?", &cols);
3251 assert!(result.is_err());
3253 }
3254
3255 #[test]
3256 fn test_validator_integration() {
3257 let dir = setup_test_dir();
3259 let path = dir.path().to_str().unwrap();
3260
3261 let metadata = vec![
3262 json!({"name": "Alice", "category": "A", "score": 95}),
3263 json!({"name": "Bob", "category": "B", "score": 87}),
3264 ];
3265 let doc_ids: Vec<i64> = (0..2).collect();
3266 create(path, &metadata, &doc_ids).unwrap();
3267
3268 let result = where_condition(path, "category = ? AND score > ?", &[json!("A"), json!(90)]);
3270 assert!(result.is_ok());
3271 assert_eq!(result.unwrap(), vec![0]);
3272
3273 let result = where_condition(path, "category = ?; DROP TABLE METADATA", &[json!("A")]);
3275 assert!(result.is_err());
3276
3277 let result = where_condition(path, "unknown = ?", &[json!("test")]);
3279 assert!(result.is_err());
3280 }
3281
3282 #[test]
3283 fn test_validator_integration_get() {
3284 let dir = setup_test_dir();
3285 let path = dir.path().to_str().unwrap();
3286
3287 let metadata = vec![
3288 json!({"name": "Alice", "score": 95}),
3289 json!({"name": "Bob", "score": 87}),
3290 ];
3291 let doc_ids: Vec<i64> = (0..2).collect();
3292 create(path, &metadata, &doc_ids).unwrap();
3293
3294 let result = get(path, Some("score > ?"), &[json!(90)], None);
3296 assert!(result.is_ok());
3297 assert_eq!(result.unwrap().len(), 1);
3298
3299 let result = get(path, Some("1=1 UNION SELECT * FROM users"), &[], None);
3301 assert!(result.is_err());
3302 }
3303
3304 #[test]
3305 fn test_create_with_empty_metadata_objects() {
3306 let dir = setup_test_dir();
3307 let path = dir.path().to_str().unwrap();
3308
3309 let metadata = vec![json!({}), json!({})];
3310 let doc_ids: Vec<i64> = vec![0, 1];
3311
3312 let result = create(path, &metadata, &doc_ids).unwrap();
3313 assert_eq!(result, 2);
3314 assert!(exists(path));
3315 assert_eq!(count(path).unwrap(), 2);
3316
3317 let all = get(path, None, &[], None).unwrap();
3319 assert_eq!(all.len(), 2);
3320 }
3321
3322 #[test]
3323 fn test_update_with_empty_metadata_objects() {
3324 let dir = setup_test_dir();
3325 let path = dir.path().to_str().unwrap();
3326
3327 let metadata = vec![json!({})];
3329 let doc_ids: Vec<i64> = vec![0];
3330 create(path, &metadata, &doc_ids).unwrap();
3331
3332 let new_metadata = vec![json!({})];
3334 let new_doc_ids: Vec<i64> = vec![1];
3335 let result = update(path, &new_metadata, &new_doc_ids).unwrap();
3336 assert_eq!(result, 1);
3337 assert_eq!(count(path).unwrap(), 2);
3338 }
3339
3340 #[test]
3341 fn test_create_with_mixed_empty_and_non_empty_metadata() {
3342 let dir = setup_test_dir();
3343 let path = dir.path().to_str().unwrap();
3344
3345 let metadata = vec![
3347 json!({"name": "Alice", "score": 95}),
3348 json!({}),
3349 json!({"name": "Charlie"}),
3350 ];
3351 let doc_ids: Vec<i64> = vec![0, 1, 2];
3352
3353 let result = create(path, &metadata, &doc_ids).unwrap();
3354 assert_eq!(result, 3);
3355 assert_eq!(count(path).unwrap(), 3);
3356
3357 let with_name = get(path, Some("name IS NOT NULL"), &[], None).unwrap();
3359 assert_eq!(with_name.len(), 2);
3360 }
3361
3362 #[test]
3363 fn test_update_with_mixed_empty_and_non_empty_metadata() {
3364 let dir = setup_test_dir();
3365 let path = dir.path().to_str().unwrap();
3366
3367 let metadata = vec![json!({"name": "Alice"})];
3369 let doc_ids: Vec<i64> = vec![0];
3370 create(path, &metadata, &doc_ids).unwrap();
3371
3372 let new_metadata = vec![json!({})];
3374 let new_doc_ids: Vec<i64> = vec![1];
3375 let result = update(path, &new_metadata, &new_doc_ids).unwrap();
3376 assert_eq!(result, 1);
3377 assert_eq!(count(path).unwrap(), 2);
3378
3379 let with_name = get(path, Some("name IS NOT NULL"), &[], None).unwrap();
3381 assert_eq!(with_name.len(), 1);
3382 }
3383
3384 #[test]
3385 fn test_read_only_helpers_work_with_query_only_connections() {
3386 let dir = setup_test_dir();
3387 let path = dir.path().to_str().unwrap();
3388
3389 let metadata: Vec<Value> = (0..950)
3390 .map(|i| {
3391 json!({
3392 "category": if i % 2 == 0 { "A" } else { "B" },
3393 "source": format!("doc-{i}")
3394 })
3395 })
3396 .collect();
3397 let doc_ids: Vec<i64> = (0..950).collect();
3398 create(path, &metadata, &doc_ids).unwrap();
3399
3400 assert_eq!(count(path).unwrap(), 950);
3401
3402 let mut sources = get_distinct_strings(path, "category").unwrap();
3403 sources.sort();
3404 assert_eq!(sources, vec!["A".to_string(), "B".to_string()]);
3405
3406 let filtered = where_condition(path, "category = ?", &[json!("A")]).unwrap();
3407 assert_eq!(filtered.len(), 475);
3408
3409 let large_subset: Vec<i64> = (0..950).collect();
3410 let rows = get(path, None, &[], Some(&large_subset)).unwrap();
3411 assert_eq!(rows.len(), 950);
3412 assert_eq!(rows[0]["_subset_"], json!(0));
3413 assert_eq!(rows[949]["_subset_"], json!(949));
3414 }
3415
3416 #[test]
3417 fn test_update_fixed_schema_fast_path_reuses_connection() {
3418 let dir = setup_test_dir();
3419 let path = dir.path().to_str().unwrap();
3420
3421 let metadata = vec![json!({"category": "A", "source": "doc-0"})];
3422 create(path, &metadata, &[0]).unwrap();
3423
3424 let new_metadata = vec![
3425 json!({"category": "B", "source": "doc-1"}),
3426 json!({"category": "A", "source": "doc-2"}),
3427 ];
3428 let inserted = update(path, &new_metadata, &[1, 2]).unwrap();
3429 assert_eq!(inserted, 2);
3430
3431 let rows = get(path, None, &[], Some(&[0, 1, 2])).unwrap();
3432 assert_eq!(rows.len(), 3);
3433 assert_eq!(rows[1]["source"], json!("doc-1"));
3434 assert_eq!(count(path).unwrap(), 3);
3435 }
3436
3437 #[test]
3438 fn test_concurrent_metadata_reads_during_updates() {
3439 let dir = setup_test_dir();
3440 let path = dir.path().to_str().unwrap().to_string();
3441
3442 let metadata: Vec<Value> = (0..20)
3443 .map(|i| {
3444 json!({
3445 "category": if i % 2 == 0 { "A" } else { "B" },
3446 "source": format!("doc-{i}")
3447 })
3448 })
3449 .collect();
3450 let doc_ids: Vec<i64> = (0..20).collect();
3451 create(&path, &metadata, &doc_ids).unwrap();
3452
3453 let reader_count = 8;
3454 let barrier = Arc::new(std::sync::Barrier::new(reader_count + 1));
3455
3456 std::thread::scope(|scope| {
3457 for _ in 0..reader_count {
3458 let path = path.clone();
3459 let barrier = Arc::clone(&barrier);
3460 scope.spawn(move || {
3461 barrier.wait();
3462 for _ in 0..100 {
3463 let ids = where_condition(&path, "category = ?", &[json!("A")]).unwrap();
3464 assert!(!ids.is_empty());
3465 let subset_len = ids.len().min(3);
3466 let rows = get(&path, None, &[], Some(&ids[..subset_len])).unwrap();
3467 assert_eq!(rows.len(), subset_len);
3468 assert!(count(&path).unwrap() >= 20);
3469 }
3470 });
3471 }
3472
3473 let writer_path = path.clone();
3474 let barrier = Arc::clone(&barrier);
3475 scope.spawn(move || {
3476 barrier.wait();
3477 for i in 20..80 {
3478 let metadata = vec![json!({
3479 "category": if i % 2 == 0 { "A" } else { "B" },
3480 "source": format!("doc-{i}")
3481 })];
3482 update(&writer_path, &metadata, &[i]).unwrap();
3483 }
3484 });
3485 });
3486
3487 assert_eq!(count(&path).unwrap(), 80);
3488 }
3489
3490 fn meta_db(path: &str) -> Connection {
3493 Connection::open(std::path::Path::new(path).join(METADATA_DB_NAME)).unwrap()
3494 }
3495
3496 fn user_version_of(path: &str) -> i64 {
3497 meta_db(path)
3498 .query_row("PRAGMA user_version", [], |r| r.get(0))
3499 .unwrap()
3500 }
3501
3502 fn subset_is_pk(path: &str) -> bool {
3503 let c = meta_db(path);
3504 let mut stmt = c.prepare("PRAGMA table_info(METADATA)").unwrap();
3505 let rows = stmt
3506 .query_map([], |row| {
3507 Ok((row.get::<_, String>(1)?, row.get::<_, i64>(5)?))
3508 })
3509 .unwrap();
3510 for r in rows {
3511 let (name, pk) = r.unwrap();
3512 if name == SUBSET_COLUMN && pk > 0 {
3513 return true;
3514 }
3515 }
3516 false
3517 }
3518
3519 fn select_star_columns(path: &str) -> Vec<String> {
3520 let rows = get(path, None, &[], None).unwrap();
3521 rows[0].as_object().unwrap().keys().cloned().collect()
3522 }
3523
3524 #[test]
3525 fn test_create_uses_v2_layout() {
3526 let dir = setup_test_dir();
3527 let path = dir.path().to_str().unwrap();
3528 let meta: Vec<serde_json::Value> = (0..5)
3529 .map(|i| json!({"file": format!("f{i}.rs"), "code": format!("c{i}")}))
3530 .collect();
3531 create(path, &meta, &(0..5).collect::<Vec<i64>>()).unwrap();
3532
3533 assert_eq!(user_version_of(path), 2);
3534 assert!(!subset_is_pk(path), "v2: _subset_ must not be the PK/rowid");
3535
3536 let cols = select_star_columns(path);
3538 assert!(cols.iter().any(|c| c == SUBSET_COLUMN));
3539 assert!(cols.iter().any(|c| c == "file") && cols.iter().any(|c| c == "code"));
3540 assert!(!cols.iter().any(|c| c == CONTENT_ID_COLUMN));
3541
3542 let c = meta_db(path);
3544 let has_content: i64 = c
3545 .query_row(
3546 &format!(
3547 "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='{}'",
3548 CONTENT_TABLE
3549 ),
3550 [],
3551 |row| row.get(0),
3552 )
3553 .unwrap();
3554 assert_eq!(has_content, 1);
3555 }
3556
3557 #[test]
3558 fn test_delete_resequences_dense() {
3559 let dir = setup_test_dir();
3560 let path = dir.path().to_str().unwrap();
3561 let meta: Vec<serde_json::Value> = (0..10)
3562 .map(|i| json!({"file": format!("f{i}.rs")}))
3563 .collect();
3564 create(path, &meta, &(0..10).collect::<Vec<i64>>()).unwrap();
3565
3566 assert_eq!(delete(path, &[2, 5, 7]).unwrap(), 3);
3567
3568 let rows = get(path, None, &[], None).unwrap();
3569 assert_eq!(rows.len(), 7);
3570 let expected = [
3571 "f0.rs", "f1.rs", "f3.rs", "f4.rs", "f6.rs", "f8.rs", "f9.rs",
3572 ];
3573 for (i, row) in rows.iter().enumerate() {
3574 assert_eq!(
3575 row[SUBSET_COLUMN].as_i64().unwrap(),
3576 i as i64,
3577 "dense 0-based"
3578 );
3579 assert_eq!(
3580 row["file"].as_str().unwrap(),
3581 expected[i],
3582 "survivor order preserved"
3583 );
3584 }
3585 }
3586
3587 #[test]
3588 fn test_legacy_v0_index_migrates_on_delete() {
3589 let dir = setup_test_dir();
3590 let path = dir.path().to_str().unwrap();
3591
3592 {
3595 let c = meta_db(path);
3596 c.execute_batch("PRAGMA user_version=0;").unwrap();
3597 c.execute(
3598 &format!(
3599 "CREATE TABLE METADATA (\"{}\" INTEGER PRIMARY KEY, file TEXT, code TEXT)",
3600 SUBSET_COLUMN
3601 ),
3602 [],
3603 )
3604 .unwrap();
3605 for i in 0..10i64 {
3606 c.execute(
3607 "INSERT INTO METADATA VALUES (?, ?, ?)",
3608 rusqlite::params![i, format!("f{i}.rs"), format!("c{i}")],
3609 )
3610 .unwrap();
3611 }
3612 }
3613 assert!(subset_is_pk(path), "precondition: legacy v0 layout");
3614 assert_eq!(user_version_of(path), 0);
3615
3616 assert_eq!(delete(path, &[3]).unwrap(), 1);
3618
3619 assert_eq!(user_version_of(path), 1, "migrated to v1");
3620 assert!(!subset_is_pk(path), "_subset_ demoted from PK");
3621
3622 let cols = select_star_columns(path);
3624 assert!(!cols
3625 .iter()
3626 .any(|c| c == "rowid" || c.starts_with("_METADATA")));
3627 assert!(cols.iter().any(|c| c == "file") && cols.iter().any(|c| c == "code"));
3628
3629 let rows = get(path, None, &[], None).unwrap();
3631 assert_eq!(rows.len(), 9);
3632 for (i, row) in rows.iter().enumerate() {
3633 assert_eq!(row[SUBSET_COLUMN].as_i64().unwrap(), i as i64);
3634 }
3635 assert_eq!(rows[3]["file"].as_str().unwrap(), "f4.rs");
3636 assert_eq!(rows[3]["code"].as_str().unwrap(), "c4");
3637 }
3638
3639 #[test]
3642 fn test_v2_delete_resequences_thin_only() {
3643 let dir = setup_test_dir();
3644 let path = dir.path().to_str().unwrap();
3645 let meta: Vec<Value> = (0..10)
3646 .map(|i| {
3647 json!({
3648 "file": format!("f{i}.rs"),
3649 "code": format!("fn func_{i}() {{}}"),
3650 })
3651 })
3652 .collect();
3653 create(path, &meta, &(0..10).collect::<Vec<i64>>()).unwrap();
3654 assert_eq!(user_version_of(path), 2);
3655
3656 let c = meta_db(path);
3658 let content_count_before: i64 = c
3659 .query_row(
3660 &format!("SELECT COUNT(*) FROM {}", CONTENT_TABLE),
3661 [],
3662 |row| row.get(0),
3663 )
3664 .unwrap();
3665 assert_eq!(content_count_before, 10);
3666
3667 assert_eq!(delete(path, &[2, 5, 7]).unwrap(), 3);
3669
3670 let rows = get(path, None, &[], None).unwrap();
3672 assert_eq!(rows.len(), 7);
3673 for (i, row) in rows.iter().enumerate() {
3674 assert_eq!(row[SUBSET_COLUMN].as_i64().unwrap(), i as i64);
3675 }
3676 assert_eq!(rows[0]["file"].as_str().unwrap(), "f0.rs");
3678 assert_eq!(rows[2]["file"].as_str().unwrap(), "f3.rs");
3679
3680 let c = meta_db(path);
3682 let content_count_after: i64 = c
3683 .query_row(
3684 &format!("SELECT COUNT(*) FROM {}", CONTENT_TABLE),
3685 [],
3686 |row| row.get(0),
3687 )
3688 .unwrap();
3689 assert_eq!(content_count_after, 7);
3690
3691 let max_content_id: i64 = c
3694 .query_row(
3695 &format!(
3696 "SELECT MAX(\"{}\") FROM {}",
3697 CONTENT_ID_COLUMN, CONTENT_TABLE
3698 ),
3699 [],
3700 |row| row.get(0),
3701 )
3702 .unwrap();
3703 assert!(max_content_id >= 7, "content IDs are stable, not compacted");
3704 }
3705
3706 #[test]
3707 fn test_v2_get_returns_all_columns() {
3708 let dir = setup_test_dir();
3709 let path = dir.path().to_str().unwrap();
3710 let meta = vec![
3711 json!({
3712 "file": "src/main.rs",
3713 "name": "main",
3714 "line": 1,
3715 "code": "fn main() { println!(\"hello\"); }",
3716 "signature": "fn main()",
3717 }),
3718 json!({
3719 "file": "src/lib.rs",
3720 "name": "helper",
3721 "line": 10,
3722 "code": "fn helper() -> i32 { 42 }",
3723 "signature": "fn helper() -> i32",
3724 }),
3725 ];
3726 create(path, &meta, &[0, 1]).unwrap();
3727
3728 let rows = get(path, None, &[], None).unwrap();
3729 assert_eq!(rows.len(), 2);
3730
3731 assert_eq!(rows[0]["file"], "src/main.rs");
3733 assert_eq!(rows[0]["name"], "main");
3734 assert_eq!(rows[0]["line"], 1);
3735
3736 assert_eq!(rows[0]["code"], "fn main() { println!(\"hello\"); }");
3738 assert_eq!(rows[0]["signature"], "fn main()");
3739
3740 assert!(rows[0].get(CONTENT_ID_COLUMN).is_none());
3742
3743 assert_eq!(rows[0][SUBSET_COLUMN], 0);
3745 assert_eq!(rows[1][SUBSET_COLUMN], 1);
3746 }
3747
3748 #[test]
3749 fn test_v2_where_condition_on_thin_column() {
3750 let dir = setup_test_dir();
3751 let path = dir.path().to_str().unwrap();
3752 let meta = vec![
3753 json!({"file": "src/a.rs", "name": "foo", "code": "fn foo() {}"}),
3754 json!({"file": "src/b.rs", "name": "bar", "code": "fn bar() {}"}),
3755 json!({"file": "src/a.rs", "name": "baz", "code": "fn baz() {}"}),
3756 ];
3757 create(path, &meta, &[0, 1, 2]).unwrap();
3758
3759 let ids = where_condition(path, "file = ?", &[json!("src/a.rs")]).unwrap();
3760 assert_eq!(ids, vec![0, 2]);
3761 }
3762
3763 #[test]
3764 fn test_v2_where_condition_regexp_on_fat_column() {
3765 let dir = setup_test_dir();
3766 let path = dir.path().to_str().unwrap();
3767 let meta = vec![
3768 json!({"file": "a.rs", "code": "fn alpha() { let x = async_fn().await; }"}),
3769 json!({"file": "b.rs", "code": "fn beta() { println!(\"hi\"); }"}),
3770 json!({"file": "c.rs", "code": "fn gamma() { let y = tokio::spawn(async {}); }"}),
3771 ];
3772 create(path, &meta, &[0, 1, 2]).unwrap();
3773
3774 let ids = where_condition_regexp(path, "code REGEXP ?", &[json!("async")]).unwrap();
3775 let mut ids_sorted = ids.clone();
3776 ids_sorted.sort();
3777 assert_eq!(ids_sorted, vec![0, 2]);
3778 }
3779
3780 #[test]
3781 fn test_v1_index_still_works() {
3782 let dir = setup_test_dir();
3783 let path = dir.path().to_str().unwrap();
3784
3785 {
3787 let c = meta_db(path);
3788 c.execute_batch(&format!("PRAGMA user_version={};", METADATA_SCHEMA_V1))
3789 .unwrap();
3790 c.execute(
3791 &format!(
3792 "CREATE TABLE METADATA (\"{}\" INTEGER NOT NULL, file TEXT, code TEXT)",
3793 SUBSET_COLUMN
3794 ),
3795 [],
3796 )
3797 .unwrap();
3798 c.execute(
3799 &format!(
3800 "CREATE INDEX \"{}\" ON METADATA (\"{}\")",
3801 SUBSET_INDEX_NAME, SUBSET_COLUMN
3802 ),
3803 [],
3804 )
3805 .unwrap();
3806 for i in 0..5i64 {
3807 c.execute(
3808 "INSERT INTO METADATA VALUES (?, ?, ?)",
3809 rusqlite::params![i, format!("f{i}.rs"), format!("c{i}")],
3810 )
3811 .unwrap();
3812 }
3813 }
3814
3815 assert_eq!(user_version_of(path), 1);
3816 assert_eq!(count(path).unwrap(), 5);
3817
3818 let rows = get(path, None, &[], None).unwrap();
3820 assert_eq!(rows.len(), 5);
3821 assert_eq!(rows[0]["file"], "f0.rs");
3822 assert_eq!(rows[0]["code"], "c0");
3823
3824 let ids = where_condition(path, "file = ?", &[json!("f2.rs")]).unwrap();
3826 assert_eq!(ids, vec![2]);
3827
3828 assert_eq!(delete(path, &[1]).unwrap(), 1);
3830 assert_eq!(count(path).unwrap(), 4);
3831 let rows = get(path, None, &[], None).unwrap();
3832 assert_eq!(rows[1]["file"], "f2.rs");
3833 assert_eq!(rows[1][SUBSET_COLUMN], 1);
3834
3835 let new_meta = vec![json!({"file": "f5.rs", "code": "c5"})];
3837 update(path, &new_meta, &[4]).unwrap();
3838 assert_eq!(count(path).unwrap(), 5);
3839 }
3840}