1use entdb::error::{EntDbError, Result};
18use entdb::types::value::{format_vector_text, parse_vector_text};
19use entdb::types::{DataType, Value};
20use pgwire::api::results::{DataRowEncoder, FieldFormat};
21use pgwire::api::Type;
22use pgwire::error::PgWireResult;
23
24pub fn ent_to_pg_type(dt: &DataType) -> Type {
25 match dt {
26 DataType::Boolean => Type::BOOL,
27 DataType::Int16 => Type::INT2,
28 DataType::Int32 => Type::INT4,
29 DataType::Int64 => Type::INT8,
30 DataType::Float32 => Type::FLOAT4,
31 DataType::Float64 => Type::FLOAT8,
32 DataType::Text => Type::TEXT,
33 DataType::Varchar(_) => Type::VARCHAR,
34 DataType::Timestamp => Type::TIMESTAMP,
35 DataType::Vector(_) => Type::TEXT,
36 DataType::Bm25Query => Type::TEXT,
37 DataType::Null => Type::UNKNOWN,
38 }
39}
40
41pub fn value_to_text(val: &Value) -> Option<String> {
42 match val {
43 Value::Null => None,
44 Value::Boolean(v) => Some(if *v { "t".to_string() } else { "f".to_string() }),
45 Value::Int16(v) => Some(v.to_string()),
46 Value::Int32(v) => Some(v.to_string()),
47 Value::Int64(v) => Some(v.to_string()),
48 Value::Float32(v) => Some(v.to_string()),
49 Value::Float64(v) => Some(v.to_string()),
50 Value::Text(v) => Some(v.clone()),
51 Value::Timestamp(v) => Some(v.to_string()),
52 Value::Vector(v) => Some(format_vector_text(v)),
53 Value::Bm25Query { terms, index_name } => Some(format!(
54 "bm25query(index={},terms={})",
55 index_name,
56 terms.join(" ")
57 )),
58 }
59}
60
61pub fn value_to_binary(val: &Value) -> Option<Vec<u8>> {
62 match val {
63 Value::Null => None,
64 Value::Boolean(v) => Some(vec![u8::from(*v)]),
65 Value::Int16(v) => Some(v.to_be_bytes().to_vec()),
66 Value::Int32(v) => Some(v.to_be_bytes().to_vec()),
67 Value::Int64(v) => Some(v.to_be_bytes().to_vec()),
68 Value::Float32(v) => Some(v.to_be_bytes().to_vec()),
69 Value::Float64(v) => Some(v.to_be_bytes().to_vec()),
70 Value::Text(v) => Some(v.as_bytes().to_vec()),
71 Value::Timestamp(v) => Some(v.to_be_bytes().to_vec()),
72 Value::Vector(v) => Some(format_vector_text(v).into_bytes()),
73 Value::Bm25Query { terms, index_name } => {
74 Some(format!("bm25query(index={},terms={})", index_name, terms.join(" ")).into_bytes())
75 }
76 }
77}
78
79pub fn text_to_value(text: &str, dt: &DataType) -> Result<Value> {
80 match dt {
81 DataType::Boolean => text
82 .parse::<bool>()
83 .map(Value::Boolean)
84 .map_err(|e| EntDbError::Query(format!("invalid BOOL literal: {e}"))),
85 DataType::Int16 => text
86 .parse::<i16>()
87 .map(Value::Int16)
88 .map_err(|e| EntDbError::Query(format!("invalid INT2 literal: {e}"))),
89 DataType::Int32 => text
90 .parse::<i32>()
91 .map(Value::Int32)
92 .map_err(|e| EntDbError::Query(format!("invalid INT4 literal: {e}"))),
93 DataType::Int64 => text
94 .parse::<i64>()
95 .map(Value::Int64)
96 .map_err(|e| EntDbError::Query(format!("invalid INT8 literal: {e}"))),
97 DataType::Float32 => text
98 .parse::<f32>()
99 .map(Value::Float32)
100 .map_err(|e| EntDbError::Query(format!("invalid FLOAT4 literal: {e}"))),
101 DataType::Float64 => text
102 .parse::<f64>()
103 .map(Value::Float64)
104 .map_err(|e| EntDbError::Query(format!("invalid FLOAT8 literal: {e}"))),
105 DataType::Text | DataType::Varchar(_) => Ok(Value::Text(text.to_string())),
106 DataType::Timestamp => text
107 .parse::<i64>()
108 .map(Value::Timestamp)
109 .map_err(|e| EntDbError::Query(format!("invalid TIMESTAMP literal: {e}"))),
110 DataType::Vector(dim) => {
111 let parsed = parse_vector_text(text)
112 .map_err(|e| EntDbError::Query(format!("invalid VECTOR literal: {e}")))?;
113 if parsed.len() != *dim as usize {
114 return Err(EntDbError::Query(format!(
115 "invalid VECTOR literal: expected dimension {}, got {}",
116 dim,
117 parsed.len()
118 )));
119 }
120 Ok(Value::Vector(parsed))
121 }
122 DataType::Null => Ok(Value::Null),
123 DataType::Bm25Query => Err(EntDbError::Query(
124 "BM25 query values are not supported as direct client literals".to_string(),
125 )),
126 }
127}
128
129pub fn encode_value(
130 encoder: &mut DataRowEncoder,
131 value: &Value,
132 data_type: &Type,
133 format: FieldFormat,
134) -> PgWireResult<()> {
135 match value {
136 Value::Null => encoder.encode_field_with_type_and_format(&None::<i32>, data_type, format),
137 Value::Boolean(v) => {
138 encoder.encode_field_with_type_and_format(&Some(*v), data_type, format)
139 }
140 Value::Int16(v) => encoder.encode_field_with_type_and_format(&Some(*v), data_type, format),
141 Value::Int32(v) => encoder.encode_field_with_type_and_format(&Some(*v), data_type, format),
142 Value::Int64(v) => encoder.encode_field_with_type_and_format(&Some(*v), data_type, format),
143 Value::Float32(v) => {
144 encoder.encode_field_with_type_and_format(&Some(*v), data_type, format)
145 }
146 Value::Float64(v) => {
147 encoder.encode_field_with_type_and_format(&Some(*v), data_type, format)
148 }
149 Value::Text(v) => {
150 encoder.encode_field_with_type_and_format(&Some(v.as_str()), data_type, format)
151 }
152 Value::Timestamp(v) => {
153 let text = v.to_string();
154 encoder.encode_field_with_type_and_format(&Some(text), data_type, format)
155 }
156 Value::Vector(v) => {
157 let text = format_vector_text(v);
158 encoder.encode_field_with_type_and_format(&Some(text), data_type, format)
159 }
160 Value::Bm25Query { terms, index_name } => {
161 let text = format!("bm25query(index={},terms={})", index_name, terms.join(" "));
162 encoder.encode_field_with_type_and_format(&Some(text), data_type, format)
163 }
164 }
165}
166
167#[cfg(test)]
168mod tests {
169 use super::{ent_to_pg_type, text_to_value, value_to_binary, value_to_text};
170 use entdb::types::{DataType, Value};
171 use pgwire::api::Type;
172
173 #[test]
174 fn data_type_mapping_covers_supported_set() {
175 assert_eq!(ent_to_pg_type(&DataType::Boolean), Type::BOOL);
176 assert_eq!(ent_to_pg_type(&DataType::Int16), Type::INT2);
177 assert_eq!(ent_to_pg_type(&DataType::Int32), Type::INT4);
178 assert_eq!(ent_to_pg_type(&DataType::Int64), Type::INT8);
179 assert_eq!(ent_to_pg_type(&DataType::Float32), Type::FLOAT4);
180 assert_eq!(ent_to_pg_type(&DataType::Float64), Type::FLOAT8);
181 assert_eq!(ent_to_pg_type(&DataType::Text), Type::TEXT);
182 assert_eq!(ent_to_pg_type(&DataType::Varchar(64)), Type::VARCHAR);
183 assert_eq!(ent_to_pg_type(&DataType::Timestamp), Type::TIMESTAMP);
184 assert_eq!(ent_to_pg_type(&DataType::Vector(3)), Type::TEXT);
185 }
186
187 #[test]
188 fn text_value_round_trip_for_core_types() {
189 let cases = vec![
190 ("true", DataType::Boolean, Value::Boolean(true)),
191 ("12", DataType::Int16, Value::Int16(12)),
192 ("123", DataType::Int32, Value::Int32(123)),
193 ("1234", DataType::Int64, Value::Int64(1234)),
194 ("1.5", DataType::Float64, Value::Float64(1.5)),
195 ("ent", DataType::Text, Value::Text("ent".to_string())),
196 ("99", DataType::Timestamp, Value::Timestamp(99)),
197 (
198 "[0.1,0.2,0.3]",
199 DataType::Vector(3),
200 Value::Vector(vec![0.1, 0.2, 0.3]),
201 ),
202 ];
203
204 for (input, dt, expected) in cases {
205 let parsed = text_to_value(input, &dt).expect("parse value");
206 assert_eq!(parsed, expected);
207 assert_eq!(value_to_text(&parsed), value_to_text(&expected));
208 }
209 }
210
211 #[test]
212 fn binary_encoding_returns_none_for_null() {
213 assert!(value_to_binary(&Value::Null).is_none());
214 assert_eq!(value_to_binary(&Value::Int32(7)).expect("bytes").len(), 4);
215 }
216}