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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ members = [
resolver = "2"

[workspace.package]
version = "2.0.2"
version = "2.0.3"
edition = "2021"
rust-version = "1.77"
license = "MIT"
Expand Down Expand Up @@ -171,7 +171,7 @@ opt-level = 3
lto = "fat"
codegen-units = 1
strip = true
panic = "abort"
panic = "unwind"

[profile.bench]
inherits = "release"
Expand Down
2 changes: 1 addition & 1 deletion crates/ruvector-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ tokio-postgres = { version = "0.7", optional = true }
deadpool-postgres = { version = "0.14", optional = true }

# LRU cache for performance optimization
lru = "0.12"
lru = "0.16"

# Compression for storage
flate2 = "1.0"
Expand Down
2 changes: 1 addition & 1 deletion crates/ruvector-graph/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ pest_derive = { version = "2.7", optional = true }
lalrpop-util = { version = "0.21", optional = true }

# Cache
lru = "0.12"
lru = "0.16"
moka = { version = "0.12", features = ["future"], optional = true }

# Compression (for storage optimization, optional for WASM)
Expand Down
2 changes: 1 addition & 1 deletion crates/ruvector-postgres/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "ruvector-postgres"
version = "2.0.1"
version = "2.0.2"
edition = "2021"
license = "MIT"
description = "High-performance PostgreSQL vector database extension v2 - pgvector drop-in replacement with 230+ SQL functions, SIMD acceleration, Flash Attention, GNN layers, hybrid search, multi-tenancy, self-healing, and self-learning capabilities"
Expand Down
4 changes: 2 additions & 2 deletions crates/ruvector-postgres/sql/ruvector--2.0.0.sql
Original file line number Diff line number Diff line change
Expand Up @@ -525,7 +525,7 @@ LANGUAGE C VOLATILE PARALLEL SAFE;

-- List all agents
CREATE OR REPLACE FUNCTION ruvector_list_agents()
RETURNS SETOF jsonb
RETURNS TABLE(name text, agent_type text, capabilities text[], cost_per_request real, avg_latency_ms real, quality_score real, success_rate real, total_requests bigint, is_active boolean)
AS 'MODULE_PATHNAME', 'ruvector_list_agents_wrapper'
LANGUAGE C VOLATILE PARALLEL SAFE;

Expand All @@ -537,7 +537,7 @@ LANGUAGE C VOLATILE PARALLEL SAFE;

-- Find agents by capability
CREATE OR REPLACE FUNCTION ruvector_find_agents_by_capability(capability text, max_results int DEFAULT 10)
RETURNS SETOF jsonb
RETURNS TABLE(name text, quality_score real, avg_latency_ms real, cost_per_request real)
AS 'MODULE_PATHNAME', 'ruvector_find_agents_by_capability_wrapper'
LANGUAGE C VOLATILE PARALLEL SAFE;

Expand Down
10 changes: 10 additions & 0 deletions crates/ruvector-postgres/src/graph/operators.rs
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,11 @@ fn ruvector_create_rdf_store(name: &str) -> bool {
/// ```
#[pg_extern]
fn ruvector_sparql(store_name: &str, query: &str, format: &str) -> Result<String, String> {
// Validate input to prevent panics
if query.trim().is_empty() {
return Err("SPARQL query cannot be empty".to_string());
}

let store = get_store(store_name)
.ok_or_else(|| format!("Triple store '{}' does not exist", store_name))?;

Expand All @@ -350,6 +355,11 @@ fn ruvector_sparql(store_name: &str, query: &str, format: &str) -> Result<String
/// ```
#[pg_extern]
fn ruvector_sparql_json(store_name: &str, query: &str) -> Result<JsonB, String> {
// Validate input to prevent panics that would abort PostgreSQL
if query.trim().is_empty() {
return Err("SPARQL query cannot be empty".to_string());
}

let result = ruvector_sparql(store_name, query, "json")?;

let json_value: JsonValue =
Expand Down
86 changes: 80 additions & 6 deletions crates/ruvector-postgres/src/index/hnsw_am.rs
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,21 @@

let header = page as *const PageHeaderData;
let data_ptr = (header as *const u8).add(size_of::<PageHeaderData>());

// Bounds check: prevent reading past page boundary. Fixes #164 segfault.
let page_size = pg_sys::BLCKSZ as usize;
let total_read_end = size_of::<PageHeaderData>()
+ size_of::<HnswNodePageHeader>()
+ dimensions * size_of::<f32>();
if total_read_end > page_size {

Check warning on line 514 in crates/ruvector-postgres/src/index/hnsw_am.rs

View workflow job for this annotation

GitHub Actions / Lint & Format

Diff in /home/runner/work/ruvector/ruvector/crates/ruvector-postgres/src/index/hnsw_am.rs
pgrx::warning!(
"HNSW: Vector read would exceed page boundary ({} > {}), skipping block {}",
total_read_end, page_size, block
);
pg_sys::UnlockReleaseBuffer(buffer);
return None;
}

let vector_ptr = data_ptr.add(size_of::<HnswNodePageHeader>()) as *const f32;

let mut vector = Vec::with_capacity(dimensions);
Expand Down Expand Up @@ -550,6 +565,23 @@
offset += count * size_of::<HnswNeighbor>();
}

// Bounds check: prevent reading past page boundary. Fixes #164 segfault.
let page_size = pg_sys::BLCKSZ as usize;
let header_size = size_of::<PageHeaderData>();
let total_read_end = header_size
+ size_of::<HnswNodePageHeader>()
+ vector_size
+ offset
+ neighbor_count * size_of::<HnswNeighbor>();
if total_read_end > page_size {

Check warning on line 576 in crates/ruvector-postgres/src/index/hnsw_am.rs

View workflow job for this annotation

GitHub Actions / Lint & Format

Diff in /home/runner/work/ruvector/ruvector/crates/ruvector-postgres/src/index/hnsw_am.rs
pgrx::warning!(
"HNSW: Neighbor read would exceed page boundary ({} > {}), skipping block {}",
total_read_end, page_size, block
);
pg_sys::UnlockReleaseBuffer(buffer);
return Vec::new();
}

let neighbors_ptr = neighbors_base.add(offset) as *const HnswNeighbor;
let mut neighbors = Vec::with_capacity(neighbor_count);
for i in 0..neighbor_count {
Expand Down Expand Up @@ -712,16 +744,16 @@
}
}

// Convert to sorted result vector
// Convert to sorted result vector.
// Use into_sorted_vec() for deterministic ordering instead of into_iter()
// which yields arbitrary order from BinaryHeap. Fixes #171.
let mut result_vec: Vec<_> = results
.into_sorted_vec()
.into_iter()
.take(k)
.map(|r| (r.block, r.tid, r.distance))
.collect();

result_vec.sort_by(|a, b| a.2.partial_cmp(&b.2).unwrap_or(Ordering::Equal));
result_vec.truncate(k);

result_vec
}

Expand All @@ -738,8 +770,32 @@
) -> *mut IndexBuildResult {
pgrx::log!("HNSW v2: Starting index build");

// Get dimensions from first tuple or index definition
let dimensions = 128; // TODO: Extract from index column definition
// Extract dimensions from the indexed column's type modifier (atttypmod).
// For ruvector(384), atttypmod == 384. Fixes #171 and #164.
let dimensions = {
let tupdesc = (*heap).rd_att;
let natts = (*index_info).ii_NumIndexAttrs as isize;
let mut dims: u32 = 0;
if natts > 0 && !tupdesc.is_null() {
let attnum = *(*index_info).ii_IndexAttrNumbers.offset(0);
if attnum > 0 && (attnum as isize) <= (*tupdesc).natts as isize {
let attr = (*tupdesc).attrs.as_ptr().offset((attnum - 1) as isize);
let typmod = (*attr).atttypmod;
if typmod > 0 {
dims = typmod as u32;
}
}
}
if dims == 0 {
pgrx::warning!(
"HNSW: Could not determine vector dimensions from column type modifier, \
defaulting to 384. Ensure column is defined as ruvector(N)."
);
dims = 384;
}
pgrx::log!("HNSW v2: Building index with {} dimensions", dims);
dims as usize
};
let config = HnswConfig::default();

// Parse options from WITH clause
Expand Down Expand Up @@ -1399,6 +1455,14 @@
state.search_done = false;
state.query_valid = false; // Reset validity flag

// Non-kNN scan (e.g., COUNT(*), WHERE embedding IS NOT NULL)
// When there are no ORDER BY operators, we cannot perform a vector search.
// Return early and let hnsw_gettuple return false, forcing PostgreSQL to
// fall back to a sequential scan. Fixes #152.
if norderbys <= 0 || orderbys.is_null() {
return;
}

// Extract query vector from ORDER BY
if norderbys > 0 && !orderbys.is_null() {
let orderby = &*orderbys;
Expand Down Expand Up @@ -1483,6 +1547,9 @@
}

// Validate query vector - CRITICAL: Prevent crashes from invalid queries
// Note: if query_valid is false due to norderbys==0 (non-kNN scan),
// we already returned early above. This check only fires for kNN scans
// where vector extraction genuinely failed.
if !state.query_valid || state.query_vector.is_empty() {
// Instead of using zeros which crash, raise a proper error
pgrx::error!(
Expand Down Expand Up @@ -1577,6 +1644,13 @@
let state = &mut *((*scan).opaque as *mut HnswScanState);
let index = (*scan).indexRelation;

// Non-kNN scan: no query vector was provided (e.g., COUNT(*), WHERE IS NOT NULL).
// Return false to tell PostgreSQL this index cannot satisfy this scan type,
// forcing fallback to sequential scan. Fixes #152.
if !state.query_valid && !state.search_done {
return false;
}

// Execute search on first call
if !state.search_done {
let (meta_page, meta_buffer) = get_meta_page(index);
Expand Down
Loading
Loading