azoth_vector/
migration.rs1use crate::types::VectorConfig;
4use azoth_core::{error::AzothError, Result};
5use rusqlite::Connection;
6
7pub fn create_vector_table(
43 conn: &Connection,
44 table_name: &str,
45 schema: &str,
46 vector_column: &str,
47 config: VectorConfig,
48) -> Result<()> {
49 if !is_valid_identifier(table_name) {
51 return Err(AzothError::InvalidState(format!(
52 "Invalid table name: {}",
53 table_name
54 )));
55 }
56 if !is_valid_identifier(vector_column) {
57 return Err(AzothError::InvalidState(format!(
58 "Invalid column name: {}",
59 vector_column
60 )));
61 }
62
63 conn.execute(&format!("CREATE TABLE {} ({})", table_name, schema), [])
65 .map_err(|e| AzothError::Projection(format!("Failed to create table: {}", e)))?;
66
67 let config_str = config.to_config_string();
69 conn.query_row(
70 &format!(
71 "SELECT vector_init('{}', '{}', ?)",
72 table_name.replace('\'', "''"),
73 vector_column.replace('\'', "''")
74 ),
75 [&config_str],
76 |_row| Ok(()),
77 )
78 .map_err(|e| AzothError::Projection(format!("Failed to init vector column: {}", e)))?;
79
80 tracing::info!(
81 "Created table {} with vector column {} ({})",
82 table_name,
83 vector_column,
84 &config_str
85 );
86
87 Ok(())
88}
89
90pub fn add_vector_column(
125 conn: &Connection,
126 table_name: &str,
127 column_name: &str,
128 config: VectorConfig,
129) -> Result<()> {
130 if !is_valid_identifier(table_name) {
132 return Err(AzothError::InvalidState(format!(
133 "Invalid table name: {}",
134 table_name
135 )));
136 }
137 if !is_valid_identifier(column_name) {
138 return Err(AzothError::InvalidState(format!(
139 "Invalid column name: {}",
140 column_name
141 )));
142 }
143
144 conn.execute(
146 &format!("ALTER TABLE {} ADD COLUMN {} BLOB", table_name, column_name),
147 [],
148 )
149 .map_err(|e| AzothError::Projection(format!("Failed to add column: {}", e)))?;
150
151 let config_str = config.to_config_string();
153 conn.execute(
154 &format!("SELECT vector_init('{}', '{}', ?)", table_name, column_name),
155 [&config_str],
156 )
157 .map_err(|e| AzothError::Projection(format!("Failed to init vector column: {}", e)))?;
158
159 tracing::info!(
160 "Added vector column {}.{} ({})",
161 table_name,
162 column_name,
163 &config_str
164 );
165
166 Ok(())
167}
168
169fn is_valid_identifier(name: &str) -> bool {
173 if name.is_empty() {
174 return false;
175 }
176
177 let first_char = name.chars().next().unwrap();
178 if !first_char.is_ascii_alphabetic() && first_char != '_' {
179 return false;
180 }
181
182 name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188
189 #[test]
190 fn test_valid_identifier() {
191 assert!(is_valid_identifier("table_name"));
192 assert!(is_valid_identifier("_private"));
193 assert!(is_valid_identifier("Table123"));
194 assert!(is_valid_identifier("_"));
195
196 assert!(!is_valid_identifier(""));
197 assert!(!is_valid_identifier("123table"));
198 assert!(!is_valid_identifier("table-name"));
199 assert!(!is_valid_identifier("table.name"));
200 assert!(!is_valid_identifier("table name"));
201 assert!(!is_valid_identifier("table'name"));
202 }
203
204 #[test]
205 fn test_create_table_invalid_name() {
206 use tempfile::tempdir;
207 let dir = tempdir().unwrap();
208 let db_path = dir.path().join("test.db");
209 let conn = Connection::open(&db_path).unwrap();
210
211 let result = create_vector_table(
212 &conn,
213 "invalid-name",
214 "id INTEGER",
215 "vector",
216 VectorConfig::default(),
217 );
218
219 assert!(result.is_err());
220 }
221
222 }