use daimon_core::{DaimonError, Result};
use deadpool_postgres::{Config, Pool, Runtime};
use tokio_postgres::NoTls;
use crate::DistanceMetric;
use crate::migrations;
use crate::store::PgVectorStore;
pub struct PgVectorStoreBuilder {
connection_string: String,
dimensions: usize,
table: String,
distance_metric: DistanceMetric,
auto_migrate: bool,
hnsw_m: Option<usize>,
hnsw_ef_construction: Option<usize>,
pool_size: usize,
}
impl PgVectorStoreBuilder {
pub fn new(connection_string: impl Into<String>, dimensions: usize) -> Self {
Self {
connection_string: connection_string.into(),
dimensions,
table: "daimon_vectors".into(),
distance_metric: DistanceMetric::Cosine,
auto_migrate: true,
hnsw_m: None,
hnsw_ef_construction: None,
pool_size: 16,
}
}
pub fn table(mut self, table: impl Into<String>) -> Self {
self.table = table.into();
self
}
pub fn distance_metric(mut self, metric: DistanceMetric) -> Self {
self.distance_metric = metric;
self
}
pub fn auto_migrate(mut self, enabled: bool) -> Self {
self.auto_migrate = enabled;
self
}
pub fn hnsw_m(mut self, m: usize) -> Self {
self.hnsw_m = Some(m);
self
}
pub fn hnsw_ef_construction(mut self, ef: usize) -> Self {
self.hnsw_ef_construction = Some(ef);
self
}
pub fn pool_size(mut self, size: usize) -> Self {
self.pool_size = size;
self
}
pub async fn build(self) -> Result<PgVectorStore> {
validate_table_name(&self.table)?;
let pool = self.create_pool()?;
if self.auto_migrate {
self.run_migrations(&pool).await?;
}
Ok(PgVectorStore {
pool,
table: self.table,
dimensions: self.dimensions,
distance_metric: self.distance_metric,
})
}
fn create_pool(&self) -> Result<Pool> {
let mut cfg = Config::new();
cfg.url = Some(self.connection_string.clone());
cfg.pool = Some(deadpool_postgres::PoolConfig {
max_size: self.pool_size,
..Default::default()
});
cfg.create_pool(Some(Runtime::Tokio1), NoTls)
.map_err(|e| DaimonError::Other(format!("pgvector pool creation error: {e}")))
}
async fn run_migrations(&self, pool: &Pool) -> Result<()> {
let client = pool
.get()
.await
.map_err(|e| DaimonError::Other(format!("pgvector migration pool error: {e}")))?;
tracing::info!("pgvector: creating extension and table '{}'", self.table);
client
.execute(migrations::CREATE_EXTENSION, &[])
.await
.map_err(|e| DaimonError::Other(format!("pgvector CREATE EXTENSION error: {e}")))?;
let create_table = migrations::create_table_sql(&self.table, self.dimensions);
client
.execute(&create_table as &str, &[])
.await
.map_err(|e| DaimonError::Other(format!("pgvector CREATE TABLE error: {e}")))?;
let ops_class = self.distance_metric.ops_class();
let create_index = migrations::create_hnsw_index_sql(
&self.table,
ops_class,
self.hnsw_m,
self.hnsw_ef_construction,
);
client
.execute(&create_index as &str, &[])
.await
.map_err(|e| DaimonError::Other(format!("pgvector CREATE INDEX error: {e}")))?;
tracing::info!("pgvector: migration complete for '{}'", self.table);
Ok(())
}
}
fn validate_table_name(table: &str) -> Result<()> {
fn is_valid_part(part: &str) -> bool {
let mut chars = part.chars();
match chars.next() {
Some(c) if c.is_ascii_alphabetic() || c == '_' => {}
_ => return false,
}
chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
}
if table.is_empty() {
return Err(DaimonError::Other(
"pgvector: table name must not be empty".to_string(),
));
}
let parts: Vec<&str> = table.split('.').collect();
if parts.len() > 2 || !parts.iter().all(|p| is_valid_part(p)) {
return Err(DaimonError::Other(format!(
"pgvector: invalid table name '{table}': expected an identifier matching \
[A-Za-z_][A-Za-z0-9_]* (optionally schema-qualified as schema.table)"
)));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_valid_table_names() {
assert!(validate_table_name("embeddings").is_ok());
assert!(validate_table_name("daimon_vectors").is_ok());
assert!(validate_table_name("_private").is_ok());
assert!(validate_table_name("Table123").is_ok());
assert!(validate_table_name("public.embeddings").is_ok());
}
#[test]
fn test_invalid_table_names_rejected() {
assert!(validate_table_name("").is_err());
assert!(validate_table_name("foo; DROP TABLE bar").is_err());
assert!(validate_table_name("foo; DROP TABLE bar; --").is_err());
assert!(validate_table_name("\"foo\"").is_err());
assert!(validate_table_name("foo bar").is_err());
assert!(validate_table_name("1foo").is_err());
assert!(validate_table_name("foo.bar.baz").is_err());
assert!(validate_table_name("foo'").is_err());
assert!(validate_table_name("foo)").is_err());
assert!(validate_table_name("schema..table").is_err());
}
}