1use std::borrow::Cow;
10
11use uqa_core::{ArrayValue, Value};
12
13use crate::ast::{ColumnType, InternalColumnRef};
14use crate::error::{Result, SQLError};
15use crate::params::SQLParam;
16use crate::result::ResultRow;
17
18use super::casting::{cast_value_from, parse_pg_array_literal};
19use super::conversion::{array_value_to_string, value_to_string};
20
21#[must_use]
22pub fn coercion_type_name(ty: &ColumnType) -> String {
23 match ty {
24 ColumnType::Domain { base, .. } => coercion_type_name(base),
25 ColumnType::Array(element) => format!("{}[]", coercion_type_name(element)),
26 _ => ty.sql_name(),
27 }
28}
29
30fn regrole_array_type(ty: &ColumnType) -> bool {
31 match ty {
32 ColumnType::Array(element) => {
33 matches!(element.as_ref(), ColumnType::Regrole) || regrole_array_type(element)
34 }
35 _ => false,
36 }
37}
38
39fn array_leaf_type(ty: &ColumnType) -> &ColumnType {
40 match ty {
41 ColumnType::Array(element) => array_leaf_type(element),
42 _ => ty,
43 }
44}
45
46fn cast_regrole_array_elements(
47 values: &[Value],
48 source_ty: Option<&str>,
49 engine: Option<&dyn EngineHook>,
50) -> Result<Vec<Value>> {
51 values
52 .iter()
53 .map(|value| match value {
54 Value::List(nested) => {
55 cast_regrole_array_elements(nested, source_ty, engine).map(Value::List)
56 }
57 value => cast_value_with_type_resolution(value, source_ty, "regrole", engine),
58 })
59 .collect()
60}
61
62fn cast_regrole_array(
63 value: &Value,
64 source_ty: Option<&ColumnType>,
65 engine: Option<&dyn EngineHook>,
66) -> Result<Value> {
67 let array = match value {
68 Value::Array(array) => array.clone(),
69 Value::Str(text) => parse_pg_array_literal(text)?,
70 other => {
71 return Err(SQLError::TypeMismatch(format!(
72 "CAST AS regrole[]: expected array, got {other:?}"
73 )));
74 }
75 };
76 let source_name = source_ty.map(array_leaf_type).map(ColumnType::sql_name);
77 let elements = cast_regrole_array_elements(array.elements(), source_name.as_deref(), engine)?;
78 ArrayValue::with_lower_bounds(elements, array.lower_bounds().to_vec())
79 .map(Value::Array)
80 .ok_or_else(|| SQLError::TypeMismatch("array dimensions changed during cast".into()))
81}
82
83pub trait EngineHook {
88 fn transaction_timestamp_micros(&self) -> Option<i64> {
90 None
91 }
92
93 fn statement_timestamp_micros(&self) -> Option<i64> {
95 None
96 }
97
98 fn nextval(&self, name: &str) -> Result<i64>;
99 fn currval(&self, name: &str) -> Result<i64>;
100 fn lastval(&self) -> Result<i64> {
101 Err(SQLError::Unsupported(
102 "lastval requires an engine hook implementation".into(),
103 ))
104 }
105 fn setval(&self, name: &str, value: i64, is_called: bool) -> Result<i64>;
106
107 fn call_scalar_function(&self, _name: &str, _args: &[Value]) -> Option<Result<Value>> {
108 None
109 }
110
111 fn call_bound_builtin_function(
116 &self,
117 _binding: &crate::ast::FunctionBinding,
118 _args: &[(Option<String>, Value)],
119 ) -> Option<Result<Value>> {
120 None
121 }
122
123 fn has_scalar_functions(&self) -> bool {
124 true
125 }
126
127 fn resolve_type_name(&self, _name: &str) -> std::result::Result<Option<ColumnType>, String> {
129 Ok(None)
130 }
131
132 fn cast_domain(
134 &self,
135 _value: &Value,
136 _source: Option<&str>,
137 _target: &ColumnType,
138 ) -> Result<Option<Value>> {
139 Ok(None)
140 }
141
142 fn resolve_regtype_input(&self, _name: &str) -> Result<Option<i64>> {
144 Ok(None)
145 }
146
147 fn resolve_regclass(&self, _name: &str) -> std::result::Result<Option<i64>, String> {
149 Ok(None)
150 }
151
152 fn resolve_regclass_input(&self, name: &str) -> Result<Option<i64>> {
154 self.resolve_regclass(name).map_err(SQLError::Internal)
155 }
156
157 fn resolve_regprocedure(&self, _name: &str) -> std::result::Result<Option<i64>, String> {
159 Ok(None)
160 }
161
162 fn resolve_regrole(&self, _name: &str) -> Result<Option<i64>> {
164 Ok(None)
165 }
166
167 fn resolve_regnamespace(&self, name: &str) -> Result<Option<i64>> {
169 self.resolve_regobject(&ColumnType::Regnamespace, name)
170 }
171
172 fn resolve_regobject(&self, ty: &ColumnType, name: &str) -> Result<Option<i64>> {
174 match ty {
175 ColumnType::Regclass => self.resolve_regclass_input(name),
176 ColumnType::Regprocedure => self.resolve_regprocedure(name).map_err(SQLError::Internal),
177 ColumnType::Regrole => self.resolve_regrole(name),
178 ColumnType::Regproc | ColumnType::Regnamespace | ColumnType::Regtype => Ok(None),
179 _ => Err(SQLError::Internal(format!(
180 "unsupported regobject lookup type `{}`",
181 ty.sql_name()
182 ))),
183 }
184 }
185
186 fn resolve_regtype_output(
188 &self,
189 _ty: &ColumnType,
190 _oid: i64,
191 ) -> std::result::Result<Option<String>, String> {
192 Ok(None)
193 }
194
195 fn current_schema(&self) -> std::result::Result<Option<String>, String> {
199 Ok(None)
200 }
201
202 fn current_user(&self) -> std::result::Result<Option<String>, String> {
203 Ok(None)
204 }
205
206 fn session_user(&self) -> std::result::Result<Option<String>, String> {
207 Ok(None)
208 }
209
210 fn current_schemas(
212 &self,
213 _include_implicit: bool,
214 ) -> std::result::Result<Option<Vec<String>>, String> {
215 Ok(None)
216 }
217
218 fn random_value(&self) -> std::result::Result<Option<f64>, String> {
221 Ok(None)
222 }
223
224 fn random_u64(&self) -> std::result::Result<Option<u64>, String> {
228 Ok(None)
229 }
230
231 fn set_random_seed(&self, _seed: f64) -> std::result::Result<bool, String> {
234 Ok(false)
235 }
236
237 fn call_user_function(
242 &self,
243 _name: &str,
244 _args: &[(Option<String>, Value)],
245 ) -> Option<Result<Value>> {
246 None
247 }
248
249 fn call_bound_user_function(
250 &self,
251 _binding: &crate::ast::FunctionBinding,
252 _args: &[(Option<String>, Value)],
253 ) -> Option<Result<Value>> {
254 None
255 }
256}
257
258pub fn format_regtype_value(
260 value: &Value,
261 ty: &ColumnType,
262 engine: Option<&dyn EngineHook>,
263) -> Result<Option<String>> {
264 if matches!(value, Value::Null) {
265 return Ok(None);
266 }
267 if let ColumnType::Array(element) = ty {
268 if !matches!(
269 element.as_ref(),
270 ColumnType::Regproc
271 | ColumnType::Regprocedure
272 | ColumnType::Regclass
273 | ColumnType::Regnamespace
274 | ColumnType::Regrole
275 | ColumnType::Regtype
276 ) {
277 return Ok(None);
278 }
279 let Value::Array(array) = value else {
280 return Ok(Some(value_to_string(value)));
281 };
282 let elements = format_regtype_array_elements(array.elements(), element, engine)?;
283 let formatted = array.with_elements(elements).ok_or_else(|| {
284 SQLError::Internal("regtype array output changed the array dimensions".into())
285 })?;
286 return Ok(Some(array_value_to_string(&formatted)));
287 }
288 if !matches!(
289 ty,
290 ColumnType::Regproc
291 | ColumnType::Regprocedure
292 | ColumnType::Regclass
293 | ColumnType::Regnamespace
294 | ColumnType::Regrole
295 | ColumnType::Regtype
296 ) {
297 return Ok(None);
298 }
299 let Value::Int(oid) = value else {
300 return Ok(Some(value_to_string(value)));
301 };
302 if *oid == 0 {
303 return Ok(Some("-".into()));
304 }
305 let resolved = engine
306 .map(|engine| engine.resolve_regtype_output(ty, *oid))
307 .transpose()
308 .map_err(SQLError::Internal)?
309 .flatten();
310 Ok(Some(resolved.unwrap_or_else(|| oid.to_string())))
311}
312
313fn format_regtype_array_elements(
314 values: &[Value],
315 element: &ColumnType,
316 engine: Option<&dyn EngineHook>,
317) -> Result<Vec<Value>> {
318 values
319 .iter()
320 .map(|value| match value {
321 Value::Null => Ok(Value::Null),
322 Value::List(nested) => {
323 format_regtype_array_elements(nested, element, engine).map(Value::List)
324 }
325 other => format_regtype_value(other, element, engine)
326 .map(|text| text.map_or_else(|| other.clone(), Value::Str)),
327 })
328 .collect()
329}
330
331pub fn cast_value_with_type_resolution(
333 value: &Value,
334 source_ty: Option<&str>,
335 target_ty: &str,
336 engine: Option<&dyn EngineHook>,
337) -> Result<Value> {
338 let resolved_target = engine
339 .map(|engine| engine.resolve_type_name(target_ty))
340 .transpose()
341 .map_err(SQLError::Internal)?
342 .flatten();
343 if let (Some(engine), Some(target)) = (engine, resolved_target.as_ref()) {
344 if let Some(value) = engine.cast_domain(value, source_ty, target)? {
345 return Ok(value);
346 }
347 if matches!(target, ColumnType::Array(_)) && requires_catalog_array_cast(target) {
348 return cast_catalog_array(value, source_ty, target, engine);
349 }
350 }
351 let resolved_source = match (engine, source_ty) {
352 (Some(engine), Some(source_ty)) => engine
353 .resolve_type_name(source_ty)
354 .map_err(SQLError::Internal)?
355 .map(|ty| coercion_type_name(&ty)),
356 _ => None,
357 };
358 let source_ty = resolved_source.as_deref().or(source_ty);
359 let target_ty = resolved_target.as_ref().map_or_else(
360 || Cow::Borrowed(target_ty),
361 |ty| Cow::Owned(coercion_type_name(ty)),
362 );
363 let target_column_type = resolved_target
364 .clone()
365 .or_else(|| ColumnType::from_sql_name(&target_ty).ok());
366 if target_column_type.as_ref().is_some_and(regrole_array_type) {
367 let source_column_type = source_ty.and_then(|name| ColumnType::from_sql_name(name).ok());
368 return cast_regrole_array(value, source_column_type.as_ref(), engine);
369 }
370 if target_ty.eq_ignore_ascii_case("text") {
371 if let Some(source_ty) = source_ty.and_then(|source| ColumnType::from_sql_name(source).ok())
372 {
373 if let Some(text) = format_regtype_value(value, &source_ty, engine)? {
374 return Ok(Value::Str(text));
375 }
376 }
377 }
378 if target_ty.eq_ignore_ascii_case("regclass") {
379 if let (Some(engine), Value::Str(name) | Value::FixedChar(name)) = (engine, value) {
380 return engine
381 .resolve_regclass_input(name)?
382 .map(Value::Int)
383 .ok_or_else(|| SQLError::Routine {
384 sqlstate: "42P01".into(),
385 message: format!("relation \"{name}\" does not exist"),
386 });
387 }
388 }
389 if target_ty.eq_ignore_ascii_case("regprocedure") {
390 if let (Some(engine), Value::Str(name) | Value::FixedChar(name)) = (engine, value) {
391 return engine
392 .resolve_regprocedure(name)
393 .map_err(SQLError::Internal)?
394 .map(Value::Int)
395 .ok_or_else(|| SQLError::Routine {
396 sqlstate: "42883".into(),
397 message: format!("function {name} does not exist"),
398 });
399 }
400 }
401 if target_ty.eq_ignore_ascii_case("regrole") {
402 if let (Some(engine), Value::Str(name) | Value::FixedChar(name)) = (engine, value) {
403 return engine
404 .resolve_regrole(name)?
405 .map(Value::Int)
406 .ok_or_else(|| SQLError::Routine {
407 sqlstate: "42704".into(),
408 message: format!("role \"{name}\" does not exist"),
409 });
410 }
411 }
412 if matches!(target_column_type.as_ref(), Some(ColumnType::Regnamespace)) {
413 if let (Some(engine), Value::Str(name) | Value::FixedChar(name)) = (engine, value) {
414 return engine
415 .resolve_regnamespace(name)?
416 .map(Value::Int)
417 .ok_or_else(|| SQLError::Routine {
418 sqlstate: "3F000".into(),
419 message: format!("schema \"{name}\" does not exist"),
420 });
421 }
422 }
423 if matches!(target_column_type.as_ref(), Some(ColumnType::Regtype)) {
424 if let (Some(engine), Value::Str(name) | Value::FixedChar(name)) = (engine, value) {
425 if let Some(oid) = engine.resolve_regtype_input(name)? {
426 return Ok(Value::Int(oid));
427 }
428 }
429 }
430 cast_value_from(value, &target_ty, source_ty)
431}
432
433fn requires_catalog_array_cast(ty: &ColumnType) -> bool {
434 match ty {
435 ColumnType::Domain { .. } | ColumnType::Regtype => true,
436 ColumnType::Array(element) => requires_catalog_array_cast(element),
437 _ => false,
438 }
439}
440
441fn cast_catalog_array(
442 value: &Value,
443 source: Option<&str>,
444 target: &ColumnType,
445 engine: &dyn EngineHook,
446) -> Result<Value> {
447 if matches!(value, Value::Null) {
448 return Ok(Value::Null);
449 }
450 let array = match value {
451 Value::Array(array) => array.clone(),
452 Value::Str(text) => parse_pg_array_literal(text)?,
453 other => {
454 return Err(SQLError::TypeMismatch(format!(
455 "CAST AS {}: expected array, got {other:?}",
456 target.sql_name()
457 )))
458 }
459 };
460 let source_element = source.map(|name| name.trim_end_matches("[]"));
461 let target_element = array_leaf_type(target).sql_name();
462 let values =
463 cast_catalog_array_elements(array.elements(), source_element, &target_element, engine)?;
464 ArrayValue::with_lower_bounds(values, array.lower_bounds().to_vec())
465 .map(Value::Array)
466 .ok_or_else(|| SQLError::TypeMismatch("array dimensions changed during cast".into()))
467}
468
469fn cast_catalog_array_elements(
470 values: &[Value],
471 source: Option<&str>,
472 target: &str,
473 engine: &dyn EngineHook,
474) -> Result<Vec<Value>> {
475 values
476 .iter()
477 .map(|value| match value {
478 Value::List(values) => {
479 cast_catalog_array_elements(values, source, target, engine).map(Value::List)
480 }
481 value => cast_value_with_type_resolution(value, source, target, Some(engine)),
482 })
483 .collect()
484}
485
486pub trait RowLookup {
490 fn column(&self, name: &str) -> Option<&Value>;
491
492 fn column_is_ambiguous(&self, _name: &str) -> bool {
496 false
497 }
498
499 fn qualified_column(&self, qualifier: &str, column: &str) -> Option<&Value>;
500
501 fn qualified_column_is_ambiguous(&self, _qualifier: &str, _column: &str) -> bool {
503 false
504 }
505
506 fn positional_column(&self, _index: usize) -> Option<&Value> {
511 None
512 }
513
514 fn internal_column(&self, _column: InternalColumnRef) -> Option<&Value> {
517 None
518 }
519
520 fn score_source(&self, _qualifier: Option<&str>) -> Option<&Value> {
522 None
523 }
524
525 fn score_source_is_ambiguous(&self, _qualifier: Option<&str>) -> bool {
527 false
528 }
529
530 fn visit_columns(&self, _visitor: &mut dyn FnMut(&str, &Value)) {}
535}
536
537impl RowLookup for ResultRow {
538 fn column(&self, name: &str) -> Option<&Value> {
539 self.get(name)
540 }
541
542 fn qualified_column(&self, _qualifier: &str, _column: &str) -> Option<&Value> {
543 None
544 }
545
546 fn visit_columns(&self, visitor: &mut dyn FnMut(&str, &Value)) {
547 for (column, value) in self {
548 visitor(column, value);
549 }
550 }
551}
552
553pub struct EvalContext<'a> {
554 pub row: Option<&'a ResultRow>,
555 row_lookup: Option<&'a dyn RowLookup>,
556 pub params: &'a [SQLParam],
557 pub engine: Option<&'a dyn EngineHook>,
558}
559
560impl<'a> EvalContext<'a> {
561 pub fn new(row: Option<&'a ResultRow>, params: &'a [SQLParam]) -> Self {
562 Self {
563 row,
564 row_lookup: row.map(|row| row as &dyn RowLookup),
565 params,
566 engine: None,
567 }
568 }
569
570 pub fn from_row_lookup(row: &'a dyn RowLookup, params: &'a [SQLParam]) -> Self {
571 Self {
572 row: None,
576 row_lookup: Some(row),
577 params,
578 engine: None,
579 }
580 }
581
582 pub fn with_engine(mut self, engine: &'a dyn EngineHook) -> Self {
583 self.engine = Some(engine);
584 self
585 }
586
587 pub(super) fn row_lookup(&self) -> Result<&'a dyn RowLookup> {
588 self.row_lookup
589 .ok_or_else(|| SQLError::Internal("column reference without row context".into()))
590 }
591
592 pub fn column_value(&self, name: &str) -> Result<Value> {
596 if self.row_lookup()?.column_is_ambiguous(name) {
597 return Err(SQLError::AmbiguousColumn(name.to_string()));
598 }
599 Ok(self
600 .row_lookup()?
601 .column(name)
602 .cloned()
603 .unwrap_or(Value::Null))
604 }
605
606 pub fn qualified_column_value(&self, qualifier: &str, column: &str) -> Result<Value> {
608 if self
609 .row_lookup()?
610 .qualified_column_is_ambiguous(qualifier, column)
611 {
612 return Err(SQLError::AmbiguousColumn(format!("{qualifier}.{column}")));
613 }
614 Ok(self
615 .row_lookup()?
616 .qualified_column(qualifier, column)
617 .cloned()
618 .unwrap_or(Value::Null))
619 }
620}