1mod array;
10mod binary_oid;
11mod legacy_vector;
12mod temporal;
13
14use super::{
15 multirange_from_ranges, out_of_range, parse_json, parse_multirange, parse_range, to_decimal,
16 to_f64, typed_json_value, value_to_json, value_to_string, vector_value_to_string, ArrayValue,
17 Result, SQLError, TemporalValue, Value,
18};
19use crate::ast::RangeSubtype;
20
21pub fn cast_value(v: &Value, ty: &str) -> Result<Value> {
25 cast_value_from(v, ty, None)
26}
27
28#[expect(
30 clippy::too_many_lines,
31 reason = "cast matrix preserves source-target and error precedence"
32)]
33pub fn cast_value_from(v: &Value, ty: &str, source_ty: Option<&str>) -> Result<Value> {
34 let normalized_type = ty.trim().to_ascii_lowercase();
35 if normalized_type
36 .strip_suffix("[]")
37 .is_some_and(|element| element.trim() == "void" || element.trim() == "pg_catalog.void")
38 {
39 return Err(SQLError::Routine {
40 sqlstate: "42704".into(),
41 message: "type \"void[]\" does not exist".into(),
42 });
43 }
44 if matches!(v, Value::Null) {
45 return Ok(Value::Null);
46 }
47 let (base, modifier) = split_type_modifier(ty);
48 let target = base
49 .trim()
50 .strip_prefix("pg_catalog.")
51 .unwrap_or(base.trim());
52 if matches!(v, Value::Void)
53 && !matches!(
54 target,
55 "void"
56 | "text"
57 | "name"
58 | "varchar"
59 | "character varying"
60 | "bpchar"
61 | "character"
62 | "char"
63 )
64 {
65 return Err(undefined_cast("void", postgres_type_display_name(target)));
66 }
67 if let Some(elem_ty) = ty.strip_suffix("[]") {
68 let source_elem_ty = source_ty
69 .and_then(|source| source.trim().strip_suffix("[]"))
70 .map(str::trim);
71 let array = match v {
72 Value::Array(array) => array.clone(),
73 Value::Str(s) => parse_pg_array_literal(s)?,
74 other => {
75 return Err(SQLError::TypeMismatch(format!(
76 "CAST AS {ty}: expected array, got {other:?}"
77 )));
78 }
79 };
80 let elements = cast_array_elements(array.elements(), elem_ty, source_elem_ty)?;
81 return ArrayValue::with_lower_bounds(elements, array.lower_bounds().to_vec())
82 .map(Value::Array)
83 .ok_or_else(|| SQLError::TypeMismatch("array dimensions changed during cast".into()));
84 }
85 match base.as_ref() {
86 "void" | "pg_catalog.void" => {
87 let source = canonical_cast_source(source_ty, v);
88 if matches!(
89 source.as_str(),
90 "unknown" | "text" | "name" | "varchar" | "bpchar" | "void"
91 ) {
92 Ok(Value::Void)
93 } else {
94 Err(undefined_cast(postgres_type_display_name(&source), "void"))
95 }
96 }
97 "smallint" | "int2" | "pg_catalog.int2" => cast_integer(v, "smallint"),
98 "integer" | "int" | "int4" | "serial" | "serial4" | "pg_catalog.int4" => {
99 cast_integer(v, "integer")
100 }
101 "bigint" | "int8" | "bigserial" | "serial8" | "pg_catalog.int8" => {
102 cast_integer(v, "bigint")
103 }
104 "real" | "float4" | "pg_catalog.float4" => {
105 super::floating::to_float(v, super::FloatWidth::Real).map(Value::Float)
106 }
107 "float8" | "double" | "double precision" | "pg_catalog.float8" => {
108 Ok(Value::Float(to_f64(v)?))
109 }
110 "numeric" | "decimal" => {
111 let value = to_decimal(v)?;
112 if let Some(modifier) = modifier {
113 let mut parts = modifier.split(',').map(str::trim);
114 let precision: u32 = parts
115 .next()
116 .and_then(|p| p.parse().ok())
117 .ok_or_else(|| SQLError::TypeMismatch("bad numeric precision".into()))?;
118 let scale: i32 = parts.next().and_then(|s| s.parse().ok()).unwrap_or(0);
119 let rounded = value
120 .round_to_scale(scale)
121 .ok_or_else(|| out_of_range("numeric"))?;
122 if !rounded.fits_precision(precision, scale) {
123 return Err(SQLError::Routine {
124 sqlstate: "22003".into(),
125 message: format!(
126 "numeric field overflow: A field with precision {precision}, scale {scale} cannot hold value {}",
127 value.to_sql_string()
128 ),
129 });
130 }
131 return Ok(Value::Decimal(rounded));
132 }
133 Ok(Value::Decimal(value))
134 }
135 "regproc" | "regprocedure" | "regrole" | "regtype" if matches!(v, Value::Int(_)) => {
136 Ok(v.clone())
137 }
138 "text"
139 | "refcursor"
140 | "pg_catalog.refcursor"
141 | "name"
142 | "regproc"
143 | "regprocedure"
144 | "regtype"
145 | "pg_node_tree"
146 | "aclitem" => {
147 let source = source_ty
148 .map(str::trim)
149 .map(|source| source.strip_prefix("pg_catalog.").unwrap_or(source));
150 let text = match (source, v) {
151 (Some("int2vector" | "oidvector"), _) => {
152 vector_value_to_string(v).unwrap_or_else(|| value_to_string(v))
153 }
154 (
155 Some(
156 "regproc" | "regprocedure" | "regclass" | "regnamespace" | "regrole"
157 | "regtype",
158 ),
159 Value::Int(0),
160 ) => "-".into(),
161 _ => cast_text(v, source_ty),
162 };
163 Ok(Value::Str(text))
164 }
165 "int2vector" | "pg_catalog.int2vector" => legacy_vector::cast_int2vector(v, source_ty),
166 "oidvector" | "pg_catalog.oidvector" => legacy_vector::cast_oidvector(v, source_ty),
167 "oid" | "pg_catalog.oid" => cast_oid(v, source_ty),
168 "regclass" | "pg_catalog.regclass" => cast_regclass(v, source_ty),
169 "regnamespace" | "pg_catalog.regnamespace" => cast_regnamespace(v, source_ty),
170 "regrole" | "pg_catalog.regrole" => cast_regrole(v, source_ty),
171 "xid" | "pg_catalog.xid" => cast_xid(v, source_ty),
172 "\"char\"" => {
173 let text = value_to_string(v);
174 let mut characters = text.chars();
175 let Some(character) = characters.next() else {
176 return Ok(Value::Str(String::new()));
177 };
178 if characters.next().is_some() || !character.is_ascii() {
179 return Err(SQLError::TypeMismatch(format!(
180 "value too long for type character(1): {text:?}"
181 )));
182 }
183 Ok(Value::Str(character.to_string()))
184 }
185 "uuid" => cast_uuid(v),
186 "varchar" | "character varying" => {
188 let text = cast_text(v, source_ty);
189 let Some(modifier) = modifier else {
190 return Ok(Value::Str(text));
191 };
192 let limit: usize = modifier
193 .trim()
194 .parse()
195 .map_err(|_| SQLError::TypeMismatch(format!("bad length modifier {modifier}")))?;
196 Ok(Value::Str(text.chars().take(limit).collect()))
197 }
198 "bpchar" if modifier.is_none() => Ok(Value::FixedChar(cast_text(v, source_ty))),
201 "character" | "char" | "bpchar" => {
202 let text = cast_text(v, source_ty);
203 let limit: usize = match modifier {
204 Some(modifier) => modifier.trim().parse().map_err(|_| {
205 SQLError::TypeMismatch(format!("bad length modifier {modifier}"))
206 })?,
207 None => 1,
208 };
209 if limit == 0 {
210 return Err(SQLError::TypeMismatch(
211 "CHARACTER length must be greater than zero".into(),
212 ));
213 }
214 let mut text = text.chars().take(limit).collect::<String>();
215 text.extend(std::iter::repeat_n(
216 ' ',
217 limit.saturating_sub(text.chars().count()),
218 ));
219 Ok(Value::FixedChar(text))
220 }
221 "date" => cast_date(v, source_ty),
222 "time" | "time without time zone" => cast_temporal(
223 v,
224 TemporalCastTarget::Time,
225 TemporalValue::parse_time,
226 "time",
227 modifier,
228 ),
229 "timetz" | "time with time zone" => cast_temporal(
230 v,
231 TemporalCastTarget::TimeTz,
232 TemporalValue::parse_time_tz,
233 "time with time zone",
234 modifier,
235 ),
236 "timestamp" | "datetime" | "timestamp without time zone" => cast_temporal(
237 v,
238 TemporalCastTarget::Timestamp,
239 TemporalValue::parse_timestamp,
240 "timestamp",
241 modifier,
242 ),
243 "timestamptz" | "timestamp with time zone" => cast_temporal(
244 v,
245 TemporalCastTarget::TimestampTz,
246 TemporalValue::parse_timestamp_tz,
247 "timestamp with time zone",
248 modifier,
249 ),
250 "interval" => temporal::cast_interval(v, ty),
251 name if name.starts_with("interval ") => temporal::cast_interval(v, ty),
252 "int4range" => cast_range(v, source_ty, RangeSubtype::Integer),
253 "int8range" => cast_range(v, source_ty, RangeSubtype::BigInteger),
254 "numrange" => cast_range(v, source_ty, RangeSubtype::Numeric),
255 "daterange" => cast_range(v, source_ty, RangeSubtype::Date),
256 "tsrange" => cast_range(v, source_ty, RangeSubtype::Timestamp),
257 "tstzrange" => cast_range(v, source_ty, RangeSubtype::TimestampTz),
258 "int4multirange" => cast_multirange(v, source_ty, RangeSubtype::Integer),
259 "int8multirange" => cast_multirange(v, source_ty, RangeSubtype::BigInteger),
260 "nummultirange" => cast_multirange(v, source_ty, RangeSubtype::Numeric),
261 "datemultirange" => cast_multirange(v, source_ty, RangeSubtype::Date),
262 "tsmultirange" => cast_multirange(v, source_ty, RangeSubtype::Timestamp),
263 "tstzmultirange" => cast_multirange(v, source_ty, RangeSubtype::TimestampTz),
264 "json" => {
265 if let Value::Json(text) = v {
266 return Ok(Value::Json(text.clone()));
267 }
268 if let Value::Str(text) | Value::FixedChar(text) = v {
269 let _validated = parse_json(text)?;
270 return Ok(Value::Json(text.clone()));
271 }
272 typed_json_value(&value_to_json(v), false)
273 }
274 "jsonb" => {
275 if let Value::JsonB(text) = v {
276 return Ok(Value::JsonB(text.clone()));
277 }
278 let parsed = match v {
279 Value::Json(text) | Value::Str(text) | Value::FixedChar(text) => parse_json(text)?,
280 other => value_to_json(other),
281 };
282 typed_json_value(&parsed, true)
283 }
284 "bytea" => cast_bytea(v, source_ty),
285 "boolean" | "bool" => cast_boolean(v),
286 other => Err(SQLError::Unsupported(format!("CAST AS {other}"))),
287 }
288}
289
290fn cast_range(v: &Value, source_ty: Option<&str>, subtype: RangeSubtype) -> Result<Value> {
291 let source = source_ty.map(canonical_type_name);
292 if source.as_deref().is_some_and(|source| {
293 source != subtype.range_name() && !matches!(source, "unknown" | "cstring")
294 }) {
295 return Err(undefined_cast(
296 source.as_deref().unwrap_or("unknown"),
297 subtype.range_name(),
298 ));
299 }
300 let (Value::Str(text) | Value::FixedChar(text)) = v else {
301 return Err(undefined_cast(
302 source.as_deref().unwrap_or("unknown"),
303 subtype.range_name(),
304 ));
305 };
306 parse_range(text, subtype).map(|range| Value::Str(range.to_text()))
307}
308
309fn cast_multirange(v: &Value, source_ty: Option<&str>, subtype: RangeSubtype) -> Result<Value> {
310 let source = source_ty.map(canonical_type_name);
311 let (Value::Str(text) | Value::FixedChar(text)) = v else {
312 return Err(undefined_cast(
313 source.as_deref().unwrap_or("unknown"),
314 subtype.multirange_name(),
315 ));
316 };
317 match source.as_deref() {
318 Some(source) if source == subtype.range_name() => {
319 let range = parse_range(text, subtype)?;
320 Ok(Value::Str(
321 multirange_from_ranges(subtype, [range]).to_text(),
322 ))
323 }
324 None | Some("unknown" | "cstring") => {
325 parse_multirange(text, subtype).map(|multirange| Value::Str(multirange.to_text()))
326 }
327 Some(source) if source == subtype.multirange_name() => {
328 parse_multirange(text, subtype).map(|multirange| Value::Str(multirange.to_text()))
329 }
330 Some(source) => Err(undefined_cast(source, subtype.multirange_name())),
331 }
332}
333
334fn canonical_type_name(type_name: &str) -> String {
335 let normalized = type_name.trim().to_ascii_lowercase();
336 normalized
337 .strip_prefix("pg_catalog.")
338 .unwrap_or(&normalized)
339 .to_string()
340}
341
342pub fn negate_value(value: &Value, source_ty: Option<&str>) -> Result<Value> {
344 if matches!(value, Value::Null) {
345 return Ok(Value::Null);
346 }
347 let source = canonical_cast_source(source_ty, value);
348 match (source.as_str(), value) {
349 ("int2", Value::Int(value)) => i16::try_from(*value)
350 .ok()
351 .and_then(i16::checked_neg)
352 .map(|value| Value::Int(i64::from(value)))
353 .ok_or_else(|| out_of_range("smallint")),
354 ("int4", Value::Int(value)) => i32::try_from(*value)
355 .ok()
356 .and_then(i32::checked_neg)
357 .map(|value| Value::Int(i64::from(value)))
358 .ok_or_else(|| out_of_range("integer")),
359 ("int8", Value::Int(value)) => value
360 .checked_neg()
361 .map(Value::Int)
362 .ok_or_else(|| out_of_range("bigint")),
363 ("float4" | "float8", Value::Float(value)) => Ok(Value::Float(-value)),
364 ("numeric", Value::Decimal(value)) => uqa_core::DecimalValue::from_i64(0)
365 .checked_sub(value)
366 .map(Value::Decimal)
367 .ok_or_else(|| out_of_range("numeric")),
368 (
369 "interval",
370 Value::Temporal(TemporalValue::Interval {
371 months,
372 days,
373 micros,
374 }),
375 ) => Ok(Value::Temporal(TemporalValue::Interval {
376 months: months
377 .checked_neg()
378 .ok_or_else(|| out_of_range("interval"))?,
379 days: days.checked_neg().ok_or_else(|| out_of_range("interval"))?,
380 micros: micros
381 .checked_neg()
382 .ok_or_else(|| out_of_range("interval"))?,
383 })),
384 _ => Err(SQLError::TypeMismatch(format!(
385 "operator does not exist: - {source}"
386 ))),
387 }
388}
389
390fn canonical_cast_source(source_ty: Option<&str>, value: &Value) -> String {
391 let source = source_ty.unwrap_or(match value {
392 Value::Str(_) | Value::FixedChar(_) => "unknown",
393 Value::Int(_) => "integer",
394 Value::Bool(_) => "boolean",
395 Value::Float(_) => "double precision",
396 Value::Decimal(_) => "numeric",
397 Value::Bytes(_) => "bytea",
398 Value::Temporal(TemporalValue::Interval { .. }) => "interval",
399 Value::Temporal(_) => "timestamp",
400 Value::Json(_) => "json",
401 Value::JsonB(_) => "jsonb",
402 Value::Array(_) => "anyarray",
403 Value::List(_) => "anyarray",
404 Value::Row(_) | Value::Record(_) => "record",
405 Value::Map(_) => "jsonb",
406 Value::Null => "unknown",
407 Value::Void => "void",
408 });
409 let (source, _) = split_type_modifier(source);
410 let source = source
411 .trim()
412 .to_ascii_lowercase()
413 .split_whitespace()
414 .collect::<Vec<_>>()
415 .join(" ");
416 let source = source.strip_prefix("pg_catalog.").unwrap_or(&source);
417 match source {
418 "smallint" | "int2" => "int2".into(),
419 "integer" | "int" | "int4" | "serial" | "serial4" => "int4".into(),
420 "bigint" | "int8" | "bigserial" | "serial8" => "int8".into(),
421 "character varying" | "varchar" => "varchar".into(),
422 "character" | "char" | "bpchar" => "bpchar".into(),
423 "boolean" | "bool" => "bool".into(),
424 "double" | "double precision" | "float8" => "float8".into(),
425 "real" | "float4" => "float4".into(),
426 other => other.into(),
427 }
428}
429
430fn undefined_cast(source: &str, target: &str) -> SQLError {
431 SQLError::Routine {
432 sqlstate: "42846".into(),
433 message: format!("cannot cast type {source} to {target}"),
434 }
435}
436
437fn postgres_type_display_name(name: &str) -> &str {
438 match name {
439 "int2" => "smallint",
440 "int4" => "integer",
441 "int8" => "bigint",
442 "float4" => "real",
443 "float8" => "double precision",
444 "bool" => "boolean",
445 "varchar" => "character varying",
446 "bpchar" => "character",
447 other => other,
448 }
449}
450
451fn cast_text(value: &Value, source: Option<&str>) -> String {
452 if let Value::Float(value) = value {
453 if source
454 .and_then(|source| crate::ast::ColumnType::from_sql_name(source).ok())
455 .is_some_and(|source| matches!(source, crate::ast::ColumnType::Real))
456 {
457 return super::floating::format_real(*value as f32);
458 }
459 }
460 value_to_string(value)
461}
462
463fn cast_uuid(value: &Value) -> Result<Value> {
464 let text = match value {
465 Value::Str(text) | Value::FixedChar(text) => text,
466 other => {
467 return Err(SQLError::TypeMismatch(format!(
468 "cannot cast {other:?} to uuid"
469 )))
470 }
471 };
472 super::uuid::canonicalize_uuid(text).map(Value::Str)
473}
474
475pub(super) use crate::ast::split_type_modifier;
476
477pub(super) fn cast_integer(v: &Value, target: &str) -> Result<Value> {
482 let n: i64 = match v {
483 Value::Int(n) => *n,
484 Value::Bool(b) => i64::from(*b),
485 Value::Float(f) => {
486 if !f.is_finite() {
487 return Err(out_of_range(target));
488 }
489 let rounded = f.round_ties_even();
490 if rounded < i64::MIN as f64 || rounded >= 9_223_372_036_854_775_808.0 {
494 return Err(out_of_range(target));
495 }
496 rounded as i64
497 }
498 Value::Decimal(d) => d
499 .round_dp(0)
500 .to_i64_trunc()
501 .ok_or_else(|| out_of_range(target))?,
502 Value::Str(s) | Value::FixedChar(s) => {
503 s.trim().parse::<i64>().map_err(|_| SQLError::Routine {
504 sqlstate: "22P02".into(),
505 message: format!("invalid input syntax for type {target}: \"{s}\""),
506 })?
507 }
508 Value::Bytes(bytes) => bytea_to_integer(bytes, target)?,
509 other => {
510 return Err(SQLError::TypeMismatch(format!(
511 "cannot cast {other:?} to {target}"
512 )));
513 }
514 };
515 let in_range = match target {
516 "smallint" => i16::try_from(n).is_ok(),
517 "integer" => i32::try_from(n).is_ok(),
518 _ => true,
519 };
520 if !in_range {
521 return Err(out_of_range(target));
522 }
523 Ok(Value::Int(n))
524}
525
526pub(super) fn cast_boolean(v: &Value) -> Result<Value> {
530 match v {
531 Value::Bool(b) => Ok(Value::Bool(*b)),
532 Value::Int(n) => Ok(Value::Bool(*n != 0)),
533 Value::Float(f) => Ok(Value::Bool(*f != 0.0)),
534 Value::Decimal(d) => Ok(Value::Bool(!d.is_zero())),
535 Value::Str(s) | Value::FixedChar(s) => {
536 let text = s.trim().to_ascii_lowercase();
537 let matches_prefix = |word: &str| !text.is_empty() && word.starts_with(&text);
538 let value = if matches_prefix("true") || matches_prefix("yes") || text == "1" {
539 Some(true)
540 } else if matches_prefix("false") || matches_prefix("no") || text == "0" {
541 Some(false)
542 } else if "on" == text {
543 Some(true)
544 } else if matches_prefix("off") && text.len() >= 2 {
545 Some(false)
546 } else {
547 None
548 };
549 value.map(Value::Bool).ok_or_else(|| SQLError::Routine {
550 sqlstate: "22P02".into(),
551 message: format!("invalid input syntax for type boolean: \"{s}\""),
552 })
553 }
554 other => Err(SQLError::TypeMismatch(format!(
555 "cannot cast {other:?} to boolean"
556 ))),
557 }
558}
559
560use array::cast_array_elements;
561pub use array::{array_dimensions, parse_pg_array_literal};
562use binary_oid::{
563 bytea_to_integer, cast_bytea, cast_oid, cast_regclass, cast_regnamespace, cast_regrole,
564 cast_xid,
565};
566use temporal::{cast_date, cast_temporal, TemporalCastTarget};
567
568#[cfg(test)]
569mod tests;