1use nodedb_types::columnar::ColumnType;
24use rust_decimal::prelude::ToPrimitive;
25
26use crate::error::{Result, SqlError};
27use crate::types::{ColumnInfo, SqlDataType, SqlValue};
28
29const VECTOR_TYPE_NAME: &str = "VECTOR";
31
32pub(crate) fn declared_vector_dim(column: &ColumnInfo) -> Option<usize> {
39 if let SqlDataType::Vector(dim) = column.data_type {
40 return Some(dim);
41 }
42 match column
43 .raw_type
44 .as_deref()
45 .and_then(ColumnType::from_declared_type)
46 {
47 Some(ColumnType::Vector(dim)) => Some(dim as usize),
48 _ => None,
49 }
50}
51
52pub(crate) fn coerce_to_vector(column: &str, value: SqlValue, dim: usize) -> Result<SqlValue> {
57 let SqlValue::Array(elements) = value else {
58 return Ok(value);
59 };
60 elements
61 .into_iter()
62 .map(|element| vector_element(column, element, dim).map(SqlValue::Float))
63 .collect::<Result<Vec<_>>>()
64 .map(SqlValue::Array)
65}
66
67fn vector_element(column: &str, element: SqlValue, dim: usize) -> Result<f64> {
69 let number = match element {
70 SqlValue::Int(i) => i as f64,
71 SqlValue::Float(f) => f,
72 SqlValue::Decimal(d) => d.to_f64().ok_or_else(|| SqlError::TypeMismatch {
73 detail: format!("column '{column}': '{d}' is not representable as VECTOR({dim})"),
74 })?,
75 other => {
76 return Err(SqlError::VectorElementNotNumeric {
77 column: column.to_string(),
78 dim,
79 element: element_description(&other),
80 });
81 }
82 };
83 if !number.is_finite() || (number as f32).is_infinite() {
84 return Err(SqlError::FloatOutOfRange {
85 column: column.to_string(),
86 value: number,
87 declared_type: VECTOR_TYPE_NAME,
88 });
89 }
90 Ok(number)
91}
92
93fn element_description(element: &SqlValue) -> String {
95 match element {
96 SqlValue::String(s) => format!("text '{s}'"),
97 SqlValue::Null => "null".to_string(),
98 SqlValue::Bool(_) => "a boolean".to_string(),
99 SqlValue::Bytes(_) => "bytes".to_string(),
100 SqlValue::Array(_) => "a nested array".to_string(),
101 SqlValue::Timestamp(_) => "a timestamp".to_string(),
102 SqlValue::Timestamptz(_) => "a timestamptz".to_string(),
103 SqlValue::Int(_) => "an integer".to_string(),
104 SqlValue::Float(_) => "a float".to_string(),
105 SqlValue::Decimal(_) => "a decimal".to_string(),
106 }
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112
113 fn decimal(text: &str) -> SqlValue {
114 SqlValue::Decimal(text.parse().expect("decimal literal"))
115 }
116
117 fn column(data_type: SqlDataType, raw_type: Option<&str>) -> ColumnInfo {
118 ColumnInfo {
119 name: "embedding".to_string(),
120 data_type,
121 nullable: true,
122 is_primary_key: false,
123 default: None,
124 raw_type: raw_type.map(str::to_string),
125 int_width: None,
126 float_width: None,
127 }
128 }
129
130 #[test]
131 fn fractional_elements_become_floats() {
132 let value = SqlValue::Array(vec![decimal("0.1"), decimal("0.2"), decimal("0.3")]);
133 assert_eq!(
134 coerce_to_vector("embedding", value, 3).expect("coerces"),
135 SqlValue::Array(vec![
136 SqlValue::Float(0.1),
137 SqlValue::Float(0.2),
138 SqlValue::Float(0.3),
139 ])
140 );
141 }
142
143 #[test]
144 fn mixed_integer_and_fractional_elements_become_floats() {
145 let value = SqlValue::Array(vec![SqlValue::Int(1), decimal("0.5"), SqlValue::Float(2.0)]);
146 assert_eq!(
147 coerce_to_vector("embedding", value, 3).expect("coerces"),
148 SqlValue::Array(vec![
149 SqlValue::Float(1.0),
150 SqlValue::Float(0.5),
151 SqlValue::Float(2.0),
152 ])
153 );
154 }
155
156 #[test]
157 fn a_string_element_is_refused_naming_the_column() {
158 for text in ["abc", "0.1"] {
159 let value = SqlValue::Array(vec![decimal("0.1"), SqlValue::String(text.into())]);
160 let error = coerce_to_vector("embedding", value, 2).expect_err("refused");
161 assert_eq!(
162 error,
163 SqlError::VectorElementNotNumeric {
164 column: "embedding".into(),
165 dim: 2,
166 element: format!("text '{text}'"),
167 }
168 );
169 }
170 }
171
172 #[test]
173 fn null_bool_and_nested_elements_are_refused() {
174 for element in [
175 SqlValue::Null,
176 SqlValue::Bool(true),
177 SqlValue::Array(vec![SqlValue::Int(1)]),
178 ] {
179 let value = SqlValue::Array(vec![element]);
180 assert!(matches!(
181 coerce_to_vector("embedding", value, 1),
182 Err(SqlError::VectorElementNotNumeric { .. })
183 ));
184 }
185 }
186
187 #[test]
188 fn an_element_past_the_f32_range_is_refused() {
189 let value = SqlValue::Array(vec![SqlValue::Float(1e39)]);
190 assert!(matches!(
191 coerce_to_vector("embedding", value, 1),
192 Err(SqlError::FloatOutOfRange { .. })
193 ));
194 }
195
196 #[test]
197 fn a_non_array_value_passes_through() {
198 for value in [
199 SqlValue::Null,
200 SqlValue::String("[0.1,0.2]".into()),
201 SqlValue::Bytes(vec![0; 8]),
202 ] {
203 assert_eq!(
204 coerce_to_vector("embedding", value.clone(), 2).expect("passes"),
205 value
206 );
207 }
208 }
209
210 #[test]
211 fn the_dimension_comes_from_the_type_or_the_declared_text() {
212 assert_eq!(
213 declared_vector_dim(&column(SqlDataType::Vector(3), None)),
214 Some(3)
215 );
216 assert_eq!(
217 declared_vector_dim(&column(SqlDataType::String, Some("VECTOR(4) NOT NULL"))),
218 Some(4)
219 );
220 assert_eq!(
221 declared_vector_dim(&column(SqlDataType::String, Some("TEXT"))),
222 None
223 );
224 assert_eq!(
225 declared_vector_dim(&column(SqlDataType::String, None)),
226 None
227 );
228 }
229}