1mod array;
10mod binary_oid;
11mod legacy_vector;
12mod temporal;
13
14use super::conversion::value_to_string_with_control;
15use super::{out_of_range, ArrayValue, Result, SQLError, TemporalValue, Value};
16use crate::ast::RangeSubtype;
17use uqa_core::memory::{Produced, ProductionControl, ProductionString, ProductionVec};
18
19pub fn cast_value(v: &Value, ty: &str) -> Result<Value> {
23 cast_value_from(v, ty, None)
24}
25
26pub fn cast_value_from(v: &Value, ty: &str, source_ty: Option<&str>) -> Result<Value> {
28 cast_value_from_with_control(v, ty, source_ty, &ProductionControl::uncontrolled())?
29 .into_uncontrolled()
30 .map_err(|_| SQLError::Internal("ordinary cast production owner".into()))
31}
32
33#[expect(
34 clippy::too_many_lines,
35 reason = "cast matrix preserves source-target and error precedence"
36)]
37pub fn cast_value_from_with_control(
38 v: &Value,
39 ty: &str,
40 source_ty: Option<&str>,
41 control: &ProductionControl<'_>,
42) -> Result<Produced<Value>> {
43 control.check()?;
44 if let Value::Datum(datum) = v {
45 return cast_value_from_with_control(
46 &*super::datums::read_with_control(datum, control)?,
47 ty,
48 source_ty,
49 control,
50 );
51 }
52 if let Some(element) = array_scalar_type_name(ty) {
53 let element = element.trim();
54 let element = element
55 .split_once('.')
56 .filter(|(schema, _)| schema.eq_ignore_ascii_case("pg_catalog"))
57 .map_or(element, |(_, name)| name);
58 if let Some(name) = ["void", "pg_node_tree"]
59 .into_iter()
60 .find(|name| element.eq_ignore_ascii_case(name))
61 {
62 return Err(SQLError::Routine {
63 sqlstate: "42704".into(),
64 message: format!("type \"{name}[]\" does not exist"),
65 });
66 }
67 }
68 if matches!(v, Value::Null) {
69 return Ok(control.finish(Value::Null, control.empty_reservation())?);
70 }
71 let canonical;
73 let ty = if ty.contains('"') || ty.starts_with("pg_catalog.") {
74 canonical = crate::ColumnType::from_sql_name_with_control(ty, control)?
75 .sql_name_with_control(control)?;
76 canonical.as_str()
77 } else {
78 ty
79 };
80 let canonical_source;
81 let source_ty = match source_ty {
82 Some(source) if source.contains('"') || source.starts_with("pg_catalog.") => {
83 canonical_source = crate::ColumnType::from_sql_name_with_control(source, control)?
84 .sql_name_with_control(control)?;
85 Some(canonical_source.as_str())
86 }
87 source => source,
88 };
89 let (base, modifier) = crate::ast::split_type_modifier_with_control(ty, control)?;
90 let target = base
91 .trim()
92 .strip_prefix("pg_catalog.")
93 .unwrap_or(base.trim());
94 if matches!(v, Value::Void)
95 && !matches!(
96 target,
97 "void"
98 | "text"
99 | "name"
100 | "varchar"
101 | "character varying"
102 | "bpchar"
103 | "character"
104 | "char"
105 )
106 {
107 return Err(undefined_cast("void", postgres_type_display_name(target)));
108 }
109 if let Some(element_type) = array_scalar_type_name(ty) {
110 let source_element_type = source_ty.and_then(array_scalar_type_name).or(match v {
111 Value::LegacyVector(vector) => Some(match vector.kind() {
112 uqa_core::LegacyVectorKind::SmallInteger => "smallint",
113 uqa_core::LegacyVectorKind::Oid => "oid",
114 }),
115 _ => None,
116 });
117 let parsed;
118 let array = match v {
119 Value::Array(array) => array,
120 Value::LegacyVector(vector) => vector.as_array(),
121 Value::Str(text) => {
122 parsed = array::parse_pg_array_literal_with_control(text, control)?;
123 &parsed
124 }
125 other => {
126 return Err(SQLError::TypeMismatch(format!(
127 "CAST AS {ty}: expected array, got {other:?}"
128 )))
129 }
130 };
131 let elements = array::cast_array_elements(
132 array.elements(),
133 element_type,
134 source_element_type,
135 control,
136 )?;
137 let normalize_empty = array.elements().is_empty()
138 && !array::binary_compatible_elements(source_element_type, element_type, control)?;
139 let mut bounds = ProductionVec::new(*control);
140 bounds.reserve(array.lower_bounds().len())?;
141 if !normalize_empty {
142 for lower in array.lower_bounds() {
143 bounds.push_copy(*lower)?;
144 }
145 }
146 let array =
147 ArrayValue::with_lower_bounds_with_control(elements, bounds.finish()?, control)?
148 .ok_or_else(|| {
149 SQLError::TypeMismatch("array dimensions changed during cast".into())
150 })?;
151 let (array, memory) = array.into_parts();
152 let element = crate::ColumnType::from_sql_name_with_control(element_type, control)?;
153 let oid = crate::catalog::type_metadata::pg_type_oid(&element) as u32;
154 return Ok(control.finish(Value::Array(array.with_element_type_oid(Some(oid))), memory)?);
155 }
156 let value = match &**base {
157 "void" | "pg_catalog.void" => {
158 let source = canonical_cast_source_with_control(source_ty, v, control)?;
159 if matches!(
160 source.as_str(),
161 "unknown" | "text" | "name" | "varchar" | "bpchar" | "void"
162 ) {
163 Ok(Value::Void)
164 } else {
165 Err(undefined_cast(postgres_type_display_name(&source), "void"))
166 }
167 }
168 "smallint" | "int2" | "pg_catalog.int2" => cast_integer(v, "smallint", control),
169 "integer" | "int" | "int4" | "serial" | "serial4" | "pg_catalog.int4" => {
170 binary_oid::cast_integer_from(v, source_ty, control)
171 }
172 "bigint" | "int8" | "bigserial" | "serial8" | "pg_catalog.int8" => {
173 cast_integer(v, "bigint", control)
174 }
175 "real" | "float4" | "pg_catalog.float4" => {
176 super::floating::to_float_with_control(v, super::FloatWidth::Real, control)
177 .map(Value::Float)
178 }
179 "float8" | "double" | "double precision" | "pg_catalog.float8" => {
180 super::floating::to_float_with_control(v, super::FloatWidth::DoublePrecision, control)
181 .map(Value::Float)
182 }
183 "numeric" | "decimal" => {
184 let value = super::conversion::to_decimal_with_control(v, control)?;
185 let value = if let Some(modifier) = modifier {
186 let mut parts = modifier.split(',').map(str::trim);
187 let precision: u32 = parts
188 .next()
189 .and_then(|p| p.parse().ok())
190 .ok_or_else(|| SQLError::TypeMismatch("bad numeric precision".into()))?;
191 let scale: i32 = parts.next().and_then(|s| s.parse().ok()).unwrap_or(0);
192 let rounded = value
193 .round_to_scale_with_control(scale, control)?
194 .ok_or_else(|| out_of_range("numeric"))?;
195 if !rounded.fits_precision_with_control(precision, scale, control)? {
196 return Err(crate::assignment::conversion::numeric_field_overflow(
197 precision, scale,
198 ));
199 }
200 rounded
201 } else {
202 value
203 };
204 let (value, memory) = value.into_parts();
205 return Ok(control.finish(Value::Decimal(value), memory)?);
206 }
207 "regproc" | "regprocedure" | "regrole" | "regtype" if matches!(v, Value::Int(_)) => {
208 Ok(v.clone())
209 }
210 "pg_node_tree" => {
211 let source = canonical_cast_source_with_control(source_ty, v, control)?;
212 return match source.as_str() {
213 "pg_node_tree" => Ok(control.copy_value(v)?),
214 "unknown" | "text" | "varchar" | "bpchar" | "name" => Err(SQLError::Unsupported(
215 "cannot accept a value of type pg_node_tree".into(),
216 )),
217 _ => Err(undefined_cast(
218 postgres_type_display_name(&source),
219 "pg_node_tree",
220 )),
221 };
222 }
223 "text"
224 | "refcursor"
225 | "pg_catalog.refcursor"
226 | "name"
227 | "regproc"
228 | "regprocedure"
229 | "regtype"
230 | "aclitem" => {
231 let source = source_ty
232 .map(str::trim)
233 .map(|source| source.strip_prefix("pg_catalog.").unwrap_or(source));
234 let text = match (source, v) {
235 (Some("int2vector" | "oidvector"), _) => {
236 match super::conversion::vector_value_to_string_with_control(v, control)? {
237 Some(text) => text,
238 None => value_to_string_with_control(v, control)?,
239 }
240 }
241 (
242 Some(
243 "regproc" | "regprocedure" | "regclass" | "regcollation" | "regnamespace"
244 | "regrole" | "regtype",
245 ),
246 Value::Int(0),
247 ) => control.copy_text("-")?,
248 _ => cast_text(v, source_ty, control)?,
249 };
250 return text_value(text, false, control);
251 }
252 "int2vector" | "pg_catalog.int2vector" => {
253 return legacy_vector::cast_int2vector(v, source_ty, control)
254 }
255 "oidvector" | "pg_catalog.oidvector" => {
256 return legacy_vector::cast_oidvector(v, source_ty, control)
257 }
258 "oid" | "pg_catalog.oid" => cast_oid(v, source_ty, control),
259 "regclass"
260 | "pg_catalog.regclass"
261 | "regnamespace"
262 | "pg_catalog.regnamespace"
263 | "regcollation"
264 | "pg_catalog.regcollation"
265 | "regrole"
266 | "pg_catalog.regrole" => return cast_named_oid(v, target, source_ty, control),
267 "xid" | "pg_catalog.xid" => cast_xid(v, source_ty, control),
268 "\"char\"" => {
269 let source = canonical_cast_source_with_control(source_ty, v, control)?;
270 let byte = if source.as_str() == "\"char\"" {
271 super::internal_char::value_byte(v)
272 .ok_or_else(|| SQLError::Internal("invalid internal char carrier".into()))?
273 } else if let ("int4", Value::Int(value)) = (source.as_str(), v) {
274 i8::try_from(*value).map_err(|_| out_of_range("\"char\""))? as u8
275 } else {
276 let text = value_to_string_with_control(v, control)?;
277 super::internal_char::input_byte(&text)
278 };
279 return Ok(super::internal_char::carrier_with_control(byte, control)?);
280 }
281 "uuid" => return cast_uuid(v, control),
282 "record" => {
284 return match v {
285 Value::Row(_) | Value::Record(_) => Ok(control.copy_value(v)?),
286 Value::Str(_) | Value::FixedChar(_) => Err(SQLError::Routine {
287 sqlstate: "0A000".into(),
288 message: "input of anonymous composite types is not implemented".into(),
289 }),
290 _ => Err(undefined_cast(
291 &canonical_cast_source_with_control(source_ty, v, control)?,
292 "record",
293 )),
294 }
295 }
296 "varchar" | "character varying" => {
297 let text = cast_text(v, source_ty, control)?;
298 let Some(modifier) = modifier else {
299 return text_value(text, false, control);
300 };
301 let limit: usize = modifier
302 .trim()
303 .parse()
304 .map_err(|_| SQLError::TypeMismatch(format!("bad length modifier {modifier}")))?;
305 return character_value(text, limit, false, control);
306 }
307 "bpchar" if modifier.is_none() => {
308 let text = match v {
309 Value::FixedChar(text) => control.copy_text(text)?,
312 _ => cast_text(v, source_ty, control)?,
313 };
314 return text_value(text, true, control);
315 }
316 "character" | "char" | "bpchar" => {
317 let text = cast_text(v, source_ty, control)?;
318 let limit: usize = match modifier {
319 Some(modifier) => modifier.trim().parse().map_err(|_| {
320 SQLError::TypeMismatch(format!("bad length modifier {modifier}"))
321 })?,
322 None => 1,
323 };
324 if limit == 0 {
325 return Err(SQLError::TypeMismatch(
326 "CHARACTER length must be greater than zero".into(),
327 ));
328 }
329 return character_value(text, limit, true, control);
330 }
331 "date" => cast_temporal(v, source_ty, TemporalCastTarget::Date, None, control),
332 "time" | "time without time zone" => {
333 cast_temporal(v, source_ty, TemporalCastTarget::Time, modifier, control)
334 }
335 "timetz" | "time with time zone" => {
336 cast_temporal(v, source_ty, TemporalCastTarget::TimeTz, modifier, control)
337 }
338 "timestamp" | "datetime" | "timestamp without time zone" => cast_temporal(
339 v,
340 source_ty,
341 TemporalCastTarget::Timestamp,
342 modifier,
343 control,
344 ),
345 "timestamptz" | "timestamp with time zone" => cast_temporal(
346 v,
347 source_ty,
348 TemporalCastTarget::TimestampTz,
349 modifier,
350 control,
351 ),
352 "interval" => temporal::cast_interval(v, source_ty, ty, control),
353 name if name.starts_with("interval ") => temporal::cast_interval(v, source_ty, ty, control),
354 "int4range" => return cast_range(v, source_ty, RangeSubtype::Integer, control),
355 "int8range" => return cast_range(v, source_ty, RangeSubtype::BigInteger, control),
356 "numrange" => return cast_range(v, source_ty, RangeSubtype::Numeric, control),
357 "daterange" => return cast_range(v, source_ty, RangeSubtype::Date, control),
358 "tsrange" => return cast_range(v, source_ty, RangeSubtype::Timestamp, control),
359 "tstzrange" => return cast_range(v, source_ty, RangeSubtype::TimestampTz, control),
360 "int4multirange" => return cast_multirange(v, source_ty, RangeSubtype::Integer, control),
361 "int8multirange" => {
362 return cast_multirange(v, source_ty, RangeSubtype::BigInteger, control)
363 }
364 "nummultirange" => return cast_multirange(v, source_ty, RangeSubtype::Numeric, control),
365 "datemultirange" => return cast_multirange(v, source_ty, RangeSubtype::Date, control),
366 "tsmultirange" => return cast_multirange(v, source_ty, RangeSubtype::Timestamp, control),
367 "tstzmultirange" => {
368 return cast_multirange(v, source_ty, RangeSubtype::TimestampTz, control)
369 }
370 "json" => return super::json::cast_json_value_with_control(v, false, control),
371 "jsonb" => return super::json::cast_json_value_with_control(v, true, control),
372 "bytea" => return cast_bytea(v, source_ty, control),
373 "boolean" | "bool" => cast_boolean(v),
374 other => Err(SQLError::Unsupported(format!("CAST AS {other}"))),
375 }?;
376 Ok(control.finish(value, control.empty_reservation())?)
377}
378
379fn array_scalar_type_name(ty: &str) -> Option<&str> {
381 let mut element = ty.trim().strip_suffix("[]")?.trim_end();
382 while let Some(inner) = element.strip_suffix("[]") {
383 element = inner.trim_end();
384 }
385 Some(element)
386}
387
388fn text_value(
389 text: Produced<String>,
390 fixed: bool,
391 control: &ProductionControl<'_>,
392) -> Result<Produced<Value>> {
393 let (text, memory) = text.into_parts();
394 Ok(control.finish(
395 if fixed {
396 Value::FixedChar(text)
397 } else {
398 Value::Str(text)
399 },
400 memory,
401 )?)
402}
403
404fn character_value(
405 text: Produced<String>,
406 limit: usize,
407 fixed: bool,
408 control: &ProductionControl<'_>,
409) -> Result<Produced<Value>> {
410 let mut count = 0;
411 let mut end = 0;
412 for (index, character) in text.char_indices().take(limit) {
413 control.check()?;
414 count += 1;
415 end = index + character.len_utf8();
416 }
417 let mut output = ProductionString::from_produced(text, *control)?;
418 output.truncate(end)?;
419 if fixed {
420 for _ in count..limit {
421 output.push(' ')?;
422 }
423 }
424 text_value(output.finish()?, fixed, control)
425}
426
427fn cast_range(
428 v: &Value,
429 source_ty: Option<&str>,
430 subtype: RangeSubtype,
431 control: &ProductionControl<'_>,
432) -> Result<Produced<Value>> {
433 let source = source_ty
434 .map(|source| canonical_type_name(source, control))
435 .transpose()?;
436 let source = source.as_ref().map(|source| source.as_str());
437 if source.is_some_and(|source| {
438 source != subtype.range_name() && !matches!(source, "unknown" | "cstring")
439 }) {
440 return Err(undefined_cast(
441 source.unwrap_or("unknown"),
442 subtype.range_name(),
443 ));
444 }
445 let (Value::Str(text) | Value::FixedChar(text)) = v else {
446 return Err(undefined_cast(
447 source.unwrap_or("unknown"),
448 subtype.range_name(),
449 ));
450 };
451 text_value(
452 super::range::canonical_range_text_with_control(text, subtype, control)?,
453 false,
454 control,
455 )
456}
457
458fn cast_multirange(
459 v: &Value,
460 source_ty: Option<&str>,
461 subtype: RangeSubtype,
462 control: &ProductionControl<'_>,
463) -> Result<Produced<Value>> {
464 let source = source_ty
465 .map(|source| canonical_type_name(source, control))
466 .transpose()?;
467 let source = source.as_ref().map(|source| source.as_str());
468 let (Value::Str(text) | Value::FixedChar(text)) = v else {
469 return Err(undefined_cast(
470 source.unwrap_or("unknown"),
471 subtype.multirange_name(),
472 ));
473 };
474 let text = match source {
475 Some(source) if source == subtype.range_name() => {
476 super::range::canonical_range_as_multirange_text_with_control(text, subtype, control)?
477 }
478 None | Some("unknown" | "cstring") => {
479 super::range::canonical_multirange_text_with_control(text, subtype, control)?
480 }
481 Some(source) if source == subtype.multirange_name() => {
482 super::range::canonical_multirange_text_with_control(text, subtype, control)?
483 }
484 Some(source) => return Err(undefined_cast(source, subtype.multirange_name())),
485 };
486 text_value(text, false, control)
487}
488
489fn canonical_type_name(
490 type_name: &str,
491 control: &ProductionControl<'_>,
492) -> Result<Produced<String>> {
493 let mut normalized = ProductionString::new(*control);
494 for character in type_name.trim().chars() {
495 normalized.push(character.to_ascii_lowercase())?;
496 }
497 Ok(control.copy_text(
498 normalized
499 .strip_prefix("pg_catalog.")
500 .unwrap_or(&normalized),
501 )?)
502}
503
504pub fn negate_value(value: &Value, source_ty: Option<&str>) -> Result<Value> {
506 negate_value_with_control(value, source_ty, &ProductionControl::uncontrolled())?
507 .into_uncontrolled()
508 .map_err(|_| SQLError::Internal("ordinary negation owner".into()))
509}
510
511pub fn negate_value_with_control(
512 value: &Value,
513 source_ty: Option<&str>,
514 control: &ProductionControl<'_>,
515) -> Result<Produced<Value>> {
516 control.check()?;
517 if matches!(value, Value::Null) {
518 return Ok(control.finish(Value::Null, control.empty_reservation())?);
519 }
520 let source = canonical_cast_source_with_control(source_ty, value, control)?;
521 let result = match (source.as_str(), value) {
522 ("int2", Value::Int(value)) => i16::try_from(*value)
523 .ok()
524 .and_then(i16::checked_neg)
525 .map(|value| Value::Int(i64::from(value)))
526 .ok_or_else(|| out_of_range("smallint")),
527 ("int4", Value::Int(value)) => i32::try_from(*value)
528 .ok()
529 .and_then(i32::checked_neg)
530 .map(|value| Value::Int(i64::from(value)))
531 .ok_or_else(|| out_of_range("integer")),
532 ("int8", Value::Int(value)) => value
533 .checked_neg()
534 .map(Value::Int)
535 .ok_or_else(|| out_of_range("bigint")),
536 ("float4" | "float8", Value::Float(value)) => Ok(Value::Float(-value)),
537 ("numeric", Value::Decimal(value)) => {
538 let (value, memory) = value.negated_with_control(control)?.into_parts();
539 return Ok(control.finish(Value::Decimal(value), memory)?);
540 }
541 ("interval", Value::Temporal(interval @ TemporalValue::Interval { .. })) => {
542 Ok(Value::Temporal(
543 super::IntervalFields::of(interval)
544 .expect("interval operand")
545 .negate()?
546 .value(),
547 ))
548 }
549 _ => Err(crate::type_resolution::undefined_prefix_operator(
550 "-",
551 postgres_type_display_name(source.as_str()),
552 )),
553 }?;
554 Ok(control.finish(result, control.empty_reservation())?)
555}
556
557fn canonical_cast_source_with_control(
558 source_ty: Option<&str>,
559 value: &Value,
560 control: &ProductionControl<'_>,
561) -> Result<Produced<String>> {
562 let source = source_ty.unwrap_or(match value {
563 Value::Str(_) | Value::FixedChar(_) => "unknown",
564 Value::Int(_) => "integer",
565 Value::Bool(_) => "boolean",
566 Value::Float(_) => "double precision",
567 Value::Decimal(_) => "numeric",
568 Value::Bytes(_) => "bytea",
569 Value::Temporal(TemporalValue::Date { .. }) => "date",
570 Value::Temporal(TemporalValue::Time { .. }) => "time without time zone",
571 Value::Temporal(TemporalValue::TimeTz { .. }) => "time with time zone",
572 Value::Temporal(TemporalValue::Timestamp { .. }) => "timestamp without time zone",
573 Value::Temporal(TemporalValue::TimestampTz { .. }) => "timestamp with time zone",
574 Value::Temporal(TemporalValue::Interval { .. }) => "interval",
575 Value::Json(_) => "json",
576 Value::JsonB(_) => "jsonb",
577 Value::Array(_) => "anyarray",
578 Value::Enum(_) => "anyenum",
579 Value::Datum(value) => super::datums::type_name(value),
580 Value::LegacyVector(vector) => vector.kind().type_name(),
581 Value::List(_) => "anyarray",
582 Value::Row(_) | Value::Record(_) => "record",
583 Value::Map(_) => "jsonb",
584 Value::Null => "unknown",
585 Value::Void => "void",
586 });
587 let declared;
588 let source = if source.contains('"') || source.starts_with("pg_catalog.") {
589 declared = crate::ColumnType::from_sql_name_with_control(source, control)?
590 .sql_name_with_control(control)?;
591 declared.as_str()
592 } else {
593 source
594 };
595 let (source, _) = crate::ast::split_type_modifier_with_control(source, control)?;
596 let mut normalized = ProductionString::new(*control);
597 for (index, word) in source.split_whitespace().enumerate() {
598 if index != 0 {
599 normalized.push(' ')?;
600 }
601 for character in word.chars() {
602 normalized.push(character.to_ascii_lowercase())?;
603 }
604 }
605 let source = normalized
606 .strip_prefix("pg_catalog.")
607 .unwrap_or(&normalized);
608 let canonical = match source {
609 "smallint" | "int2" => "int2",
610 "integer" | "int" | "int4" | "serial" | "serial4" => "int4",
611 "bigint" | "int8" | "bigserial" | "serial8" => "int8",
612 "character varying" | "varchar" => "varchar",
613 "character" | "char" | "bpchar" => "bpchar",
614 "boolean" | "bool" => "bool",
615 "double" | "double precision" | "float8" => "float8",
616 "real" | "float4" => "float4",
617 other => other,
618 };
619 Ok(control.copy_text(canonical)?)
620}
621
622fn undefined_cast(source: &str, target: &str) -> SQLError {
623 SQLError::Routine {
624 sqlstate: "42846".into(),
625 message: format!("cannot cast type {source} to {target}"),
626 }
627}
628
629fn postgres_type_display_name(name: &str) -> &str {
630 match name {
631 "int2" => "smallint",
632 "int4" => "integer",
633 "int8" => "bigint",
634 "float4" => "real",
635 "float8" => "double precision",
636 "bool" => "boolean",
637 "varchar" => "character varying",
638 "bpchar" => "character",
639 other => other,
640 }
641}
642
643fn cast_text(
644 value: &Value,
645 source: Option<&str>,
646 control: &ProductionControl<'_>,
647) -> Result<Produced<String>> {
648 if source == Some("\"char\"") {
649 let byte = super::internal_char::value_byte(value)
650 .ok_or_else(|| SQLError::Internal("invalid internal char carrier".into()))?;
651 return Ok(super::internal_char::output_with_control(byte, control)?);
652 }
653 if let (Value::Float(value), Some(source)) = (value, source) {
654 let source = crate::ast::ColumnType::from_sql_name_with_control(source, control);
655 match source {
656 Ok(source) if matches!(&*source, crate::ast::ColumnType::Real) => {
657 return super::floating::format_real_with_control(*value as f32, control)
658 }
659 Err(error) if matches!(error.sqlstate(), Some("53200" | "57014")) => return Err(error),
660 _ => {}
661 }
662 }
663 value_to_string_with_control(value, control)
664}
665
666fn cast_uuid(value: &Value, control: &ProductionControl<'_>) -> Result<Produced<Value>> {
667 let text = match value {
668 Value::Str(text) | Value::FixedChar(text) => text,
669 other => {
670 return Err(SQLError::TypeMismatch(format!(
671 "cannot cast {other:?} to uuid"
672 )))
673 }
674 };
675 text_value(
676 super::uuid::canonicalize_uuid_with_control(text, control)?,
677 false,
678 control,
679 )
680}
681
682pub(super) fn cast_integer(
687 v: &Value,
688 target: &str,
689 control: &ProductionControl<'_>,
690) -> Result<Value> {
691 control.check()?;
692 let n: i64 = match v {
693 Value::Int(n) => *n,
694 Value::Bool(b) => i64::from(*b),
695 Value::Float(f) => {
696 if !f.is_finite() {
697 return Err(out_of_range(target));
698 }
699 let rounded = f.round_ties_even();
700 if rounded < i64::MIN as f64 || rounded >= 9_223_372_036_854_775_808.0 {
704 return Err(out_of_range(target));
705 }
706 rounded as i64
707 }
708 Value::Decimal(d) => d
709 .round_to_scale_with_control(0, control)?
710 .ok_or_else(|| out_of_range(target))?
711 .to_i64_trunc_with_control(control)?
712 .ok_or_else(|| out_of_range(target))?,
713 Value::Str(s) | Value::FixedChar(s) => return integer_from_text(s, target),
714 Value::Bytes(bytes) => bytea_to_integer(bytes, target)?,
715 other => {
716 return Err(SQLError::TypeMismatch(format!(
717 "cannot cast {other:?} to {target}"
718 )));
719 }
720 };
721 let in_range = match target {
722 "smallint" => i16::try_from(n).is_ok(),
723 "integer" => i32::try_from(n).is_ok(),
724 _ => true,
725 };
726 if !in_range {
727 return Err(out_of_range(target));
728 }
729 Ok(Value::Int(n))
730}
731
732#[must_use]
734pub fn parse_boolean_input(text: &str) -> Option<bool> {
735 let text = text.trim();
736 let matches_prefix = |word: &str| {
737 !text.is_empty()
738 && word
739 .get(..text.len())
740 .is_some_and(|prefix| prefix.eq_ignore_ascii_case(text))
741 };
742 if matches_prefix("true") || matches_prefix("yes") || text == "1" {
743 Some(true)
744 } else if matches_prefix("false") || matches_prefix("no") || text == "0" {
745 Some(false)
746 } else if text.eq_ignore_ascii_case("on") {
747 Some(true)
748 } else if matches_prefix("off") && text.len() >= 2 {
749 Some(false)
750 } else {
751 None
752 }
753}
754
755#[must_use]
757pub fn invalid_boolean_input(text: &str) -> SQLError {
758 SQLError::Routine {
759 sqlstate: "22P02".into(),
760 message: format!("invalid input syntax for type boolean: \"{text}\""),
761 }
762}
763
764fn integer_from_text(text: &str, target: &str) -> Result<Value> {
766 use crate::expr::integer_input::{parse_int8, IntegerInputError};
767 let out_of_range = || SQLError::Routine {
768 sqlstate: "22003".into(),
769 message: format!("value \"{text}\" is out of range for type {target}"),
770 };
771 let value = parse_int8(text).map_err(|error| match error {
772 IntegerInputError::OutOfRange => out_of_range(),
773 IntegerInputError::InvalidSyntax => SQLError::Routine {
774 sqlstate: "22P02".into(),
775 message: format!("invalid input syntax for type {target}: \"{text}\""),
776 },
777 })?;
778 let in_range = match target {
779 "smallint" => i16::try_from(value).is_ok(),
780 "integer" => i32::try_from(value).is_ok(),
781 _ => true,
782 };
783 if in_range {
784 Ok(Value::Int(value))
785 } else {
786 Err(out_of_range())
787 }
788}
789
790pub(super) fn cast_boolean(v: &Value) -> Result<Value> {
794 match v {
795 Value::Bool(b) => Ok(Value::Bool(*b)),
796 Value::Int(n) => Ok(Value::Bool(*n != 0)),
797 Value::Float(f) => Ok(Value::Bool(*f != 0.0)),
798 Value::Decimal(d) => Ok(Value::Bool(!d.is_zero())),
799 Value::Str(s) | Value::FixedChar(s) => parse_boolean_input(s)
800 .map(Value::Bool)
801 .ok_or_else(|| invalid_boolean_input(s)),
802 other => Err(SQLError::TypeMismatch(format!(
803 "cannot cast {other:?} to boolean"
804 ))),
805 }
806}
807
808pub use array::{array_dimensions, parse_pg_array_literal, parse_pg_array_literal_with_control};
809use binary_oid::{bytea_to_integer, cast_bytea, cast_named_oid, cast_oid, cast_xid};
810use temporal::{cast_temporal, TemporalCastTarget};
811
812#[cfg(test)]
813mod production_tests;
814#[cfg(test)]
815mod tests;