1use std::sync::Arc;
14
15use uqa_core::{ArrayValue, EnumLabelKey, EnumValue, Value};
16
17use super::{Result, SQLError};
18use crate::ast::ColumnType;
19
20mod functions;
21pub use functions::enum_function_value;
22
23#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct EnumTypeLabel {
26 pub oid: u32,
27 pub key: EnumLabelKey,
28 pub label: String,
29}
30
31#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct EnumTypeLabels {
34 pub type_oid: u32,
35 pub labels: Vec<EnumTypeLabel>,
36}
37
38impl EnumTypeLabels {
39 pub fn by_key(&self, key: &EnumLabelKey) -> Option<&EnumTypeLabel> {
40 self.labels
41 .binary_search_by(|label| label.key.cmp(key))
42 .ok()
43 .map(|index| &self.labels[index])
44 }
45
46 pub fn by_text(&self, text: &str) -> Option<&EnumTypeLabel> {
47 self.labels.iter().find(|label| label.label == text)
48 }
49
50 fn value(&self, label: &EnumTypeLabel) -> Value {
51 Value::Enum(EnumValue::new(self.type_oid, label.key.clone()))
52 }
53}
54
55pub trait EnumLabelCatalog {
57 fn enum_type_labels(&self, type_oid: u32) -> Result<Option<Arc<EnumTypeLabels>>>;
59
60 fn enum_label_uncommitted(&self, label_oid: u32) -> bool;
62
63 fn enum_type_name(&self, type_oid: u32) -> Result<Option<String>>;
65
66 fn has_enum_types(&self) -> bool;
68}
69
70fn catalog_unavailable(type_oid: u32) -> SQLError {
71 SQLError::Internal(format!(
72 "enum type OID {type_oid} is not available in the statement catalog"
73 ))
74}
75
76pub(crate) fn labels(
77 catalog: Option<&dyn EnumLabelCatalog>,
78 type_oid: u32,
79) -> Result<Arc<EnumTypeLabels>> {
80 catalog
81 .ok_or_else(|| catalog_unavailable(type_oid))?
82 .enum_type_labels(type_oid)?
83 .ok_or_else(|| catalog_unavailable(type_oid))
84}
85
86pub(crate) fn type_name(catalog: Option<&dyn EnumLabelCatalog>, type_oid: u32) -> Result<String> {
87 catalog
88 .ok_or_else(|| catalog_unavailable(type_oid))?
89 .enum_type_name(type_oid)?
90 .ok_or_else(|| catalog_unavailable(type_oid))
91}
92
93fn check_safe(
95 catalog: Option<&dyn EnumLabelCatalog>,
96 labels: &EnumTypeLabels,
97 label: &EnumTypeLabel,
98) -> Result<()> {
99 if catalog.is_some_and(|catalog| catalog.enum_label_uncommitted(label.oid)) {
100 return Err(SQLError::Diagnostic {
101 sqlstate: "55P04".into(),
102 message: format!(
103 "unsafe use of new value \"{}\" of enum type {}",
104 label.label,
105 type_name(catalog, labels.type_oid)?
106 ),
107 detail: None,
108 hint: Some("New enum values must be committed before they can be used.".into()),
109 });
110 }
111 Ok(())
112}
113
114fn invalid_internal_value(value: &EnumValue) -> SQLError {
115 SQLError::Internal(format!(
116 "invalid internal value for enum type OID {}",
117 value.type_oid()
118 ))
119}
120
121pub(crate) fn enum_label(
123 catalog: Option<&dyn EnumLabelCatalog>,
124 value: &EnumValue,
125) -> Result<EnumTypeLabel> {
126 labels(catalog, value.type_oid())?
127 .by_key(value.key())
128 .cloned()
129 .ok_or_else(|| invalid_internal_value(value))
130}
131
132pub fn enum_label_text(
134 catalog: Option<&dyn EnumLabelCatalog>,
135 value: &EnumValue,
136) -> Result<String> {
137 enum_label(catalog, value).map(|label| label.label)
138}
139
140pub fn enum_label_oid(catalog: Option<&dyn EnumLabelCatalog>, value: &EnumValue) -> Result<u32> {
142 enum_label(catalog, value).map(|label| label.oid)
143}
144
145pub fn enum_value_from_text(
147 catalog: Option<&dyn EnumLabelCatalog>,
148 type_oid: u32,
149 text: &str,
150) -> Result<Value> {
151 let labels = labels(catalog, type_oid)?;
152 let Some(label) = labels.by_text(text) else {
153 return Err(SQLError::Routine {
154 sqlstate: "22P02".into(),
155 message: format!(
156 "invalid input value for enum {}: \"{text}\"",
157 type_name(catalog, type_oid)?
158 ),
159 });
160 };
161 check_safe(catalog, &labels, label)?;
162 Ok(labels.value(label))
163}
164
165pub fn enum_endpoint(
167 catalog: Option<&dyn EnumLabelCatalog>,
168 type_oid: u32,
169 last: bool,
170) -> Result<Value> {
171 let labels = labels(catalog, type_oid)?;
172 let label = if last {
173 labels.labels.last()
174 } else {
175 labels.labels.first()
176 };
177 let Some(label) = label else {
178 return Err(SQLError::Routine {
179 sqlstate: "55000".into(),
180 message: format!("enum {} contains no values", type_name(catalog, type_oid)?),
181 });
182 };
183 check_safe(catalog, &labels, label)?;
184 Ok(labels.value(label))
185}
186
187pub fn enum_range(
189 catalog: Option<&dyn EnumLabelCatalog>,
190 type_oid: u32,
191 lower: Option<&EnumValue>,
192 upper: Option<&EnumValue>,
193) -> Result<Value> {
194 let labels = labels(catalog, type_oid)?;
195 let mut elements = Vec::new();
196 for label in &labels.labels {
197 if lower.is_some_and(|lower| &label.key < lower.key()) {
198 continue;
199 }
200 if upper.is_some_and(|upper| &label.key > upper.key()) {
201 break;
202 }
203 check_safe(catalog, &labels, label)?;
204 elements.push(labels.value(label));
205 }
206 ArrayValue::try_new(elements)
207 .map(Value::Array)
208 .ok_or_else(|| SQLError::Internal("enum range array has invalid dimensions".into()))
209}
210
211pub fn render_enum_labels(catalog: Option<&dyn EnumLabelCatalog>, value: &Value) -> Result<Value> {
213 Ok(match value {
214 Value::Enum(label) => Value::Str(enum_label_text(catalog, label)?),
215 Value::Array(array) => Value::Array(map_array(array, |element| {
216 render_enum_labels(catalog, element)
217 })?),
218 Value::List(values) => Value::List(
219 values
220 .iter()
221 .map(|element| render_enum_labels(catalog, element))
222 .collect::<Result<_>>()?,
223 ),
224 Value::Row(values) => Value::Row(
225 values.clone().with_values(
226 values
227 .iter()
228 .map(|element| render_enum_labels(catalog, element))
229 .collect::<Result<_>>()?,
230 )?,
231 ),
232 Value::Record(fields) => Value::Record(
233 fields
234 .iter()
235 .map(|(name, element)| Ok((name.clone(), render_enum_labels(catalog, element)?)))
236 .collect::<Result<_>>()?,
237 ),
238 Value::Map(fields) => Value::Map(
239 fields
240 .iter()
241 .map(|(name, element)| Ok((name.clone(), render_enum_labels(catalog, element)?)))
242 .collect::<Result<_>>()?,
243 ),
244 other => other.clone(),
245 })
246}
247
248fn map_array(
250 array: &ArrayValue,
251 mut convert: impl FnMut(&Value) -> Result<Value>,
252) -> Result<ArrayValue> {
253 fn leaves(
254 values: &[Value],
255 convert: &mut dyn FnMut(&Value) -> Result<Value>,
256 ) -> Result<Vec<Value>> {
257 values
258 .iter()
259 .map(|value| match value {
260 Value::List(values) => leaves(values, convert).map(Value::List),
261 value => convert(value),
262 })
263 .collect()
264 }
265 let elements = leaves(array.elements(), &mut convert)?;
266 ArrayValue::with_lower_bounds(elements, array.lower_bounds().to_vec())
267 .filter(|converted| converted.dimensions() == array.dimensions())
268 .ok_or_else(|| SQLError::Internal("enum array conversion changed array dimensions".into()))
269}
270
271pub fn contains_enum_carrier(value: &Value) -> bool {
273 match value {
274 Value::Enum(_) => true,
275 Value::Array(array) => array.elements().iter().any(contains_enum_carrier),
276 Value::List(values) => values.iter().any(contains_enum_carrier),
277 Value::Row(values) => values.iter().any(contains_enum_carrier),
278 Value::Record(fields) => fields.iter().any(|(_, value)| contains_enum_carrier(value)),
279 Value::Map(fields) => fields.values().any(contains_enum_carrier),
280 _ => false,
281 }
282}
283
284#[must_use]
286pub fn call_applies_output_functions(name: &str, arguments: &[(Option<String>, Value)]) -> bool {
287 match name {
288 "concat_op" => !arguments
289 .iter()
290 .any(|(_, value)| matches!(value, Value::Array(_) | Value::List(_))),
291 "concat" | "concat_ws" | "format" | "quote_literal" | "quote_nullable" | "to_json"
292 | "to_jsonb" | "row_to_json" | "array_to_json" | "json_build_object"
293 | "json_build_array" | "jsonb_build_object" | "jsonb_build_array" | "array_to_string" => {
294 true
295 }
296 _ => false,
297 }
298}
299
300pub fn render_call_arguments(
302 catalog: Option<&dyn EnumLabelCatalog>,
303 arguments: Vec<(Option<String>, Value)>,
304) -> Result<Vec<(Option<String>, Value)>> {
305 arguments
306 .into_iter()
307 .map(|(name, value)| {
308 if contains_enum_carrier(&value) {
309 Ok((name, render_enum_labels(catalog, &value)?))
310 } else {
311 Ok((name, value))
312 }
313 })
314 .collect()
315}
316
317pub fn render_host_arguments(
319 catalog: Option<&dyn EnumLabelCatalog>,
320 arguments: &[Value],
321) -> Result<Vec<Value>> {
322 arguments
323 .iter()
324 .map(|value| {
325 if contains_enum_carrier(value) {
326 render_enum_labels(catalog, value)
327 } else {
328 Ok(value.clone())
329 }
330 })
331 .collect()
332}
333
334#[must_use]
336pub fn is_enum_bearing(ty: &ColumnType) -> bool {
337 match ty {
338 ColumnType::Enum(_) => true,
339 ColumnType::Array(element) => is_enum_bearing(element),
340 _ => false,
341 }
342}
343
344pub fn fold_unknown_literal(
346 catalog: Option<&dyn EnumLabelCatalog>,
347 value: &Value,
348 target: &ColumnType,
349) -> Result<Option<Value>> {
350 match (target, value) {
351 (ColumnType::Enum(_) | ColumnType::Array(_), Value::Null) if is_enum_bearing(target) => {
352 Ok(Some(Value::Null))
353 }
354 (ColumnType::Enum(reference), Value::Str(text)) => {
355 enum_value_from_text(catalog, reference.oid, text).map(Some)
356 }
357 (ColumnType::Array(element), Value::Str(text)) if is_enum_bearing(element) => {
358 let mut leaf = element.as_ref();
359 while let ColumnType::Array(inner) = leaf {
360 leaf = inner;
361 }
362 let ColumnType::Enum(reference) = leaf else {
363 return Ok(None);
364 };
365 let parsed = super::casting::parse_pg_array_literal(text)?;
366 map_array(&parsed, |element| match element {
367 Value::Null => Ok(Value::Null),
368 Value::Str(text) => enum_value_from_text(catalog, reference.oid, text),
369 other => Err(SQLError::Internal(format!(
370 "array literal parsing produced a non-text element {other:?}"
371 ))),
372 })
373 .map(|array| Some(Value::Array(array)))
374 }
375 _ => Ok(None),
376 }
377}
378
379fn string_category(ty: &ColumnType) -> bool {
381 match ty {
382 ColumnType::Text
383 | ColumnType::Varchar(_)
384 | ColumnType::Bpchar
385 | ColumnType::Character(_)
386 | ColumnType::Name => true,
387 ColumnType::Domain { base, .. } => string_category(base),
388 _ => false,
389 }
390}
391
392fn cannot_cast(source: &str, target: &str) -> SQLError {
393 SQLError::Routine {
394 sqlstate: "42846".into(),
395 message: format!("cannot cast type {source} to {target}"),
396 }
397}
398
399pub fn cast_to_enum(
401 catalog: Option<&dyn EnumLabelCatalog>,
402 value: &Value,
403 source: Option<&ColumnType>,
404 target: &ColumnType,
405) -> Result<Option<Value>> {
406 let ColumnType::Enum(reference) = target else {
407 return Ok(None);
408 };
409 let source_name = |source: Option<&ColumnType>| -> Result<String> {
410 Ok(match (value, source) {
411 (Value::Enum(label), _) => type_name(catalog, label.type_oid())?,
412 (_, Some(ColumnType::Enum(source))) => type_name(catalog, source.oid)?,
413 (_, Some(source)) => source.sql_name(),
414 (_, None) => "unknown".into(),
415 })
416 };
417 match value {
418 Value::Null => Ok(Some(Value::Null)),
419 Value::Enum(label) if label.type_oid() == reference.oid => Ok(Some(value.clone())),
420 Value::Str(text) | Value::FixedChar(text) if source.is_none_or(string_category) => {
421 enum_value_from_text(catalog, reference.oid, text).map(Some)
422 }
423 _ => Err(cannot_cast(
424 &source_name(source)?,
425 &type_name(catalog, reference.oid)?,
426 )),
427 }
428}
429
430pub fn enum_output_for_cast(
432 catalog: Option<&dyn EnumLabelCatalog>,
433 label: &EnumValue,
434 target: &ColumnType,
435) -> Result<Value> {
436 if string_category(target) {
437 return enum_label_text(catalog, label).map(Value::Str);
438 }
439 Err(cannot_cast(
440 &type_name(catalog, label.type_oid())?,
441 &match target {
442 ColumnType::Enum(reference) => type_name(catalog, reference.oid)?,
443 other => other.sql_name(),
444 },
445 ))
446}