1use super::AssignmentContext;
10use crate::expr::{value_to_tensor, value_to_vector};
11use crate::{ColumnType, SQLError};
12use uqa_core::{ArrayValue, DecimalValue, TemporalValue, Value};
13
14pub fn coerce_assignment_value(
15 context: &dyn AssignmentContext,
16 value: Value,
17 target: &ColumnType,
18 source: Option<&ColumnType>,
19) -> Result<Value, SQLError> {
20 if source.is_some_and(|source| same_domain_identity(source, target)) {
21 return Ok(value);
22 }
23 let value = if target.is_character_string() {
24 source
25 .map(|source| crate::expr::format_regtype_value(&value, source, Some(context)))
26 .transpose()?
27 .flatten()
28 .map(Value::Str)
29 .unwrap_or(value)
30 } else {
31 value
32 };
33 convert_value_to_column_type_with_context(context, value, target)
34}
35
36fn same_domain_identity(source: &ColumnType, target: &ColumnType) -> bool {
37 match (source, target) {
38 (ColumnType::Domain { oid: source, .. }, ColumnType::Domain { oid: target, .. }) => {
39 source == target
40 }
41 (ColumnType::Array(source), ColumnType::Array(target)) => {
42 same_domain_identity(source, target)
43 }
44 _ => false,
45 }
46}
47
48pub fn coerce_json_value(value: Value, jsonb: bool) -> Result<Value, SQLError> {
49 crate::expr::cast_value(&value, if jsonb { "jsonb" } else { "json" })
50}
51
52pub fn convert_declared_value_to_column_type(
53 context: &dyn AssignmentContext,
54 value: Value,
55 source_ty: &ColumnType,
56 target_ty: &ColumnType,
57) -> Result<Value, SQLError> {
58 match (source_ty, target_ty) {
59 (ColumnType::Domain { base, .. }, target) => {
60 convert_declared_value_to_column_type(context, value, base, target)
61 }
62 (source, ColumnType::Domain { base, .. }) => {
63 convert_declared_value_to_column_type(context, value, source, base)
64 }
65 (ColumnType::Array(source), ColumnType::Array(target)) => {
66 let Value::Array(array) = value else {
67 return Err(SQLError::TypeMismatch(format!(
68 "cannot cast a non-array value to {}[]",
69 column_type_name(target)
70 )));
71 };
72 let source = array_scalar_type(source);
73 let target = array_scalar_type(target);
74 let converted =
75 convert_declared_array_elements(context, array.elements(), source, target)?;
76 ArrayValue::with_lower_bounds(converted, array.lower_bounds().to_vec())
77 .map(Value::Array)
78 .ok_or_else(|| {
79 SQLError::TypeMismatch(
80 "multidimensional arrays must have matching dimensions".into(),
81 )
82 })
83 }
84 (ColumnType::Range(source), ColumnType::Range(target)) if source == target => {
85 crate::expr::cast_value_from(&value, target.range_name(), Some(source.range_name()))
86 }
87 (ColumnType::Range(source), ColumnType::Multirange(target)) if source == target => {
88 crate::expr::cast_value_from(
89 &value,
90 target.multirange_name(),
91 Some(source.range_name()),
92 )
93 }
94 (ColumnType::Multirange(source), ColumnType::Multirange(target)) if source == target => {
95 crate::expr::cast_value_from(
96 &value,
97 target.multirange_name(),
98 Some(source.multirange_name()),
99 )
100 }
101 (_, ColumnType::Range(_) | ColumnType::Multirange(_)) => {
102 Err(SQLError::TypeMismatch(format!(
103 "column cannot be cast automatically from type {} to type {}",
104 column_type_name(source_ty),
105 column_type_name(target_ty)
106 )))
107 }
108 (source, ColumnType::Oid)
109 if matches!(
110 source,
111 ColumnType::SmallInteger
112 | ColumnType::Integer
113 | ColumnType::BigInteger
114 | ColumnType::Oid
115 | ColumnType::Regproc
116 | ColumnType::Regprocedure
117 | ColumnType::Regclass
118 | ColumnType::Regnamespace
119 | ColumnType::Regrole
120 | ColumnType::Regtype
121 ) =>
122 {
123 crate::expr::cast_value_from(&value, "oid", Some(column_type_name(source)))
124 }
125 (ColumnType::Xid, ColumnType::Xid) => Ok(value),
126 (ColumnType::Bytea, ColumnType::Bytea) => Ok(value),
127 (_, ColumnType::Oid | ColumnType::Xid | ColumnType::Bytea) => {
128 Err(SQLError::TypeMismatch(format!(
129 "column cannot be cast automatically from type {} to type {}",
130 column_type_name(source_ty),
131 column_type_name(target_ty)
132 )))
133 }
134 _ => convert_value_to_column_type_with_context(context, value, target_ty),
135 }
136}
137
138fn type_requires_catalog_resolution(ty: &ColumnType) -> bool {
139 match ty {
140 ColumnType::Regrole | ColumnType::Domain { .. } => true,
141 ColumnType::Array(element) => type_requires_catalog_resolution(element),
142 _ => false,
143 }
144}
145
146pub fn convert_value_to_column_type_with_context(
147 context: &dyn AssignmentContext,
148 value: Value,
149 ty: &ColumnType,
150) -> Result<Value, SQLError> {
151 if let Some(value) = super::domain::assign_domain_value(context, &value, ty)? {
152 return Ok(value);
153 }
154 if matches!(value, Value::Null) {
155 return Ok(Value::Null);
156 }
157 if let ColumnType::Array(element) = ty {
158 if type_requires_catalog_resolution(element) {
159 return convert_catalog_array(context, value, element);
160 }
161 }
162 if type_requires_catalog_resolution(ty) {
163 return crate::expr::cast_value_with_type_resolution(
164 &value,
165 None,
166 &ty.sql_name(),
167 Some(context),
168 );
169 }
170 convert_value_to_column_type(value, ty)
171}
172
173fn convert_catalog_array(
174 context: &dyn AssignmentContext,
175 value: Value,
176 element: &ColumnType,
177) -> Result<Value, SQLError> {
178 let array = match value {
179 Value::Array(array) => array,
180 Value::Str(text) => crate::expr::parse_pg_array_literal(&text)?,
181 other => {
182 return Err(SQLError::TypeMismatch(format!(
183 "expected an array, got {other:?}"
184 )))
185 }
186 };
187 let values = convert_catalog_array_elements(context, array.elements(), element)?;
188 ArrayValue::with_lower_bounds(values, array.lower_bounds().to_vec())
189 .map(Value::Array)
190 .ok_or_else(|| {
191 SQLError::TypeMismatch("multidimensional arrays must have matching dimensions".into())
192 })
193}
194
195fn convert_catalog_array_elements(
196 context: &dyn AssignmentContext,
197 values: &[Value],
198 element: &ColumnType,
199) -> Result<Vec<Value>, SQLError> {
200 let mut element = element;
201 while let ColumnType::Array(nested) = element {
202 element = nested;
203 }
204 values
205 .iter()
206 .map(|value| match value {
207 Value::List(values) => {
208 convert_catalog_array_elements(context, values, element).map(Value::List)
209 }
210 value => convert_value_to_column_type_with_context(context, value.clone(), element),
211 })
212 .collect()
213}
214
215#[expect(
216 clippy::too_many_lines,
217 reason = "preserves DDL dependency and action order"
218)]
219pub fn convert_value_to_column_type(value: Value, ty: &ColumnType) -> Result<Value, SQLError> {
220 if matches!(value, Value::Null) {
221 return Ok(Value::Null);
222 }
223 match ty {
224 ColumnType::Named(name) => Err(SQLError::Routine {
225 sqlstate: "42704".into(),
226 message: format!("type \"{name}\" does not exist"),
227 }),
228 ColumnType::SmallInteger => crate::expr::cast_value(&value, "smallint"),
229 ColumnType::Integer => crate::expr::cast_value(&value, "integer"),
230 ColumnType::BigInteger => crate::expr::cast_value(&value, "bigint"),
231 ColumnType::Oid | ColumnType::Xid => {
232 let Value::Int(value) = crate::expr::cast_value(&value, "bigint")? else {
233 unreachable!("bigint cast returned a non-integer value")
234 };
235 u32::try_from(value)
236 .map(|value| Value::Int(i64::from(value)))
237 .map_err(|_| {
238 SQLError::TypeMismatch(format!(
239 "value {value} is out of range for type {}",
240 column_type_name(ty)
241 ))
242 })
243 }
244 ColumnType::Boolean => match value {
245 Value::Bool(_) => Ok(value),
246 Value::Str(text) => parse_boolean_text(&text)
247 .map(Value::Bool)
248 .ok_or_else(|| SQLError::TypeMismatch(format!("cannot cast `{text}` to boolean"))),
249 other => Err(SQLError::TypeMismatch(format!(
250 "cannot cast {other:?} to boolean"
251 ))),
252 },
253 ColumnType::Void => Ok(Value::Void),
254 ColumnType::Text | ColumnType::RefCursor => Ok(Value::Str(value_to_text(&value))),
255 ColumnType::Name => crate::expr::cast_value(&value, "name"),
256 ColumnType::Uuid => crate::expr::cast_value(&value, "uuid"),
257 ColumnType::Varchar(None) => Ok(Value::Str(value_to_text(&value))),
258 ColumnType::Varchar(Some(length)) => convert_varying_character(value, *length),
259 ColumnType::Bpchar => Ok(Value::FixedChar(value_to_text(&value))),
260 ColumnType::Character(length) => {
261 let length = usize::try_from(*length).map_err(|_| {
262 SQLError::TypeMismatch(format!(
263 "character length {length} exceeds the platform addressable range"
264 ))
265 })?;
266 let text = value_to_text(&value);
267 let char_count = text.chars().count();
268 let significant = if char_count > length {
269 let retained = text.chars().take(length).collect::<String>();
270 let discarded = text.chars().skip(length).collect::<String>();
271 if !discarded.chars().all(|character| character == ' ') {
272 return Err(SQLError::Routine {
273 sqlstate: "22001".into(),
274 message: format!("value too long for type character({length})"),
275 });
276 }
277 retained
278 } else {
279 text
280 };
281 let padding = length.saturating_sub(significant.chars().count());
282 let mut padded = significant;
283 padded.extend(std::iter::repeat_n(' ', padding));
284 Ok(Value::FixedChar(padded))
285 }
286 ColumnType::Real | ColumnType::DoublePrecision => {
287 crate::expr::cast_value(&value, &ty.sql_name())
288 }
289 ColumnType::Numeric { precision, scale } => {
290 let decimal = match value {
291 Value::Decimal(d) => d,
292 Value::Int(i) => DecimalValue::from_i64(i),
293 Value::Float(f) => DecimalValue::from_f64_lossy(f).ok_or_else(|| {
294 SQLError::TypeMismatch(format!("cannot cast {f:?} to numeric"))
295 })?,
296 Value::Bool(b) => DecimalValue::from_bool(b),
297 Value::Str(s) => DecimalValue::parse(&s).ok_or_else(|| {
298 SQLError::TypeMismatch(format!("cannot cast `{s}` to numeric"))
299 })?,
300 other => {
301 return Err(SQLError::TypeMismatch(format!(
302 "cannot cast {other:?} to numeric"
303 )));
304 }
305 };
306 let rounded = match scale {
307 Some(s) => decimal.round_to_scale(*s).ok_or_else(|| {
308 SQLError::TypeMismatch(format!("cannot round numeric to scale {s}"))
309 })?,
310 None => decimal,
311 };
312 if let Some(precision) = precision {
313 let scale = scale.unwrap_or(0);
314 if !rounded.fits_precision(*precision, scale) {
315 return Err(SQLError::TypeMismatch(format!(
316 "numeric field overflow: value {} exceeds precision {precision}, scale {scale}",
317 rounded.to_sql_string()
318 )));
319 }
320 }
321 Ok(Value::Decimal(rounded))
322 }
323 ColumnType::Json => coerce_json_value(value, false),
324 ColumnType::JsonB => coerce_json_value(value, true),
325 ColumnType::Bytea => Ok(match value {
326 Value::Bytes(_) => value,
327 Value::Str(s) => Value::Bytes(s.into_bytes()),
328 other => Value::Bytes(value_to_text(&other).into_bytes()),
329 }),
330 ColumnType::InternalChar => {
331 let text = value_to_text(&value);
332 if text.len() == 1 {
333 Ok(Value::Str(text))
334 } else {
335 Err(SQLError::TypeMismatch(format!(
336 "value `{text}` must be exactly one byte for type \"char\""
337 )))
338 }
339 }
340 ColumnType::Regproc
341 | ColumnType::Regprocedure
342 | ColumnType::Regclass
343 | ColumnType::Regnamespace
344 | ColumnType::Regtype
345 | ColumnType::PgNodeTree
346 | ColumnType::AclItem => Ok(match value {
347 Value::Int(_) | Value::Str(_) => value,
348 other => Value::Str(value_to_text(&other)),
349 }),
350 ColumnType::Regrole => match value {
351 Value::Int(value) => u32::try_from(value)
352 .map(|value| Value::Int(i64::from(value)))
353 .map_err(|_| {
354 SQLError::TypeMismatch(format!(
355 "value {value} is out of range for type regrole"
356 ))
357 }),
358 Value::Str(_) | Value::FixedChar(_) => Err(SQLError::Internal(
359 "regrole name conversion requires catalog resolution".into(),
360 )),
361 other => Err(SQLError::TypeMismatch(format!(
362 "cannot cast {other:?} to regrole"
363 ))),
364 },
365 ColumnType::Int2Vector => convert_value_to_column_type(
366 value,
367 &ColumnType::Array(Box::new(ColumnType::SmallInteger)),
368 ),
369 ColumnType::OidVector => {
370 convert_value_to_column_type(value, &ColumnType::Array(Box::new(ColumnType::Oid)))
371 }
372 ColumnType::AnyArray => match value {
373 Value::Array(_) => Ok(value),
374 other => Err(SQLError::TypeMismatch(format!(
375 "cannot cast {other:?} to anyarray"
376 ))),
377 },
378 ColumnType::Record => match value {
379 Value::Record(_) => Ok(value),
380 Value::Row(values) => Ok(Value::Record(
381 values
382 .into_iter()
383 .enumerate()
384 .map(|(index, value)| (format!("f{}", index + 1), value))
385 .collect(),
386 )),
387 other => Err(SQLError::TypeMismatch(format!(
388 "cannot cast {other:?} to record"
389 ))),
390 },
391 ColumnType::Array(element_type) => {
392 let array = match value {
393 Value::Array(array) => array,
394 Value::List(elements) => ArrayValue::try_new(elements).ok_or_else(|| {
395 SQLError::TypeMismatch(
396 "multidimensional arrays must have matching dimensions".into(),
397 )
398 })?,
399 Value::Str(text) => crate::expr::parse_pg_array_literal(&text)?,
400 other => {
401 return Err(SQLError::TypeMismatch(format!(
402 "cannot cast {other:?} to {}[]",
403 column_type_name(element_type)
404 )))
405 }
406 };
407 let converted = convert_array_elements(array.elements(), element_type)?;
408 ArrayValue::with_lower_bounds(converted, array.lower_bounds().to_vec())
409 .map(Value::Array)
410 .ok_or_else(|| {
411 SQLError::TypeMismatch(
412 "multidimensional arrays must have matching dimensions".into(),
413 )
414 })
415 }
416 ColumnType::Date
417 | ColumnType::Time
418 | ColumnType::TimePrecision(_)
419 | ColumnType::TimeTz
420 | ColumnType::TimeTzPrecision(_)
421 | ColumnType::Timestamp
422 | ColumnType::TimestampPrecision(_)
423 | ColumnType::TimestampTz
424 | ColumnType::TimestampTzPrecision(_)
425 | ColumnType::Interval
426 | ColumnType::IntervalWithFields { .. } => convert_temporal_value(value, ty),
427 ColumnType::Range(subtype) => crate::expr::cast_value(&value, subtype.range_name()),
428 ColumnType::Multirange(subtype) => {
429 crate::expr::cast_value(&value, subtype.multirange_name())
430 }
431 ColumnType::Vector(dim) => {
432 let vector = value_to_vector(&value)?;
433 validate_vector_dimensions(*dim, vector.len())?;
434 Ok(vector_to_value(vector))
435 }
436 ColumnType::Tensor(dim) => {
437 let tensor = value_to_tensor(&value)?;
438 for vector in &tensor {
439 validate_vector_dimensions(*dim, vector.len())?;
440 }
441 Ok(Value::List(
442 tensor.into_iter().map(vector_to_value).collect(),
443 ))
444 }
445 ColumnType::Domain { base, .. } => convert_value_to_column_type(value, base),
446 }
447}
448
449fn convert_array_elements(
450 elements: &[Value],
451 element_type: &ColumnType,
452) -> Result<Vec<Value>, SQLError> {
453 let element_type = array_scalar_type(element_type);
454 elements
455 .iter()
456 .cloned()
457 .map(|element| match element {
458 Value::List(nested) => convert_array_elements(&nested, element_type).map(Value::List),
459 scalar => convert_value_to_column_type(scalar, element_type),
460 })
461 .collect()
462}
463
464fn convert_declared_array_elements(
465 context: &dyn AssignmentContext,
466 elements: &[Value],
467 source_type: &ColumnType,
468 target_type: &ColumnType,
469) -> Result<Vec<Value>, SQLError> {
470 elements
471 .iter()
472 .cloned()
473 .map(|element| match element {
474 Value::List(nested) => {
475 convert_declared_array_elements(context, &nested, source_type, target_type)
476 .map(Value::List)
477 }
478 scalar => {
479 convert_declared_value_to_column_type(context, scalar, source_type, target_type)
480 }
481 })
482 .collect()
483}
484
485fn array_scalar_type(mut ty: &ColumnType) -> &ColumnType {
486 while let ColumnType::Array(element) = ty {
487 ty = element;
488 }
489 ty
490}
491
492fn convert_varying_character(value: Value, length: u32) -> Result<Value, SQLError> {
493 let length = usize::try_from(length).map_err(|_| {
494 SQLError::TypeMismatch(format!(
495 "character varying length {length} exceeds the platform addressable range"
496 ))
497 })?;
498 let text = value_to_text(&value);
499 if text.chars().count() <= length {
500 return Ok(Value::Str(text));
501 }
502 let retained = text.chars().take(length).collect::<String>();
503 let discarded = text.chars().skip(length).collect::<String>();
504 if discarded.chars().all(|character| character == ' ') {
505 Ok(Value::Str(retained))
506 } else {
507 Err(SQLError::Routine {
508 sqlstate: "22001".into(),
509 message: format!("value too long for type character varying({length})"),
510 })
511 }
512}
513
514fn vector_to_value(vector: Vec<f32>) -> Value {
515 Value::List(
516 vector
517 .into_iter()
518 .map(|value| Value::Float(f64::from(value)))
519 .collect(),
520 )
521}
522
523pub fn validate_vector_dimensions(expected: u32, actual: usize) -> Result<(), SQLError> {
524 let expected = usize::try_from(expected).map_err(|_| {
525 SQLError::TypeMismatch(format!(
526 "declared vector dimension {expected} exceeds the platform usize range"
527 ))
528 })?;
529 if actual == expected {
530 Ok(())
531 } else {
532 Err(SQLError::VectorDimMismatch { expected, actual })
533 }
534}
535
536pub use crate::catalog::type_metadata::column_type_name;
537
538fn parse_boolean_text(text: &str) -> Option<bool> {
539 match text.trim().to_ascii_lowercase().as_str() {
540 "true" | "t" | "yes" | "y" | "on" | "1" => Some(true),
541 "false" | "f" | "no" | "n" | "off" | "0" => Some(false),
542 _ => None,
543 }
544}
545
546fn convert_temporal_value(value: Value, ty: &ColumnType) -> Result<Value, SQLError> {
547 crate::expr::cast_value(&value, &ty.sql_name())
548}
549
550pub use crate::expr::value_to_text;
551
552pub fn json_to_core_value(json: serde_json::Value) -> Value {
553 match json {
554 serde_json::Value::Null => Value::Null,
555 serde_json::Value::Bool(b) => Value::Bool(b),
556 serde_json::Value::Number(n) => {
557 if let Some(i) = n.as_i64() {
558 Value::Int(i)
559 } else if let Some(d) = DecimalValue::parse(&n.to_string()) {
560 Value::Decimal(d)
561 } else if let Some(f) = n.as_f64() {
562 Value::Float(f)
563 } else {
564 Value::Null
565 }
566 }
567 serde_json::Value::String(s) => Value::Str(s),
568 serde_json::Value::Array(items) => {
569 Value::List(items.into_iter().map(json_to_core_value).collect())
570 }
571 serde_json::Value::Object(obj) => {
572 if let Ok(temporal) =
573 serde_json::from_value::<TemporalValue>(serde_json::Value::Object(obj.clone()))
574 {
575 return Value::Temporal(temporal);
576 }
577 Value::Map(
578 obj.into_iter()
579 .map(|(k, v)| (k, json_to_core_value(v)))
580 .collect(),
581 )
582 }
583 }
584}
585
586pub use crate::expr::core_value_to_json;
587
588pub fn json_table_value_to_text(value: &serde_json::Value) -> Value {
589 match value {
590 serde_json::Value::Null => Value::Null,
591 serde_json::Value::String(s) => Value::Str(s.clone()),
592 serde_json::Value::Bool(b) => Value::Str(b.to_string()),
593 serde_json::Value::Number(n) => Value::Str(n.to_string()),
594 serde_json::Value::Array(_) | serde_json::Value::Object(_) => Value::Str(value.to_string()),
595 }
596}
597
598pub fn json_table_arg(value: &Value, name: &str) -> Result<serde_json::Value, SQLError> {
599 match value {
600 Value::Json(s) | Value::JsonB(s) | Value::Str(s) => {
601 serde_json::from_str::<serde_json::Value>(s)
602 .map_err(|e| SQLError::TypeMismatch(format!("{name}: invalid JSON: {e}")))
603 }
604 other => Ok(core_value_to_json(other)),
605 }
606}