1use super::{
10 array_scalar_type, column_type_name, validate_vector_dimensions, ColumnType, SQLError,
11};
12use crate::expr::{
13 cast_value_from_with_control, parse_pg_array_literal_with_control,
14 value_to_tensor_with_control, value_to_text_with_control, value_to_vector_with_control,
15};
16use uqa_core::{
17 memory::{MemoryReservation, Produced, ProductionControl, ProductionString, ProductionVec},
18 ArrayValue, DecimalValue, Value,
19};
20
21type Result<T> = std::result::Result<T, SQLError>;
22
23#[expect(
24 clippy::too_many_lines,
25 reason = "assignment matrix preserves conversion and validation order"
26)]
27pub fn convert_value_to_column_type_with_control(
28 value: Produced<Value>,
29 ty: &ColumnType,
30 control: &ProductionControl<'_>,
31) -> Result<Produced<Value>> {
32 let (value, memory) = value.into_parts();
33 let value = control.finish(value, memory)?;
34 if matches!(&*value, Value::Null) {
35 return Ok(value);
36 }
37 match ty {
38 ColumnType::Named(name) => Err(SQLError::Routine {
39 sqlstate: "42704".into(),
40 message: format!("type \"{name}\" does not exist"),
41 }),
42 ColumnType::Enum(reference) => match &*value {
44 Value::Enum(label) if label.type_oid() == reference.oid => Ok(value),
45 _ => Err(SQLError::Internal(format!(
46 "enum input for type OID {} requires catalog-aware conversion",
47 reference.oid
48 ))),
49 },
50 ColumnType::Composite(reference) => match &*value {
52 Value::Record(_) => Ok(value),
53 _ => Err(SQLError::Internal(format!(
54 "composite input for type OID {} requires catalog-aware conversion",
55 reference.oid
56 ))),
57 },
58 ColumnType::SmallInteger => cast_value_from_with_control(&value, "smallint", None, control),
59 ColumnType::Integer => cast_value_from_with_control(&value, "integer", None, control),
60 ColumnType::BigInteger => cast_value_from_with_control(&value, "bigint", None, control),
61 ColumnType::Oid => cast_value_from_with_control(&value, "oid", None, control),
63 ColumnType::Xid => {
64 let converted = cast_value_from_with_control(&value, "bigint", None, control)?;
65 let Value::Int(number) = &*converted else {
66 unreachable!("bigint cast returned a non-integer value");
67 };
68 u32::try_from(*number).map_err(|_| {
69 SQLError::TypeMismatch(format!(
70 "value {number} is out of range for type {}",
71 column_type_name(ty)
72 ))
73 })?;
74 Ok(converted)
75 }
76 ColumnType::Boolean => match &*value {
77 Value::Bool(_) => Ok(value),
78 Value::Str(text) => {
79 let boolean = parse_boolean_text(text)
80 .ok_or_else(|| crate::expr::invalid_boolean_input(text))?;
81 Ok(control.finish(Value::Bool(boolean), control.empty_reservation())?)
82 }
83 other => Err(SQLError::TypeMismatch(format!(
84 "cannot cast {other:?} to boolean"
85 ))),
86 },
87 ColumnType::Void => Ok(control.finish(Value::Void, control.empty_reservation())?),
88 ColumnType::Text | ColumnType::RefCursor | ColumnType::Varchar(None) => {
89 text_value(value_to_text_with_control(&value, control)?, false, control)
90 }
91 ColumnType::Name => cast_value_from_with_control(&value, "name", None, control),
92 ColumnType::Uuid => cast_value_from_with_control(&value, "uuid", None, control),
93 ColumnType::Varchar(Some(length)) => character_value(&value, *length, false, control),
94 ColumnType::Bpchar => {
95 text_value(value_to_text_with_control(&value, control)?, true, control)
96 }
97 ColumnType::Character(length) => character_value(&value, *length, true, control),
98 ColumnType::Real => cast_value_from_with_control(&value, "real", None, control),
99 ColumnType::DoublePrecision => {
100 cast_value_from_with_control(&value, "double precision", None, control)
101 }
102 ColumnType::Numeric { precision, scale } => {
103 numeric_value(value, *precision, *scale, control)
104 }
105 ColumnType::Json => cast_value_from_with_control(&value, "json", None, control),
106 ColumnType::JsonB => cast_value_from_with_control(&value, "jsonb", None, control),
107 ColumnType::Bytea => match &*value {
109 Value::Bytes(_) => Ok(value),
110 Value::Str(_) | Value::FixedChar(_) => {
111 cast_value_from_with_control(&value, "bytea", Some("text"), control)
112 }
113 other => Err(SQLError::TypeMismatch(format!(
114 "cannot cast {other:?} to bytea"
115 ))),
116 },
117 ColumnType::InternalChar => {
118 let text = value_to_text_with_control(&value, control)?;
119 if text.len() != 1 {
120 return Err(SQLError::TypeMismatch(format!(
121 "value `{}` must be exactly one byte for type \"char\"",
122 text.as_str()
123 )));
124 }
125 text_value(text, false, control)
126 }
127 ColumnType::Regproc
128 | ColumnType::Regprocedure
129 | ColumnType::Regclass
130 | ColumnType::Regcollation
131 | ColumnType::Regnamespace
132 | ColumnType::Regtype
133 | ColumnType::PgNodeTree
134 | ColumnType::AclItem => {
135 if matches!(&*value, Value::Int(_) | Value::Str(_)) {
136 Ok(value)
137 } else {
138 text_value(value_to_text_with_control(&value, control)?, false, control)
139 }
140 }
141 ColumnType::Regrole => match &*value {
142 Value::Int(number) => {
143 u32::try_from(*number).map_err(|_| {
144 SQLError::TypeMismatch(format!(
145 "value {number} is out of range for type regrole"
146 ))
147 })?;
148 Ok(value)
149 }
150 Value::Str(_) | Value::FixedChar(_) => Err(SQLError::Internal(
151 "regrole name conversion requires catalog resolution".into(),
152 )),
153 other => Err(SQLError::TypeMismatch(format!(
154 "cannot cast {other:?} to regrole"
155 ))),
156 },
157 ColumnType::Int2Vector | ColumnType::OidVector => {
158 let name = column_type_name(ty);
159 if matches!(&*value, Value::LegacyVector(vector) if vector.kind().type_name() == name) {
160 Ok(value)
161 } else {
162 cast_value_from_with_control(&value, name, None, control)
163 }
164 }
165 ColumnType::AnyArray => {
166 if value.array_view().is_some() {
167 Ok(value)
168 } else {
169 Err(SQLError::TypeMismatch(format!(
170 "cannot cast {:?} to anyarray",
171 *value
172 )))
173 }
174 }
175 ColumnType::Record => match &*value {
176 Value::Record(_) => Ok(value),
177 Value::Row(_) => record_value(value, control),
178 other => Err(SQLError::TypeMismatch(format!(
179 "cannot cast {other:?} to record"
180 ))),
181 },
182 ColumnType::Array(element) => array_value(value, element, control),
183 ColumnType::Date
184 | ColumnType::Time
185 | ColumnType::TimePrecision(_)
186 | ColumnType::TimeTz
187 | ColumnType::TimeTzPrecision(_)
188 | ColumnType::Timestamp
189 | ColumnType::TimestampPrecision(_)
190 | ColumnType::TimestampTz
191 | ColumnType::TimestampTzPrecision(_)
192 | ColumnType::Interval
193 | ColumnType::IntervalWithFields { .. } => {
194 let name = ty.sql_name_with_control(control)?;
195 cast_value_from_with_control(&value, &name, None, control)
196 }
197 ColumnType::Range(subtype) => {
198 cast_value_from_with_control(&value, subtype.range_name(), None, control)
199 }
200 ColumnType::Multirange(subtype) => {
201 cast_value_from_with_control(&value, subtype.multirange_name(), None, control)
202 }
203 ColumnType::Vector(dimensions) => {
204 let vector = value_to_vector_with_control(&value, control)?;
205 validate_vector_dimensions(*dimensions, vector.len())?;
206 vector_value(&vector, control)
207 }
208 ColumnType::Tensor(dimensions) => {
209 let tensor = value_to_tensor_with_control(&value, control)?;
210 for vector in &*tensor {
211 control.check()?;
212 validate_vector_dimensions(*dimensions, vector.len())?;
213 }
214 let mut output = ProductionVec::new(*control);
215 output.reserve(tensor.len())?;
216 for vector in &*tensor {
217 output.push_produced(vector_value(vector, control)?)?;
218 }
219 let (values, memory) = output.finish()?.into_parts();
220 Ok(control.finish(Value::List(values), memory)?)
221 }
222 ColumnType::Domain { base, .. } => {
223 convert_value_to_column_type_with_control(value, base, control)
224 }
225 }
226}
227
228fn numeric_value(
229 value: Produced<Value>,
230 precision: Option<u32>,
231 scale: Option<i32>,
232 control: &ProductionControl<'_>,
233) -> Result<Produced<Value>> {
234 let decimal = match &*value {
235 Value::Decimal(_) => {
236 let (Value::Decimal(value), memory) = value.into_parts() else {
237 unreachable!();
238 };
239 control.finish(value, memory)?
240 }
241 Value::Int(number) => DecimalValue::from_i64_with_control(*number, control)?,
242 Value::Bool(boolean) => DecimalValue::from_i64_with_control(i64::from(*boolean), control)?,
243 Value::Float(number) => DecimalValue::from_f64_lossy_with_control(*number, control)?
244 .ok_or_else(|| SQLError::TypeMismatch(format!("cannot cast {number:?} to numeric")))?,
245 Value::Str(text) => DecimalValue::parse_with_control(text, control)?
246 .ok_or_else(|| crate::expr::invalid_numeric_input(text))?,
247 other => {
248 return Err(SQLError::TypeMismatch(format!(
249 "cannot cast {other:?} to numeric"
250 )))
251 }
252 };
253 let rounded = match scale {
254 Some(scale) => decimal
255 .round_to_scale_with_control(scale, control)?
256 .ok_or_else(|| {
257 SQLError::TypeMismatch(format!("cannot round numeric to scale {scale}"))
258 })?,
259 None => decimal,
260 };
261 if let Some(precision) = precision {
262 let scale = scale.unwrap_or(0);
263 if !rounded.fits_precision_with_control(precision, scale, control)? {
264 return Err(numeric_field_overflow(precision, scale));
265 }
266 }
267 let (decimal, memory) = rounded.into_parts();
268 Ok(control.finish(Value::Decimal(decimal), memory)?)
269}
270
271fn text_value(
272 text: Produced<String>,
273 fixed: bool,
274 control: &ProductionControl<'_>,
275) -> Result<Produced<Value>> {
276 let (text, memory) = text.into_parts();
277 Ok(control.finish(
278 if fixed {
279 Value::FixedChar(text)
280 } else {
281 Value::Str(text)
282 },
283 memory,
284 )?)
285}
286
287fn character_value(
288 value: &Value,
289 length: u32,
290 fixed: bool,
291 control: &ProductionControl<'_>,
292) -> Result<Produced<Value>> {
293 let name = if fixed {
294 "character"
295 } else {
296 "character varying"
297 };
298 let length = usize::try_from(length).map_err(|_| {
299 SQLError::TypeMismatch(format!(
300 "{name} length {length} exceeds the platform addressable range"
301 ))
302 })?;
303 let text = value_to_text_with_control(value, control)?;
304 let mut count = 0;
305 let mut end = 0;
306 for (index, character) in text.char_indices() {
307 control.check()?;
308 if count < length {
309 count += 1;
310 end = index + character.len_utf8();
311 } else if character != ' ' {
312 return Err(SQLError::Routine {
313 sqlstate: "22001".into(),
314 message: format!("value too long for type {name}({length})"),
315 });
316 }
317 }
318 let mut output = ProductionString::from_produced(text, *control)?;
319 output.truncate(end)?;
320 if fixed {
321 for _ in count..length {
322 output.push(' ')?;
323 }
324 }
325 text_value(output.finish()?, fixed, control)
326}
327
328fn array_value(
329 value: Produced<Value>,
330 element: &ColumnType,
331 control: &ProductionControl<'_>,
332) -> Result<Produced<Value>> {
333 let array = match &*value {
334 Value::Array(_) => {
335 let (Value::Array(array), memory) = value.into_parts() else {
336 unreachable!();
337 };
338 control.finish(array, memory)?
339 }
340 Value::LegacyVector(_) => {
341 let (Value::LegacyVector(vector), memory) = value.into_parts() else {
342 unreachable!();
343 };
344 control.finish(vector.into_array(), memory)?
345 }
346 Value::List(_) => {
347 let (Value::List(elements), memory) = value.into_parts() else {
348 unreachable!();
349 };
350 ArrayValue::try_new_with_control(control.finish(elements, memory)?, control)?
351 .ok_or_else(array_shape_error)?
352 }
353 Value::Str(text) => parse_pg_array_literal_with_control(text, control)?,
354 other => {
355 return Err(SQLError::TypeMismatch(format!(
356 "cannot cast {other:?} to {}[]",
357 column_type_name(element)
358 )))
359 }
360 };
361 let converted = array_elements(array.elements(), array_scalar_type(element), control)?;
362 let mut bounds = ProductionVec::new(*control);
363 bounds.reserve(array.lower_bounds().len())?;
364 for bound in array.lower_bounds() {
365 bounds.push_copy(*bound)?;
366 }
367 let array = ArrayValue::with_lower_bounds_with_control(converted, bounds.finish()?, control)?
368 .ok_or_else(array_shape_error)?;
369 let (array, memory) = array.into_parts();
370 let oid = crate::catalog::type_metadata::pg_type_oid(array_scalar_type(element)) as u32;
371 Ok(control.finish(Value::Array(array.with_element_type_oid(Some(oid))), memory)?)
372}
373
374fn array_elements(
375 values: &[Value],
376 element: &ColumnType,
377 control: &ProductionControl<'_>,
378) -> Result<Produced<Vec<Value>>> {
379 let mut output = ProductionVec::new(*control);
380 output.reserve(values.len())?;
381 for value in values {
382 let value = match value {
383 Value::List(values) => {
384 let (values, memory) = array_elements(values, element, control)?.into_parts();
385 control.finish(Value::List(values), memory)?
386 }
387 scalar => convert_value_to_column_type_with_control(
388 control.copy_value(scalar)?,
389 element,
390 control,
391 )?,
392 };
393 output.push_produced(value)?;
394 }
395 Ok(output.finish()?)
396}
397
398fn array_shape_error() -> SQLError {
399 SQLError::TypeMismatch("multidimensional arrays must have matching dimensions".into())
400}
401
402fn record_value(
403 value: Produced<Value>,
404 control: &ProductionControl<'_>,
405) -> Result<Produced<Value>> {
406 let Value::Row(source) = &*value else {
407 unreachable!();
408 };
409 if source.field_types().is_some() {
411 return Ok(value);
412 }
413 let mut records = ProductionVec::new(*control);
414 records.reserve(source.len())?;
415 for index in 0..source.len() {
416 let (name, memory) = control.format(format_args!("f{}", index + 1))?.into_parts();
417 records.push_produced(control.finish((name, Value::Null), memory)?)?;
418 }
419 let (records, records_memory) = records.finish()?.into_parts();
420 let (Value::Row(source), source_memory) = value.into_parts() else {
421 unreachable!();
422 };
423 let old_buffer_bytes =
424 source.capacity() * size_of::<Value>() + uqa_core::RowValue::retained_header_bytes();
425 let mut parts = RecordParts {
426 source: source.into_values(),
427 records,
428 memory: control.combine(source_memory, records_memory),
429 };
430 for ((_, destination), source) in parts.records.iter_mut().zip(parts.source) {
431 control.check()?;
432 *destination = source;
433 }
434 if let Some(memory) = &mut parts.memory {
435 drop(memory.split(old_buffer_bytes));
436 }
437 let (record, memory) = uqa_core::RecordValue::with_control(
438 control.finish(parts.records, parts.memory)?,
439 None,
440 control,
441 )?
442 .into_parts();
443 Ok(control.finish(Value::Record(record), memory)?)
444}
445
446struct RecordParts {
447 source: Vec<Value>,
448 records: Vec<(String, Value)>,
449 memory: Option<MemoryReservation>,
450}
451
452fn vector_value(vector: &[f32], control: &ProductionControl<'_>) -> Result<Produced<Value>> {
453 let mut output = ProductionVec::new(*control);
454 output.reserve(vector.len())?;
455 for value in vector {
456 output.push_produced(
457 control.finish(Value::Float(f64::from(*value)), control.empty_reservation())?,
458 )?;
459 }
460 let (values, memory) = output.finish()?.into_parts();
461 Ok(control.finish(Value::List(values), memory)?)
462}
463
464fn parse_boolean_text(text: &str) -> Option<bool> {
465 let text = text.trim();
466 if ["true", "t", "yes", "y", "on", "1"]
467 .iter()
468 .any(|candidate| text.eq_ignore_ascii_case(candidate))
469 {
470 Some(true)
471 } else if ["false", "f", "no", "n", "off", "0"]
472 .iter()
473 .any(|candidate| text.eq_ignore_ascii_case(candidate))
474 {
475 Some(false)
476 } else {
477 None
478 }
479}
480
481#[cfg(test)]
482mod tests;
483
484#[must_use]
486pub fn numeric_field_overflow(precision: u32, scale: i32) -> SQLError {
487 let maxdigits = i64::from(precision) - i64::from(scale);
488 SQLError::Diagnostic {
489 sqlstate: "22003".into(),
490 message: "numeric field overflow".into(),
491 detail: Some(format!(
492 "A field with precision {precision}, scale {scale} must round to an absolute value less than {}.",
493 if maxdigits == 0 {
494 "1".to_string()
495 } else {
496 format!("10^{maxdigits}")
497 }
498 )),
499 hint: None,
500 }
501}