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