1use super::{
10 out_of_range, parse_json, to_decimal, to_f64, typed_json_value, value_to_json, value_to_string,
11 ArrayValue, Result, SQLError, TemporalValue, Value,
12};
13
14pub fn cast_value(v: &Value, ty: &str) -> Result<Value> {
18 cast_value_from(v, ty, None)
19}
20
21pub fn cast_value_from(v: &Value, ty: &str, source_ty: Option<&str>) -> Result<Value> {
23 if matches!(v, Value::Null) {
24 return Ok(Value::Null);
25 }
26 if let Some(elem_ty) = ty.strip_suffix("[]") {
27 let source_elem_ty = source_ty
28 .and_then(|source| source.trim().strip_suffix("[]"))
29 .map(str::trim);
30 let array = match v {
31 Value::Array(array) => array.clone(),
32 Value::Str(s) => parse_pg_array_literal(s)?,
33 other => {
34 return Err(SQLError::TypeMismatch(format!(
35 "CAST AS {ty}: expected array, got {other:?}"
36 )));
37 }
38 };
39 let elements = cast_array_elements(array.elements(), elem_ty, source_elem_ty)?;
40 return ArrayValue::with_lower_bounds(elements, array.lower_bounds().to_vec())
41 .map(Value::Array)
42 .ok_or_else(|| SQLError::TypeMismatch("array dimensions changed during cast".into()));
43 }
44 let (base, modifier) = split_type_modifier(ty);
45 match base {
46 "smallint" | "int2" | "pg_catalog.int2" => cast_integer(v, "smallint"),
47 "integer" | "int" | "int4" | "serial" | "serial4" | "pg_catalog.int4" => {
48 cast_integer(v, "integer")
49 }
50 "bigint" | "int8" | "bigserial" | "serial8" | "pg_catalog.int8" => {
51 cast_integer(v, "bigint")
52 }
53 "real" | "float4" | "float8" | "double" | "double precision" => {
54 Ok(Value::Float(to_f64(v)?))
55 }
56 "numeric" | "decimal" => {
57 let value = to_decimal(v)?;
58 if let Some(modifier) = modifier {
59 let mut parts = modifier.split(',').map(str::trim);
60 let precision: u32 = parts
61 .next()
62 .and_then(|p| p.parse().ok())
63 .ok_or_else(|| SQLError::TypeMismatch("bad numeric precision".into()))?;
64 let scale: i32 = parts.next().and_then(|s| s.parse().ok()).unwrap_or(0);
65 let rounded = value
66 .round_to_scale(scale)
67 .ok_or_else(|| out_of_range("numeric"))?;
68 if !rounded.fits_precision(precision, scale) {
69 return Err(SQLError::Routine {
70 sqlstate: "22003".into(),
71 message: format!(
72 "numeric field overflow: A field with precision {precision}, scale {scale} cannot hold value {}",
73 value.to_sql_string()
74 ),
75 });
76 }
77 return Ok(Value::Decimal(rounded));
78 }
79 Ok(Value::Decimal(value))
80 }
81 "text" | "name" | "regproc" | "regtype" | "pg_node_tree" | "aclitem" => {
82 Ok(Value::Str(value_to_string(v)))
83 }
84 "oid" | "pg_catalog.oid" => cast_oid(v, source_ty),
85 "regclass" | "pg_catalog.regclass" => cast_regclass(v, source_ty),
86 "regnamespace" | "pg_catalog.regnamespace" => cast_regnamespace(v, source_ty),
87 "xid" | "pg_catalog.xid" => cast_xid(v, source_ty),
88 "\"char\"" => {
89 let text = value_to_string(v);
90 let mut characters = text.chars();
91 let Some(character) = characters.next() else {
92 return Ok(Value::Str(String::new()));
93 };
94 if characters.next().is_some() || !character.is_ascii() {
95 return Err(SQLError::TypeMismatch(format!(
96 "value too long for type character(1): {text:?}"
97 )));
98 }
99 Ok(Value::Str(character.to_string()))
100 }
101 "uuid" => cast_uuid(v),
102 "varchar" | "character varying" => {
104 let text = value_to_string(v);
105 let Some(modifier) = modifier else {
106 return Ok(Value::Str(text));
107 };
108 let limit: usize = modifier
109 .trim()
110 .parse()
111 .map_err(|_| SQLError::TypeMismatch(format!("bad length modifier {modifier}")))?;
112 Ok(Value::Str(text.chars().take(limit).collect()))
113 }
114 "bpchar" if modifier.is_none() => Ok(Value::FixedChar(value_to_string(v))),
117 "character" | "char" | "bpchar" => {
118 let text = value_to_string(v);
119 let limit: usize = match modifier {
120 Some(modifier) => modifier.trim().parse().map_err(|_| {
121 SQLError::TypeMismatch(format!("bad length modifier {modifier}"))
122 })?,
123 None => 1,
124 };
125 if limit == 0 {
126 return Err(SQLError::TypeMismatch(
127 "CHARACTER length must be greater than zero".into(),
128 ));
129 }
130 let mut text = text.chars().take(limit).collect::<String>();
131 text.extend(std::iter::repeat_n(
132 ' ',
133 limit.saturating_sub(text.chars().count()),
134 ));
135 Ok(Value::FixedChar(text))
136 }
137 "date" => cast_date(v, source_ty),
138 "time" | "time without time zone" => cast_temporal(
139 v,
140 TemporalCastTarget::Time,
141 TemporalValue::parse_time,
142 "time",
143 ),
144 "timetz" | "time with time zone" => cast_temporal(
145 v,
146 TemporalCastTarget::TimeTz,
147 TemporalValue::parse_time_tz,
148 "time with time zone",
149 ),
150 "timestamp" | "datetime" | "timestamp without time zone" => cast_temporal(
151 v,
152 TemporalCastTarget::Timestamp,
153 TemporalValue::parse_timestamp,
154 "timestamp",
155 ),
156 "timestamptz" | "timestamp with time zone" => cast_temporal(
157 v,
158 TemporalCastTarget::TimestampTz,
159 TemporalValue::parse_timestamp_tz,
160 "timestamp with time zone",
161 ),
162 "interval" => cast_temporal(
163 v,
164 TemporalCastTarget::Interval,
165 TemporalValue::parse_interval,
166 "interval",
167 ),
168 "json" => {
169 if let Value::Json(text) = v {
170 return Ok(Value::Json(text.clone()));
171 }
172 if let Value::Str(text) | Value::FixedChar(text) = v {
173 let _validated = parse_json(text)?;
174 return Ok(Value::Json(text.clone()));
175 }
176 typed_json_value(&value_to_json(v), false)
177 }
178 "jsonb" => {
179 if let Value::JsonB(text) = v {
180 return Ok(Value::JsonB(text.clone()));
181 }
182 let parsed = match v {
183 Value::Json(text) | Value::Str(text) | Value::FixedChar(text) => parse_json(text)?,
184 other => value_to_json(other),
185 };
186 typed_json_value(&parsed, true)
187 }
188 "bytea" => cast_bytea(v, source_ty),
189 "boolean" | "bool" => cast_boolean(v),
190 other => Err(SQLError::Unsupported(format!("CAST AS {other}"))),
191 }
192}
193
194pub fn negate_value(value: &Value, source_ty: Option<&str>) -> Result<Value> {
196 if matches!(value, Value::Null) {
197 return Ok(Value::Null);
198 }
199 let source = canonical_cast_source(source_ty, value);
200 match (source.as_str(), value) {
201 ("int2", Value::Int(value)) => i16::try_from(*value)
202 .ok()
203 .and_then(i16::checked_neg)
204 .map(|value| Value::Int(i64::from(value)))
205 .ok_or_else(|| out_of_range("smallint")),
206 ("int4", Value::Int(value)) => i32::try_from(*value)
207 .ok()
208 .and_then(i32::checked_neg)
209 .map(|value| Value::Int(i64::from(value)))
210 .ok_or_else(|| out_of_range("integer")),
211 ("int8", Value::Int(value)) => value
212 .checked_neg()
213 .map(Value::Int)
214 .ok_or_else(|| out_of_range("bigint")),
215 ("float4" | "float8", Value::Float(value)) => Ok(Value::Float(-value)),
216 ("numeric", Value::Decimal(value)) => uqa_core::DecimalValue::from_i64(0)
217 .checked_sub(value)
218 .map(Value::Decimal)
219 .ok_or_else(|| out_of_range("numeric")),
220 (
221 "interval",
222 Value::Temporal(TemporalValue::Interval {
223 months,
224 days,
225 micros,
226 }),
227 ) => Ok(Value::Temporal(TemporalValue::Interval {
228 months: months
229 .checked_neg()
230 .ok_or_else(|| out_of_range("interval"))?,
231 days: days.checked_neg().ok_or_else(|| out_of_range("interval"))?,
232 micros: micros
233 .checked_neg()
234 .ok_or_else(|| out_of_range("interval"))?,
235 })),
236 _ => Err(SQLError::TypeMismatch(format!(
237 "operator does not exist: - {source}"
238 ))),
239 }
240}
241
242fn canonical_cast_source(source_ty: Option<&str>, value: &Value) -> String {
243 let source = source_ty.unwrap_or(match value {
244 Value::Str(_) | Value::FixedChar(_) => "unknown",
245 Value::Int(_) => "integer",
246 Value::Bool(_) => "boolean",
247 Value::Float(_) => "double precision",
248 Value::Decimal(_) => "numeric",
249 Value::Bytes(_) => "bytea",
250 Value::Temporal(TemporalValue::Interval { .. }) => "interval",
251 Value::Temporal(_) => "timestamp",
252 Value::Json(_) => "json",
253 Value::JsonB(_) => "jsonb",
254 Value::Array(_) => "anyarray",
255 Value::List(_) => "anyarray",
256 Value::Row(_) | Value::Record(_) => "record",
257 Value::Map(_) => "jsonb",
258 Value::Null => "unknown",
259 });
260 let (source, _) = split_type_modifier(source);
261 let source = source
262 .trim()
263 .to_ascii_lowercase()
264 .split_whitespace()
265 .collect::<Vec<_>>()
266 .join(" ");
267 let source = source.strip_prefix("pg_catalog.").unwrap_or(&source);
268 match source {
269 "smallint" | "int2" => "int2".into(),
270 "integer" | "int" | "int4" | "serial" | "serial4" => "int4".into(),
271 "bigint" | "int8" | "bigserial" | "serial8" => "int8".into(),
272 "character varying" | "varchar" => "varchar".into(),
273 "character" | "char" | "bpchar" => "bpchar".into(),
274 "boolean" | "bool" => "bool".into(),
275 "double" | "double precision" | "float8" => "float8".into(),
276 "real" | "float4" => "float4".into(),
277 other => other.into(),
278 }
279}
280
281fn cast_oid(value: &Value, source_ty: Option<&str>) -> Result<Value> {
282 let source = canonical_cast_source(source_ty, value);
283 match (source.as_str(), value) {
284 (
285 "unknown" | "text" | "varchar" | "bpchar" | "name",
286 Value::Str(text) | Value::FixedChar(text),
287 ) => parse_uint32_input(text, "oid"),
288 ("int2", Value::Int(value)) => {
289 let value = i16::try_from(*value).map_err(|_| out_of_range("smallint"))?;
290 Ok(Value::Int(i64::from(i32::from(value) as u32)))
291 }
292 ("int4", Value::Int(value)) => {
293 let value = i32::try_from(*value).map_err(|_| out_of_range("integer"))?;
294 Ok(Value::Int(i64::from(value as u32)))
295 }
296 ("int8", Value::Int(value)) => u32::try_from(*value)
297 .map(|value| Value::Int(i64::from(value)))
298 .map_err(|_| SQLError::Routine {
299 sqlstate: "22003".into(),
300 message: "OID out of range".into(),
301 }),
302 (
303 "oid" | "regclass" | "regcollation" | "regconfig" | "regdictionary" | "regnamespace"
304 | "regoper" | "regoperator" | "regproc" | "regprocedure" | "regrole" | "regtype",
305 Value::Int(value),
306 ) => u32::try_from(*value)
307 .map(|value| Value::Int(i64::from(value)))
308 .map_err(|_| out_of_range("oid")),
309 _ => Err(undefined_cast(&source, "oid")),
310 }
311}
312
313fn cast_regclass(value: &Value, source_ty: Option<&str>) -> Result<Value> {
314 let source = canonical_cast_source(source_ty, value);
315 match (source.as_str(), value) {
316 (
317 "unknown" | "text" | "varchar" | "bpchar" | "name" | "regclass",
318 Value::Str(text) | Value::FixedChar(text),
319 ) => Ok(Value::Str(text.clone())),
320 (_, Value::Int(_)) => cast_oid(value, source_ty),
321 _ => Err(undefined_cast(&source, "regclass")),
322 }
323}
324
325fn cast_regnamespace(value: &Value, source_ty: Option<&str>) -> Result<Value> {
326 let source = canonical_cast_source(source_ty, value);
327 match (source.as_str(), value) {
328 (
329 "unknown" | "text" | "varchar" | "bpchar" | "name" | "regnamespace",
330 Value::Str(text) | Value::FixedChar(text),
331 ) => Ok(Value::Str(text.clone())),
332 (_, Value::Int(_)) => cast_oid(value, source_ty),
333 _ => Err(undefined_cast(&source, "regnamespace")),
334 }
335}
336
337fn cast_xid(value: &Value, source_ty: Option<&str>) -> Result<Value> {
338 let source = canonical_cast_source(source_ty, value);
339 match (source.as_str(), value) {
340 (
341 "unknown" | "text" | "varchar" | "bpchar" | "name",
342 Value::Str(text) | Value::FixedChar(text),
343 ) => parse_uint32_input(text, "xid"),
344 ("xid", Value::Int(value)) => u32::try_from(*value)
345 .map(|value| Value::Int(i64::from(value)))
346 .map_err(|_| out_of_range("xid")),
347 _ => Err(undefined_cast(&source, "xid")),
348 }
349}
350
351fn cast_bytea(value: &Value, source_ty: Option<&str>) -> Result<Value> {
352 let source = canonical_cast_source(source_ty, value);
353 match (source.as_str(), value) {
354 ("bytea", Value::Bytes(bytes)) => Ok(Value::Bytes(bytes.clone())),
355 ("int2" | "int4" | "int8", Value::Int(value)) => integer_to_bytea(*value, Some(&source)),
356 (
357 "unknown" | "text" | "varchar" | "bpchar" | "name",
358 Value::Str(text) | Value::FixedChar(text),
359 ) => parse_bytea_input(text),
360 _ => Err(undefined_cast(&source, "bytea")),
361 }
362}
363
364fn parse_bytea_input(text: &str) -> Result<Value> {
365 if let Some(hex) = text.strip_prefix("\\x") {
366 if !hex.len().is_multiple_of(2) {
367 return Err(invalid_bytea(
368 "invalid hexadecimal data: odd number of digits",
369 ));
370 }
371 let mut bytes = Vec::with_capacity(hex.len() / 2);
372 for pair in hex.as_bytes().chunks_exact(2) {
373 let hi = (pair[0] as char)
374 .to_digit(16)
375 .ok_or_else(|| invalid_bytea("invalid hexadecimal digit"))?;
376 let lo = (pair[1] as char)
377 .to_digit(16)
378 .ok_or_else(|| invalid_bytea("invalid hexadecimal digit"))?;
379 bytes.push((hi * 16 + lo) as u8);
380 }
381 return Ok(Value::Bytes(bytes));
382 }
383
384 let input = text.as_bytes();
385 let mut output = Vec::with_capacity(input.len());
386 let mut index = 0;
387 while index < input.len() {
388 if input[index] != b'\\' {
389 output.push(input[index]);
390 index += 1;
391 continue;
392 }
393 if input.get(index + 1) == Some(&b'\\') {
394 output.push(b'\\');
395 index += 2;
396 continue;
397 }
398 let Some(octal) = input.get(index + 1..index + 4) else {
399 return Err(invalid_bytea("invalid input syntax for type bytea"));
400 };
401 if !matches!(octal[0], b'0'..=b'3')
402 || !octal[1..].iter().all(|byte| matches!(byte, b'0'..=b'7'))
403 {
404 return Err(invalid_bytea("invalid input syntax for type bytea"));
405 }
406 output.push((octal[0] - b'0') * 64 + (octal[1] - b'0') * 8 + (octal[2] - b'0'));
407 index += 4;
408 }
409 Ok(Value::Bytes(output))
410}
411
412fn invalid_bytea(message: &str) -> SQLError {
413 SQLError::Routine {
414 sqlstate: "22023".into(),
415 message: message.into(),
416 }
417}
418
419fn parse_uint32_input(text: &str, target: &str) -> Result<Value> {
420 let trimmed = text.trim();
421 let digits = trimmed
422 .strip_prefix('+')
423 .or_else(|| trimmed.strip_prefix('-'))
424 .unwrap_or(trimmed);
425 if digits.is_empty() || !digits.bytes().all(|byte| byte.is_ascii_digit()) {
426 return Err(SQLError::Routine {
427 sqlstate: "22P02".into(),
428 message: format!("invalid input syntax for type {target}: \"{text}\""),
429 });
430 }
431 let parsed = trimmed.parse::<i128>().map_err(|_| SQLError::Routine {
432 sqlstate: "22003".into(),
433 message: format!("value \"{text}\" is out of range for type {target}"),
434 })?;
435 if !((i128::from(i32::MIN))..=i128::from(u32::MAX)).contains(&parsed) {
436 return Err(SQLError::Routine {
437 sqlstate: "22003".into(),
438 message: format!("value \"{text}\" is out of range for type {target}"),
439 });
440 }
441 let value = if parsed < 0 {
442 u32::from_ne_bytes((parsed as i32).to_ne_bytes())
443 } else {
444 parsed as u32
445 };
446 Ok(Value::Int(i64::from(value)))
447}
448
449fn undefined_cast(source: &str, target: &str) -> SQLError {
450 SQLError::Routine {
451 sqlstate: "42846".into(),
452 message: format!("cannot cast type {source} to {target}"),
453 }
454}
455
456fn integer_to_bytea(value: i64, source_ty: Option<&str>) -> Result<Value> {
457 let source = source_ty
458 .map(split_type_modifier)
459 .map(|(base, _)| base)
460 .unwrap_or("integer");
461 let bytes = match source {
462 "smallint" | "int2" | "pg_catalog.int2" => i16::try_from(value)
463 .map(i16::to_be_bytes)
464 .map(|bytes| bytes.to_vec())
465 .map_err(|_| out_of_range("smallint"))?,
466 "bigint" | "int8" | "bigserial" | "serial8" | "pg_catalog.int8" => {
467 value.to_be_bytes().to_vec()
468 }
469 "integer" | "int" | "int4" | "serial" | "serial4" | "pg_catalog.int4" => {
470 i32::try_from(value)
471 .map(i32::to_be_bytes)
472 .map(|bytes| bytes.to_vec())
473 .map_err(|_| out_of_range("integer"))?
474 }
475 other => {
476 return Err(SQLError::TypeMismatch(format!(
477 "cannot cast {other} to bytea"
478 )));
479 }
480 };
481 Ok(Value::Bytes(bytes))
482}
483
484fn cast_uuid(value: &Value) -> Result<Value> {
485 let text = match value {
486 Value::Str(text) | Value::FixedChar(text) => text,
487 other => {
488 return Err(SQLError::TypeMismatch(format!(
489 "cannot cast {other:?} to uuid"
490 )))
491 }
492 };
493 let digits = text
494 .strip_prefix('{')
495 .and_then(|text| text.strip_suffix('}'))
496 .unwrap_or(text);
497 if digits.starts_with('{') || digits.ends_with('}') {
498 return Err(invalid_uuid(text));
499 }
500 let mut normalized = String::with_capacity(32);
501 let mut group_digits = 0_usize;
502 for character in digits.chars() {
503 if character == '-' {
504 if group_digits == 0 || !group_digits.is_multiple_of(4) {
505 return Err(invalid_uuid(text));
506 }
507 group_digits = 0;
508 continue;
509 }
510 if !character.is_ascii_hexdigit() {
511 return Err(invalid_uuid(text));
512 }
513 normalized.push(character.to_ascii_lowercase());
514 group_digits += 1;
515 }
516 if normalized.len() != 32 || group_digits == 0 {
517 return Err(invalid_uuid(text));
518 }
519 Ok(Value::Str(format!(
520 "{}-{}-{}-{}-{}",
521 &normalized[0..8],
522 &normalized[8..12],
523 &normalized[12..16],
524 &normalized[16..20],
525 &normalized[20..32]
526 )))
527}
528
529fn invalid_uuid(text: &str) -> SQLError {
530 SQLError::Routine {
531 sqlstate: "22P02".into(),
532 message: format!("invalid input syntax for type uuid: \"{text}\""),
533 }
534}
535
536pub(super) fn split_type_modifier(ty: &str) -> (&str, Option<&str>) {
538 match (ty.find('('), ty.rfind(')')) {
539 (Some(open), Some(close)) if close > open => {
540 (ty[..open].trim_end(), Some(&ty[open + 1..close]))
541 }
542 _ => (ty, None),
543 }
544}
545
546pub(super) fn cast_integer(v: &Value, target: &str) -> Result<Value> {
551 let n: i64 = match v {
552 Value::Int(n) => *n,
553 Value::Bool(b) => i64::from(*b),
554 Value::Float(f) => {
555 if !f.is_finite() {
556 return Err(out_of_range(target));
557 }
558 let rounded = f.round_ties_even();
559 if rounded < i64::MIN as f64 || rounded >= 9_223_372_036_854_775_808.0 {
563 return Err(out_of_range(target));
564 }
565 rounded as i64
566 }
567 Value::Decimal(d) => d
568 .round_dp(0)
569 .to_i64_trunc()
570 .ok_or_else(|| out_of_range(target))?,
571 Value::Str(s) | Value::FixedChar(s) => {
572 s.trim().parse::<i64>().map_err(|_| SQLError::Routine {
573 sqlstate: "22P02".into(),
574 message: format!("invalid input syntax for type {target}: \"{s}\""),
575 })?
576 }
577 Value::Bytes(bytes) => bytea_to_integer(bytes, target)?,
578 other => {
579 return Err(SQLError::TypeMismatch(format!(
580 "cannot cast {other:?} to {target}"
581 )));
582 }
583 };
584 let in_range = match target {
585 "smallint" => i16::try_from(n).is_ok(),
586 "integer" => i32::try_from(n).is_ok(),
587 _ => true,
588 };
589 if !in_range {
590 return Err(out_of_range(target));
591 }
592 Ok(Value::Int(n))
593}
594
595fn bytea_to_integer(bytes: &[u8], target: &str) -> Result<i64> {
596 let width = match target {
597 "smallint" => 2,
598 "integer" => 4,
599 _ => 8,
600 };
601 if bytes.len() > width {
602 return Err(out_of_range(target));
603 }
604 let mut extended = [0_u8; 8];
605 let offset = width - bytes.len();
606 extended[8 - width + offset..].copy_from_slice(bytes);
607 Ok(match width {
608 2 => i64::from(i16::from_be_bytes([extended[6], extended[7]])),
609 4 => i64::from(i32::from_be_bytes([
610 extended[4],
611 extended[5],
612 extended[6],
613 extended[7],
614 ])),
615 _ => i64::from_be_bytes(extended),
616 })
617}
618
619pub(super) fn cast_boolean(v: &Value) -> Result<Value> {
623 match v {
624 Value::Bool(b) => Ok(Value::Bool(*b)),
625 Value::Int(n) => Ok(Value::Bool(*n != 0)),
626 Value::Float(f) => Ok(Value::Bool(*f != 0.0)),
627 Value::Decimal(d) => Ok(Value::Bool(!d.is_zero())),
628 Value::Str(s) | Value::FixedChar(s) => {
629 let text = s.trim().to_ascii_lowercase();
630 let matches_prefix = |word: &str| !text.is_empty() && word.starts_with(&text);
631 let value = if matches_prefix("true") || matches_prefix("yes") || text == "1" {
632 Some(true)
633 } else if matches_prefix("false") || matches_prefix("no") || text == "0" {
634 Some(false)
635 } else if "on" == text {
636 Some(true)
637 } else if matches_prefix("off") && text.len() >= 2 {
638 Some(false)
639 } else {
640 None
641 };
642 value.map(Value::Bool).ok_or_else(|| SQLError::Routine {
643 sqlstate: "22P02".into(),
644 message: format!("invalid input syntax for type boolean: \"{s}\""),
645 })
646 }
647 other => Err(SQLError::TypeMismatch(format!(
648 "cannot cast {other:?} to boolean"
649 ))),
650 }
651}
652
653pub fn parse_pg_array_literal(text: &str) -> Result<ArrayValue> {
657 let mut parser = PgArrayLiteralParser::new(text);
658 let (declared_dimensions, items) = parser.parse()?;
659 if let Err(error) = array_shape(&items) {
660 return Err(SQLError::Routine {
661 sqlstate: "22P02".into(),
662 message: format!("malformed array literal: \"{text}\" ({})", error.message()),
663 });
664 }
665 let array = ArrayValue::try_new(items).ok_or_else(|| SQLError::Routine {
666 sqlstate: "22P02".into(),
667 message: format!("malformed array literal: \"{text}\""),
668 })?;
669 let Some(declared_dimensions) = declared_dimensions else {
670 return Ok(array);
671 };
672 let declared_lengths = declared_dimensions
673 .iter()
674 .map(|(_, length)| *length)
675 .collect::<Vec<_>>();
676 if declared_lengths != array.dimensions() {
677 return Err(SQLError::Routine {
678 sqlstate: "22P02".into(),
679 message: format!(
680 "malformed array literal: \"{text}\" (specified array dimensions do not match array contents)"
681 ),
682 });
683 }
684 let lower_bounds = declared_dimensions
685 .into_iter()
686 .map(|(lower, _)| lower)
687 .collect();
688 ArrayValue::with_lower_bounds(array.into_elements(), lower_bounds).ok_or_else(|| {
689 SQLError::Routine {
690 sqlstate: "22P02".into(),
691 message: format!("malformed array literal: \"{text}\""),
692 }
693 })
694}
695
696fn cast_array_elements(
697 items: &[Value],
698 element_type: &str,
699 source_element_type: Option<&str>,
700) -> Result<Vec<Value>> {
701 items
702 .iter()
703 .map(|item| match item {
704 Value::List(nested) => {
705 cast_array_elements(nested, element_type, source_element_type).map(Value::List)
706 }
707 other => cast_value_from(other, element_type, source_element_type),
708 })
709 .collect()
710}
711
712pub(super) struct PgArrayLiteralParser<'a> {
713 source: &'a str,
714 chars: std::iter::Peekable<std::str::Chars<'a>>,
715}
716
717type ParsedArrayLiteral = (Option<Vec<(i32, usize)>>, Vec<Value>);
718
719impl<'a> PgArrayLiteralParser<'a> {
720 fn new(source: &'a str) -> Self {
721 Self {
722 source,
723 chars: source.chars().peekable(),
724 }
725 }
726
727 fn parse(&mut self) -> Result<ParsedArrayLiteral> {
728 self.skip_whitespace();
729 let dimensions = self.parse_dimension_declaration()?;
730 let items = self.parse_array()?;
731 self.skip_whitespace();
732 if self.chars.peek().is_some() {
733 return Err(self.error("unexpected content after closing brace"));
734 }
735 Ok((dimensions, items))
736 }
737
738 fn parse_dimension_declaration(&mut self) -> Result<Option<Vec<(i32, usize)>>> {
739 if self.chars.peek() != Some(&'[') {
740 return Ok(None);
741 }
742 let mut dimensions = Vec::new();
743 while self.chars.next_if_eq(&'[').is_some() {
744 self.skip_whitespace();
745 let lower = self.parse_dimension_bound()?;
746 self.skip_whitespace();
747 if self.chars.next() != Some(':') {
748 return Err(self.error("array dimension must contain `:`"));
749 }
750 self.skip_whitespace();
751 let upper = self.parse_dimension_bound()?;
752 self.skip_whitespace();
753 if self.chars.next() != Some(']') {
754 return Err(self.error("array dimension is missing a closing `]`"));
755 }
756 if upper == i32::MAX {
757 return Err(SQLError::Routine {
758 sqlstate: "54000".into(),
759 message: format!("array upper bound is too large: {upper}"),
760 });
761 }
762 if upper < lower {
763 return Err(SQLError::Routine {
764 sqlstate: "2202E".into(),
765 message: "upper bound cannot be less than lower bound".into(),
766 });
767 }
768 let length = i64::from(upper)
769 .checked_sub(i64::from(lower))
770 .and_then(|difference| difference.checked_add(1))
771 .and_then(|length| usize::try_from(length).ok())
772 .ok_or_else(|| self.error("array dimension is out of range"))?;
773 dimensions.push((lower, length));
774 self.skip_whitespace();
775 }
776 if self.chars.next() != Some('=') {
777 return Err(self.error("array dimensions must be followed by `=`"));
778 }
779 self.skip_whitespace();
780 Ok(Some(dimensions))
781 }
782
783 fn parse_dimension_bound(&mut self) -> Result<i32> {
784 let mut text = String::new();
785 if self
786 .chars
787 .peek()
788 .is_some_and(|character| matches!(character, '+' | '-'))
789 {
790 text.push(self.chars.next().expect("peeked array bound sign"));
791 }
792 while self.chars.peek().is_some_and(char::is_ascii_digit) {
793 text.push(self.chars.next().expect("peeked array bound digit"));
794 }
795 if text.is_empty() || matches!(text.as_str(), "+" | "-") {
796 return Err(self.error("array dimension bound must be an integer"));
797 }
798 text.parse()
799 .map_err(|_| self.error("array dimension bound is out of range"))
800 }
801
802 fn parse_array(&mut self) -> Result<Vec<Value>> {
803 if self.chars.next() != Some('{') {
804 return Err(self.error("array value must start with `{`"));
805 }
806 self.skip_whitespace();
807 if self.chars.next_if_eq(&'}').is_some() {
808 return Ok(Vec::new());
809 }
810
811 let mut items = Vec::new();
812 loop {
813 self.skip_whitespace();
814 items.push(self.parse_element()?);
815 self.skip_whitespace();
816 match self.chars.next() {
817 Some(',') => {
818 self.skip_whitespace();
819 if matches!(self.chars.peek(), None | Some('}')) {
820 return Err(self.error("array contains a missing element"));
821 }
822 }
823 Some('}') => break,
824 Some(_) => {
825 return Err(self.error("array elements must be separated by commas"));
826 }
827 None => return Err(self.error("array is missing a closing `}`")),
828 }
829 }
830 Ok(items)
831 }
832
833 fn parse_element(&mut self) -> Result<Value> {
834 match self.chars.peek() {
835 Some('{') => self.parse_array().map(Value::List),
836 Some('"') => self.parse_quoted_element().map(Value::Str),
837 Some(',') | Some('}') | None => Err(self.error("array contains a missing element")),
838 Some(_) => self.parse_unquoted_element(),
839 }
840 }
841
842 fn parse_quoted_element(&mut self) -> Result<String> {
843 let _opening_quote = self.chars.next();
844 let mut value = String::new();
845 loop {
846 match self.chars.next() {
847 Some('"') => return Ok(value),
848 Some('\\') => value.push(
849 self.chars
850 .next()
851 .ok_or_else(|| self.error("quoted element ends with an escape"))?,
852 ),
853 Some(character) => value.push(character),
854 None => return Err(self.error("array contains an unterminated quoted element")),
855 }
856 }
857 }
858
859 fn parse_unquoted_element(&mut self) -> Result<Value> {
860 let mut value = String::new();
861 let mut significant_len = 0;
862 let mut was_escaped = false;
863 while let Some(character) = self.chars.peek().copied() {
864 match character {
865 ',' | '}' => break,
866 '{' | '"' => {
867 return Err(self.error("array contains an unescaped special character"));
868 }
869 '\\' => {
870 let _escape = self.chars.next();
871 let escaped = self
872 .chars
873 .next()
874 .ok_or_else(|| self.error("array element ends with an escape"))?;
875 value.push(escaped);
876 significant_len = value.len();
877 was_escaped = true;
878 }
879 _ => {
880 let _character = self.chars.next();
881 value.push(character);
882 if !character.is_whitespace() {
883 significant_len = value.len();
884 }
885 }
886 }
887 }
888 value.truncate(significant_len);
889 if value.is_empty() {
890 return Err(self.error("array contains a missing element"));
891 }
892 if !was_escaped && value.eq_ignore_ascii_case("null") {
893 Ok(Value::Null)
894 } else {
895 Ok(Value::Str(value))
896 }
897 }
898
899 fn skip_whitespace(&mut self) {
900 while self
901 .chars
902 .next_if(|character| character.is_whitespace())
903 .is_some()
904 {}
905 }
906
907 fn error(&self, detail: &str) -> SQLError {
908 SQLError::Routine {
909 sqlstate: "22P02".into(),
910 message: format!("malformed array literal: \"{}\" ({detail})", self.source),
911 }
912 }
913}
914
915#[derive(Clone, Copy, Debug, PartialEq, Eq)]
916pub(super) enum ArrayShapeError {
917 MixedNesting,
918 MismatchedDimensions,
919}
920
921impl ArrayShapeError {
922 fn message(self) -> &'static str {
923 match self {
924 Self::MixedNesting => "cannot mix nested arrays and scalar elements",
925 Self::MismatchedDimensions => "multidimensional arrays must have matching dimensions",
926 }
927 }
928}
929
930pub(super) fn array_shape(items: &[Value]) -> std::result::Result<Vec<usize>, ArrayShapeError> {
931 let mut dimensions = vec![items.len()];
932 let mut nested_shape: Option<Vec<usize>> = None;
933 let mut has_scalar = false;
934 for item in items {
935 if let Value::List(nested) = item {
936 let shape = array_shape(nested)?;
937 if has_scalar {
938 return Err(ArrayShapeError::MixedNesting);
939 }
940 if nested_shape
941 .as_ref()
942 .is_some_and(|expected| *expected != shape)
943 {
944 return Err(ArrayShapeError::MismatchedDimensions);
945 }
946 nested_shape = Some(shape);
947 } else {
948 if nested_shape.is_some() {
949 return Err(ArrayShapeError::MixedNesting);
950 }
951 has_scalar = true;
952 }
953 }
954 if let Some(shape) = nested_shape {
955 dimensions.extend(shape);
956 }
957 Ok(dimensions)
958}
959
960pub fn array_dimensions(items: &[Value]) -> Result<Vec<usize>> {
965 array_shape(items).map_err(|error| SQLError::TypeMismatch(error.message().to_string()))
966}
967
968#[derive(Clone, Copy)]
969pub(super) enum TemporalCastTarget {
970 Date,
971 Time,
972 TimeTz,
973 Timestamp,
974 TimestampTz,
975 Interval,
976}
977
978pub(super) fn cast_temporal(
979 v: &Value,
980 target: TemporalCastTarget,
981 parse: fn(&str) -> Option<TemporalValue>,
982 ty: &str,
983) -> Result<Value> {
984 match v {
985 Value::Temporal(value) => cast_temporal_kind(value, target)
986 .map(Value::Temporal)
987 .ok_or_else(|| SQLError::TypeMismatch(format!("cannot cast {v:?} to {ty}"))),
988 other => parse(&value_to_string(other))
989 .map(Value::Temporal)
990 .ok_or_else(|| SQLError::TypeMismatch(format!("cannot cast {v:?} to {ty}"))),
991 }
992}
993
994fn cast_date(v: &Value, source_ty: Option<&str>) -> Result<Value> {
995 match v {
996 Value::Temporal(value) => cast_temporal_kind(value, TemporalCastTarget::Date)
997 .map(Value::Temporal)
998 .ok_or_else(|| undefined_cast(&canonical_cast_source(source_ty, v), "date")),
999 Value::Str(text) | Value::FixedChar(text) => TemporalValue::try_parse_date(text)
1000 .map(Value::Temporal)
1001 .map_err(|error| {
1002 let field_overflow = matches!(
1003 error.kind(),
1004 chrono::format::ParseErrorKind::OutOfRange
1005 | chrono::format::ParseErrorKind::Impossible
1006 );
1007 SQLError::Routine {
1008 sqlstate: if field_overflow { "22008" } else { "22007" }.into(),
1009 message: if field_overflow {
1010 format!("date/time field value out of range: \"{text}\"")
1011 } else {
1012 format!("invalid input syntax for type date: \"{text}\"")
1013 },
1014 }
1015 }),
1016 _ => Err(undefined_cast(&canonical_cast_source(source_ty, v), "date")),
1017 }
1018}
1019
1020fn cast_temporal_kind(value: &TemporalValue, target: TemporalCastTarget) -> Option<TemporalValue> {
1021 const MICROS_PER_DAY: i64 = 86_400_000_000;
1022 match (target, value) {
1023 (TemporalCastTarget::Date, TemporalValue::Date { days }) => {
1024 Some(TemporalValue::Date { days: *days })
1025 }
1026 (
1027 TemporalCastTarget::Date,
1028 TemporalValue::Timestamp { micros } | TemporalValue::TimestampTz { micros },
1029 ) => Some(TemporalValue::Date {
1030 days: i32::try_from(micros.div_euclid(MICROS_PER_DAY)).ok()?,
1031 }),
1032 (TemporalCastTarget::Time, TemporalValue::Time { micros })
1033 | (TemporalCastTarget::Time, TemporalValue::TimeTz { micros, .. })
1034 | (
1035 TemporalCastTarget::Time,
1036 TemporalValue::Timestamp { micros } | TemporalValue::TimestampTz { micros },
1037 )
1038 | (TemporalCastTarget::Time, TemporalValue::Interval { micros, .. }) => {
1039 Some(TemporalValue::Time {
1040 micros: micros.rem_euclid(MICROS_PER_DAY),
1041 })
1042 }
1043 (
1044 TemporalCastTarget::TimeTz,
1045 TemporalValue::TimeTz {
1046 micros,
1047 offset_minutes,
1048 },
1049 ) => Some(TemporalValue::TimeTz {
1050 micros: *micros,
1051 offset_minutes: *offset_minutes,
1052 }),
1053 (TemporalCastTarget::TimeTz, TemporalValue::Time { micros })
1054 | (TemporalCastTarget::TimeTz, TemporalValue::TimestampTz { micros }) => {
1055 Some(TemporalValue::TimeTz {
1056 micros: micros.rem_euclid(MICROS_PER_DAY),
1057 offset_minutes: 0,
1058 })
1059 }
1060 (TemporalCastTarget::Timestamp, TemporalValue::Timestamp { micros })
1061 | (TemporalCastTarget::Timestamp, TemporalValue::TimestampTz { micros }) => {
1062 Some(TemporalValue::Timestamp { micros: *micros })
1063 }
1064 (TemporalCastTarget::Timestamp, TemporalValue::Date { days }) => {
1065 Some(TemporalValue::Timestamp {
1066 micros: i64::from(*days).checked_mul(MICROS_PER_DAY)?,
1067 })
1068 }
1069 (TemporalCastTarget::TimestampTz, TemporalValue::TimestampTz { micros })
1070 | (TemporalCastTarget::TimestampTz, TemporalValue::Timestamp { micros }) => {
1071 Some(TemporalValue::TimestampTz { micros: *micros })
1072 }
1073 (TemporalCastTarget::TimestampTz, TemporalValue::Date { days }) => {
1074 Some(TemporalValue::TimestampTz {
1075 micros: i64::from(*days).checked_mul(MICROS_PER_DAY)?,
1076 })
1077 }
1078 (
1079 TemporalCastTarget::Interval,
1080 TemporalValue::Interval {
1081 months,
1082 days,
1083 micros,
1084 },
1085 ) => Some(TemporalValue::Interval {
1086 months: *months,
1087 days: *days,
1088 micros: *micros,
1089 }),
1090 (TemporalCastTarget::Interval, TemporalValue::Time { micros }) => {
1091 Some(TemporalValue::Interval {
1092 months: 0,
1093 days: 0,
1094 micros: *micros,
1095 })
1096 }
1097 _ => None,
1098 }
1099}
1100
1101#[cfg(test)]
1102mod tests {
1103 use super::*;
1104
1105 #[test]
1106 fn array_literal_rejects_postgresql_unrepresentable_upper_bound() {
1107 let error = parse_pg_array_literal("[2147483647:2147483647]={1}").unwrap_err();
1108 assert_eq!(error.sqlstate(), Some("54000"));
1109 assert_eq!(
1110 error.to_string(),
1111 "array upper bound is too large: 2147483647"
1112 );
1113 assert!(parse_pg_array_literal("[2147483646:2147483646]={1}").is_ok());
1114 }
1115 use uqa_core::DecimalValue;
1116
1117 #[test]
1118 fn temporal_cross_casts_convert_the_carrier_kind() {
1119 let date = Value::Temporal(TemporalValue::parse_date("2020-01-02").unwrap());
1120 assert_eq!(
1121 cast_value(&date, "timestamp").unwrap(),
1122 Value::Temporal(TemporalValue::parse_timestamp("2020-01-02 00:00:00").unwrap())
1123 );
1124 let timestamp =
1125 Value::Temporal(TemporalValue::parse_timestamp("2020-01-02 03:04:05").unwrap());
1126 assert_eq!(
1127 cast_value(×tamp, "date").unwrap(),
1128 Value::Temporal(TemporalValue::parse_date("2020-01-02").unwrap())
1129 );
1130 assert_eq!(
1131 cast_value(×tamp, "time").unwrap(),
1132 Value::Temporal(TemporalValue::parse_time("03:04:05").unwrap())
1133 );
1134 let interval = Value::Temporal(TemporalValue::parse_interval("1 day 25:02:03").unwrap());
1135 assert_eq!(
1136 cast_value(&interval, "time").unwrap(),
1137 Value::Temporal(TemporalValue::parse_time("01:02:03").unwrap())
1138 );
1139 }
1140
1141 #[test]
1142 fn uuid_cast_matches_postgresql_input_and_canonical_output() {
1143 for input in [
1144 "A0EEBC99-9C0B-4EF8-BB6D-6BB9BD380A11",
1145 "a0eebc999c0b4ef8bb6d6bb9bd380a11",
1146 "{a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11}",
1147 "a0ee-bc99-9c0b-4ef8-bb6d-6bb9-bd38-0a11",
1148 ] {
1149 assert_eq!(
1150 cast_value(&Value::Str(input.into()), "uuid").unwrap(),
1151 Value::Str("a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11".into())
1152 );
1153 }
1154 }
1155
1156 #[test]
1157 fn uuid_cast_rejects_postgresql_invalid_forms() {
1158 for input in [
1159 " a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11 ",
1160 "a0e-ebc99-9c0b-4ef8-bb6d-6bb9bd380a11",
1161 "not-a-uuid",
1162 ] {
1163 let error = cast_value(&Value::Str(input.into()), "uuid").unwrap_err();
1164 assert_eq!(error.sqlstate(), Some("22P02"));
1165 }
1166 }
1167
1168 #[test]
1169 fn oid_cast_preserves_postgresql_source_type_rules() {
1170 assert_eq!(
1171 cast_value_from(&Value::Int(-1), "oid", Some("smallint")).unwrap(),
1172 Value::Int(i64::from(u32::MAX))
1173 );
1174 assert_eq!(
1175 cast_value_from(&Value::Int(-1), "oid", Some("integer")).unwrap(),
1176 Value::Int(i64::from(u32::MAX))
1177 );
1178 assert_eq!(
1179 cast_value_from(&Value::Int(i64::from(u32::MAX)), "oid", Some("bigint")).unwrap(),
1180 Value::Int(i64::from(u32::MAX))
1181 );
1182 let error = cast_value_from(&Value::Int(-1), "oid", Some("bigint")).unwrap_err();
1183 assert_eq!(error.sqlstate(), Some("22003"));
1184 assert_eq!(error.to_string(), "OID out of range");
1185 for source in ["boolean", "numeric", "double precision"] {
1186 let value = match source {
1187 "boolean" => Value::Bool(true),
1188 "numeric" => Value::Decimal(DecimalValue::from_i64(1)),
1189 _ => Value::Float(1.0),
1190 };
1191 let error = cast_value_from(&value, "oid", Some(source)).unwrap_err();
1192 assert_eq!(error.sqlstate(), Some("42846"));
1193 }
1194 }
1195
1196 #[test]
1197 fn regclass_cast_preserves_bound_relation_names_and_oid_carriers() {
1198 assert_eq!(
1199 cast_value_from(
1200 &Value::Str("app.items".into()),
1201 "pg_catalog.regclass",
1202 Some("unknown")
1203 )
1204 .unwrap(),
1205 Value::Str("app.items".into())
1206 );
1207 assert_eq!(
1208 cast_value_from(&Value::Int(2205), "regclass", Some("oid")).unwrap(),
1209 Value::Int(2205)
1210 );
1211 }
1212
1213 #[test]
1214 fn oid_and_xid_text_input_use_postgresql_uint32_syntax() {
1215 for target in ["oid", "xid"] {
1216 assert_eq!(
1217 cast_value(&Value::Str("-1".into()), target).unwrap(),
1218 Value::Int(i64::from(u32::MAX))
1219 );
1220 assert_eq!(
1221 cast_value(&Value::Str(i32::MIN.to_string()), target).unwrap(),
1222 Value::Int(i64::from(i32::MIN as u32))
1223 );
1224 assert_eq!(
1225 cast_value(&Value::Str(u32::MAX.to_string()), target).unwrap(),
1226 Value::Int(i64::from(u32::MAX))
1227 );
1228 for input in ["-2147483649", "4294967296"] {
1229 let error = cast_value(&Value::Str(input.into()), target).unwrap_err();
1230 assert_eq!(error.sqlstate(), Some("22003"));
1231 }
1232 let error = cast_value(&Value::Str("1.0".into()), target).unwrap_err();
1233 assert_eq!(error.sqlstate(), Some("22P02"));
1234 }
1235 }
1236
1237 #[test]
1238 fn xid_rejects_integer_and_oid_cast_sources() {
1239 for source in ["smallint", "integer", "bigint", "oid"] {
1240 let error = cast_value_from(&Value::Int(1), "xid", Some(source)).unwrap_err();
1241 assert_eq!(error.sqlstate(), Some("42846"));
1242 }
1243 }
1244
1245 #[test]
1246 fn bytea_cast_preserves_postgresql_source_type_and_input_rules() {
1247 assert_eq!(
1248 cast_value_from(&Value::Int(-1), "bytea", Some("smallint")).unwrap(),
1249 Value::Bytes(vec![0xff, 0xff])
1250 );
1251 assert_eq!(
1252 cast_value_from(&Value::Int(-1), "bytea", Some("integer")).unwrap(),
1253 Value::Bytes(vec![0xff; 4])
1254 );
1255 assert_eq!(
1256 cast_value_from(&Value::Int(-1), "bytea", Some("bigint")).unwrap(),
1257 Value::Bytes(vec![0xff; 8])
1258 );
1259 assert_eq!(
1260 cast_value_from(&Value::Str("\\x6162".into()), "bytea", Some("text")).unwrap(),
1261 Value::Bytes(b"ab".to_vec())
1262 );
1263 assert_eq!(
1264 cast_value_from(&Value::Str("a\\\\b\\141".into()), "bytea", Some("text")).unwrap(),
1265 Value::Bytes(b"a\\ba".to_vec())
1266 );
1267 for (value, source) in [
1268 (Value::Bool(true), "boolean"),
1269 (Value::Decimal(DecimalValue::from_i64(1)), "numeric"),
1270 (Value::Float(1.0), "double precision"),
1271 ] {
1272 let error = cast_value_from(&value, "bytea", Some(source)).unwrap_err();
1273 assert_eq!(error.sqlstate(), Some("42846"));
1274 }
1275 for input in ["\\x1", "\\xzz", "\\9"] {
1276 let error = cast_value(&Value::Str(input.into()), "bytea").unwrap_err();
1277 assert_eq!(error.sqlstate(), Some("22023"));
1278 }
1279 }
1280
1281 #[test]
1282 fn unary_minus_preserves_integer_width_and_overflow() {
1283 for (source, input, expected) in [
1284 ("smallint", 1_i64, -1_i64),
1285 ("integer", 1_i64, -1_i64),
1286 ("bigint", 1_i64, -1_i64),
1287 ] {
1288 assert_eq!(
1289 negate_value(&Value::Int(input), Some(source)).unwrap(),
1290 Value::Int(expected)
1291 );
1292 }
1293 for (source, minimum) in [
1294 ("smallint", i64::from(i16::MIN)),
1295 ("integer", i64::from(i32::MIN)),
1296 ("bigint", i64::MIN),
1297 ] {
1298 let error = negate_value(&Value::Int(minimum), Some(source)).unwrap_err();
1299 assert_eq!(error.sqlstate(), Some("22003"));
1300 }
1301 }
1302
1303 #[test]
1304 fn unary_minus_preserves_interval_fields() {
1305 assert_eq!(
1306 negate_value(
1307 &Value::Temporal(TemporalValue::Interval {
1308 months: 2,
1309 days: -3,
1310 micros: 4,
1311 }),
1312 Some("interval"),
1313 )
1314 .unwrap(),
1315 Value::Temporal(TemporalValue::Interval {
1316 months: -2,
1317 days: 3,
1318 micros: -4,
1319 })
1320 );
1321 }
1322}