Skip to main content

uqa_sql/expr/context/
casting.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Catalog-aware cast selection shares controlled type, array and value constructors.
8
9use uqa_core::{
10    memory::{Produced, ProductionControl, ProductionString, ProductionVec},
11    ArrayValue, Value,
12};
13
14use super::super::casting::{cast_value_from_with_control, parse_pg_array_literal_with_control};
15use super::{format_regtype_elements_with_control, format_regtype_value_with_control, EngineHook};
16use crate::{
17    ast::ColumnType,
18    error::{Result, SQLError},
19};
20
21mod datestyle;
22mod intervalstyle;
23mod timezone;
24
25/// Catalog input needed after analysis selects a type for an unknown literal. Domain array input includes each element's constraints, so the caller must retain this value rather than read the text again at execution.
26pub trait CatalogInputFunctions {
27    fn read_unknown_input(&self, text: &str, target: &ColumnType) -> Result<Value>;
28}
29
30pub(crate) fn requires_domain_array_input(target: &ColumnType) -> bool {
31    matches!(target, ColumnType::Array(element) if matches!(array_leaf_type(element), ColumnType::Domain { .. }))
32}
33
34/// Input constants containing named composites or domain array elements must be read while the definition's catalog descriptor is available.
35pub(crate) fn requires_catalog_constant_input(target: &ColumnType) -> bool {
36    requires_domain_array_input(target)
37        || match target {
38            ColumnType::Composite(_) => true,
39            ColumnType::Array(element) | ColumnType::Domain { base: element, .. } => {
40                requires_catalog_constant_input(element)
41            }
42            _ => false,
43        }
44}
45
46/// Read a catalog-dependent constant once, retaining its typed datum in stored syntax.
47pub fn read_catalog_input(
48    text: &str,
49    target: &ColumnType,
50    engine: &dyn EngineHook,
51) -> Result<Value> {
52    if requires_domain_array_input(target) {
53        return read_catalog_array_input(text, target, engine);
54    }
55    if !requires_catalog_constant_input(target) {
56        return Err(SQLError::Internal(
57            "catalog input requires a composite or domain array".into(),
58        ));
59    }
60    super::cast_value_with_type_resolution(
61        &Value::Str(text.into()),
62        Some("unknown"),
63        &target.catalog_name(),
64        Some(engine),
65    )
66}
67
68/// Read an array whose elements are domains with the same input and constraint path as a catalog-aware cast. The resolved target preserves domain identity; a scalar outer domain remains the surrounding expression's runtime coercion.
69pub fn read_catalog_array_input(
70    text: &str,
71    target: &ColumnType,
72    engine: &dyn EngineHook,
73) -> Result<Value> {
74    if !requires_domain_array_input(target) {
75        return Err(SQLError::Internal(
76            "catalog array input requires domain elements".into(),
77        ));
78    }
79    let control = ProductionControl::uncontrolled();
80    let parsed = parse_pg_array_literal_with_control(text, &control)?
81        .into_uncontrolled()
82        .map_err(|_| SQLError::Internal("ordinary catalog array input owner".into()))?;
83    cast_catalog_array(&Value::Array(parsed), None, target, engine, &control)?
84        .into_uncontrolled()
85        .map_err(|_| SQLError::Internal("ordinary catalog array input owner".into()))
86}
87
88#[must_use]
89pub fn coercion_type_name(ty: &ColumnType) -> String {
90    coercion_type_name_with_control(ty, &ProductionControl::uncontrolled())
91        .expect("ordinary coercion type name")
92        .into_uncontrolled()
93        .expect("ordinary coercion type name owner")
94}
95
96fn coercion_type_name_with_control(
97    ty: &ColumnType,
98    control: &ProductionControl<'_>,
99) -> Result<Produced<String>> {
100    control.check()?;
101    match ty {
102        ColumnType::Domain { base, .. } => coercion_type_name_with_control(base, control),
103        ColumnType::Array(element) => {
104            let element = coercion_type_name_with_control(element, control)?;
105            let mut name = ProductionString::new(*control);
106            name.push_str(&element)?;
107            name.push_str("[]")?;
108            Ok(name.finish()?)
109        }
110        _ => Ok(ty.sql_name_with_control(control)?),
111    }
112}
113
114/// The types of `PostgreSQL`'s string category, which any type casts to through its output function.
115fn is_string_type(ty: &ColumnType) -> bool {
116    matches!(
117        ty,
118        ColumnType::Text | ColumnType::Name | ColumnType::Varchar(_) | ColumnType::Character(_)
119    )
120}
121
122fn regrole_array_type(ty: &ColumnType) -> bool {
123    match ty {
124        ColumnType::Array(element) => {
125            matches!(element.as_ref(), ColumnType::Regrole) || regrole_array_type(element)
126        }
127        _ => false,
128    }
129}
130
131fn array_leaf_type(ty: &ColumnType) -> &ColumnType {
132    match ty {
133        ColumnType::Array(element) => array_leaf_type(element),
134        _ => ty,
135    }
136}
137
138fn optional_type_name(
139    name: &str,
140    control: &ProductionControl<'_>,
141) -> Result<Option<Produced<ColumnType>>> {
142    match ColumnType::from_sql_name_with_control(name, control) {
143        Ok(ty) => Ok(Some(ty)),
144        Err(error) if matches!(error.sqlstate(), Some("53200" | "57014")) => Err(error),
145        Err(_) => Ok(None),
146    }
147}
148
149/// Cast a value after resolving catalog-owned types and enforcing domain constraints before exposing a base-type carrier.
150pub fn cast_value_with_type_resolution(
151    value: &Value,
152    source_ty: Option<&str>,
153    target_ty: &str,
154    engine: Option<&dyn EngineHook>,
155) -> Result<Value> {
156    cast_value_with_type_resolution_with_control(
157        value,
158        source_ty,
159        target_ty,
160        engine,
161        &ProductionControl::uncontrolled(),
162    )?
163    .into_uncontrolled()
164    .map_err(|_| SQLError::Internal("ordinary catalog cast owner".into()))
165}
166
167/// The text a value's output function spells, and the type to cast it from, when a cast goes through output: an enum label, `record_out` of a composite record cast to a string type, and a container whose enum fields and elements spell their current labels.
168fn output_for_cast<'a>(
169    engine: &dyn EngineHook,
170    value: &Value,
171    source_ty: Option<&'a str>,
172    resolved_source_type: Option<&ColumnType>,
173    target: &ColumnType,
174) -> Result<Option<(Value, Option<&'a str>)>> {
175    if let Value::Enum(label) = value {
176        let output =
177            super::super::enums::enum_output_for_cast(engine.enum_labels(), label, target)?;
178        return Ok(Some((output, Some("text"))));
179    }
180    if let Value::Record(record) = value {
181        if let Some(oid) = record.type_oid().filter(|_| is_string_type(target)) {
182            let ty = engine
183                .resolve_type_oid(oid)
184                .map_err(SQLError::Internal)?
185                .ok_or_else(|| SQLError::Routine {
186                    sqlstate: "42704".into(),
187                    message: format!("type with OID {oid} does not exist"),
188                })?;
189            return Ok(Some((
190                Value::Str(crate::result::format_postgres_text(
191                    value,
192                    &ty,
193                    Some(engine),
194                )?),
195                Some("text"),
196            )));
197        }
198    }
199    // `record_out` spells each field through its attribute type's output function.
200    if let (Value::Record(_), Some(source @ ColumnType::Composite(_))) =
201        (value, resolved_source_type)
202    {
203        if is_string_type(target) {
204            let text = crate::result::format_postgres_text(value, source, Some(engine))?;
205            return Ok(Some((Value::Str(text), Some("text"))));
206        }
207    }
208    if matches!(value, Value::Record(_) | Value::Row(_))
209        && is_string_type(target)
210        && super::super::enums::contains_enum_carrier(value)
211    {
212        let rendered = super::super::enums::render_enum_labels(engine.enum_labels(), value)?;
213        return Ok(Some((rendered, source_ty)));
214    }
215    Ok(None)
216}
217
218/// Array output borrows the input and retains both intermediate labels and final text under the caller's production budget.
219fn catalog_array_output(
220    value: &Value,
221    source_ty: Option<&str>,
222    target_ty: &str,
223    target: &ColumnType,
224    engine: &dyn EngineHook,
225    control: &ProductionControl<'_>,
226) -> Result<Option<Produced<Value>>> {
227    if !matches!(value, Value::Array(_))
228        || !is_string_type(target)
229        || !(super::super::enums::contains_enum_carrier(value)
230            || super::super::datums::contains_datum(value))
231    {
232        return Ok(None);
233    }
234    let actual_source;
235    let source_ty = if let Value::Array(array) = value {
236        actual_source = array
237            .element_type_oid()
238            .map(|oid| engine.resolve_type_oid(oid))
239            .transpose()
240            .map_err(SQLError::Internal)?
241            .flatten()
242            .map(|element| control.copy_text(&ColumnType::Array(Box::new(element)).catalog_name()))
243            .transpose()?;
244        actual_source.as_deref().map(String::as_str).or(source_ty)
245    } else {
246        source_ty
247    };
248    let text_array = ColumnType::Array(Box::new(ColumnType::Text));
249    let labels = cast_catalog_array(value, source_ty, &text_array, engine, control)?;
250    cast_value_from_with_control(&labels, target_ty, Some("text[]"), control).map(Some)
251}
252
253fn physical_output_cast(
254    value: &Value,
255    source_ty: Option<&str>,
256    target_ty: &str,
257    target: &ColumnType,
258    engine: &dyn EngineHook,
259    control: &ProductionControl<'_>,
260) -> Result<Option<Produced<Value>>> {
261    let Value::Datum(datum) = value else {
262        return Ok(None);
263    };
264    if !is_string_type(target) {
265        return Ok(None);
266    }
267    let decoded =
268        super::super::datums::read_with_catalog_and_control(datum, Some(engine), control)?;
269    cast_value_with_type_resolution_with_control(
270        &decoded,
271        source_ty,
272        target_ty,
273        Some(engine),
274        control,
275    )
276    .map(Some)
277}
278
279/// Resolve catalog inputs at their external handoff, then admit SQL-owned names, element conversions and output before constructing them.
280pub fn cast_value_with_type_resolution_with_control(
281    value: &Value,
282    source_ty: Option<&str>,
283    target_ty: &str,
284    engine: Option<&dyn EngineHook>,
285    control: &ProductionControl<'_>,
286) -> Result<Produced<Value>> {
287    control.check()?;
288    let resolved_target = resolve_catalog_type_with_control(target_ty, engine, control)?;
289    control.check()?;
290    let resolved_source_type = source_ty
291        .map(|source| resolve_catalog_type_with_control(source, engine, control))
292        .transpose()?
293        .flatten();
294    let parsed_target = if resolved_target.is_some() {
295        None
296    } else {
297        optional_type_name(target_ty, control)?
298    };
299    let target_column_type = resolved_target.as_deref().or(parsed_target.as_deref());
300    let _date_order = datestyle::input_scope(value, target_column_type, engine, control)?;
301    let _interval_style = intervalstyle::cast_scope(value, target_column_type, engine, control)?;
302    if let (Some(engine), Some(target)) = (engine, resolved_target.as_deref()) {
303        if let Some(value) = record_identity_cast(
304            value,
305            resolved_source_type.as_deref(),
306            target,
307            engine,
308            control,
309        )? {
310            return Ok(value);
311        }
312        if let Some(output) =
313            physical_output_cast(value, source_ty, target_ty, target, engine, control)?
314        {
315            return Ok(output);
316        }
317        if let Some(value) = engine.cast_domain(value, source_ty, target)? {
318            return Ok(control.retain_external_value(value)?);
319        }
320        if let Some(value) = super::super::enums::cast_to_enum(
321            engine.enum_labels(),
322            value,
323            resolved_source_type.as_deref(),
324            target,
325        )? {
326            return Ok(control.retain_external_value(value)?);
327        }
328        if let Some(value) = super::super::composites::cast_to_composite(
329            engine,
330            value,
331            resolved_source_type.as_deref(),
332            target,
333        )? {
334            return Ok(control.retain_external_value(value)?);
335        }
336        if let Some(output) =
337            catalog_array_output(value, source_ty, target_ty, target, engine, control)?
338        {
339            return Ok(output);
340        }
341        if let Some((output, output_source)) = output_for_cast(
342            engine,
343            value,
344            source_ty,
345            resolved_source_type.as_deref(),
346            target,
347        )? {
348            return cast_value_with_type_resolution_with_control(
349                &output,
350                output_source,
351                target_ty,
352                Some(engine),
353                control,
354            );
355        }
356        control.check()?;
357        if matches!(target, ColumnType::Array(_))
358            && (requires_catalog_array_cast(target)
359                || super::super::enums::contains_enum_carrier(value)
360                || resolved_source_type
361                    .as_deref()
362                    .is_some_and(super::super::enums::is_enum_bearing))
363        {
364            return cast_catalog_array(value, source_ty, target, engine, control);
365        }
366    }
367    let resolved_source = resolved_source_type
368        .map(|ty| coercion_type_name_with_control(&ty, control))
369        .transpose()?;
370    let source_ty = resolved_source.as_deref().map(String::as_str).or(source_ty);
371    let target_name = resolved_target
372        .as_ref()
373        .map(|ty| coercion_type_name_with_control(ty, control))
374        .transpose()?;
375    let target_ty = target_name.as_ref().map_or(target_ty, |name| name.as_str());
376    cast_resolved_value(
377        value,
378        source_ty,
379        target_ty,
380        target_column_type,
381        engine,
382        control,
383    )
384}
385
386fn resolve_catalog_type_with_control(
387    name: &str,
388    engine: Option<&dyn EngineHook>,
389    control: &ProductionControl<'_>,
390) -> Result<Option<Produced<ColumnType>>> {
391    engine
392        .map(|engine| engine.resolve_type_name(name))
393        .transpose()
394        .map_err(SQLError::Internal)?
395        .flatten()
396        .map(|ty| {
397            ty.retain_external_with_control(control)
398                .map_err(SQLError::from)
399        })
400        .transpose()
401}
402
403fn record_identity_cast(
404    value: &Value,
405    resolved_source_type: Option<&ColumnType>,
406    target: &ColumnType,
407    engine: &dyn EngineHook,
408    control: &ProductionControl<'_>,
409) -> Result<Option<Produced<Value>>> {
410    if matches!(target, ColumnType::Record) {
411        // Composite-to-record coercion is a binary relabel: even malformed physical fields remain unread.
412        if let Value::Datum(datum) = value {
413            let ty = engine
414                .resolve_type_oid(datum.type_oid())
415                .map_err(SQLError::Internal)?;
416            if matches!(ty, Some(ColumnType::Composite(_))) {
417                return Ok(Some(control.copy_value(value)?));
418            }
419        }
420        if let (Value::Record(record), Some(ColumnType::Composite(source))) =
421            (value, resolved_source_type)
422        {
423            let (value, memory) = control.copy_value(value)?.into_parts();
424            let Value::Record(value) = value else {
425                unreachable!();
426            };
427            return Ok(Some(control.finish(
428                Value::Record(value.with_type_oid(Some(record.type_oid().unwrap_or(source.oid)))),
429                memory,
430            )?));
431        }
432    }
433
434    Ok(None)
435}
436
437fn cast_resolved_value(
438    value: &Value,
439    source_ty: Option<&str>,
440    target_ty: &str,
441    target_column_type: Option<&ColumnType>,
442    engine: Option<&dyn EngineHook>,
443    control: &ProductionControl<'_>,
444) -> Result<Produced<Value>> {
445    if target_column_type.is_some_and(regrole_array_type) {
446        let source_column_type = source_ty
447            .map(|name| optional_type_name(name, control))
448            .transpose()?
449            .flatten();
450        let source_name = source_column_type
451            .as_deref()
452            .map(array_leaf_type)
453            .map(|ty| ty.sql_name_with_control(control))
454            .transpose()?;
455        return cast_array(
456            value,
457            source_name.as_ref().map(|name| name.as_str()),
458            "regrole",
459            "regrole[]",
460            engine,
461            control,
462        );
463    }
464    // `find_coercion_pathway` casts an OID alias to a string type through its output function, as `CoerceViaIO`, and the string type then applies its own modifier; an array does so element by element.
465    if let Some(ColumnType::Array(element)) = target_column_type {
466        if is_string_type(element) {
467            if let Some(source_ty) = source_ty
468                .map(|source| optional_type_name(source, control))
469                .transpose()?
470                .flatten()
471            {
472                if let Some(elements) =
473                    format_regtype_elements_with_control(value, &source_ty, engine, control)?
474                {
475                    let (elements, _memory) = elements.into_parts();
476                    return cast_value_from_with_control(
477                        &Value::Array(elements),
478                        target_ty,
479                        Some("text[]"),
480                        control,
481                    );
482                }
483            }
484        }
485    }
486    let text_target = target_ty.eq_ignore_ascii_case("text");
487    if text_target || target_column_type.is_some_and(is_string_type) {
488        if let Some(source_ty) = source_ty
489            .map(|source| optional_type_name(source, control))
490            .transpose()?
491            .flatten()
492        {
493            if let Some(text) =
494                format_regtype_value_with_control(value, &source_ty, engine, control)?
495            {
496                let (text, memory) = text.into_parts();
497                if text_target {
498                    return Ok(control.finish(Value::Str(text), memory)?);
499                }
500                return cast_value_from_with_control(
501                    &Value::Str(text),
502                    target_ty,
503                    Some("text"),
504                    control,
505                );
506            }
507        }
508    }
509    if let (Some(engine), Value::Str(name) | Value::FixedChar(name)) = (engine, value) {
510        let oid = resolve_regobject_input(name, target_ty, target_column_type, engine, control)?;
511        if let Some(oid) = oid {
512            return Ok(control.finish(Value::Int(oid), control.empty_reservation())?);
513        }
514    }
515    let zoned = timezone::cast_local_timestamp(value, target_column_type, engine, control)?;
516    cast_value_from_with_control(
517        zoned.as_ref().unwrap_or(value),
518        target_ty,
519        source_ty,
520        control,
521    )
522}
523
524fn resolve_regobject_input(
525    name: &str,
526    target_ty: &str,
527    target_column_type: Option<&ColumnType>,
528    engine: &dyn EngineHook,
529    control: &ProductionControl<'_>,
530) -> Result<Option<i64>> {
531    enum ObjectKind {
532        Relation,
533        RoutineName,
534        Routine,
535        Role,
536        Namespace,
537        Collation,
538        Type,
539    }
540    let kind = if target_ty.eq_ignore_ascii_case("regclass") {
541        ObjectKind::Relation
542    } else if matches!(target_column_type, Some(ColumnType::Regproc))
543        || target_ty.eq_ignore_ascii_case("regproc")
544    {
545        ObjectKind::RoutineName
546    } else if target_ty.eq_ignore_ascii_case("regprocedure") {
547        ObjectKind::Routine
548    } else if target_ty.eq_ignore_ascii_case("regrole") {
549        ObjectKind::Role
550    } else if matches!(target_column_type, Some(ColumnType::Regnamespace)) {
551        ObjectKind::Namespace
552    } else if matches!(target_column_type, Some(ColumnType::Regcollation)) {
553        ObjectKind::Collation
554    } else if matches!(target_column_type, Some(ColumnType::Regtype)) {
555        ObjectKind::Type
556    } else {
557        return Ok(None);
558    };
559    let oid = match kind {
560        ObjectKind::Relation => engine.resolve_regclass_input(name)?,
561        // `regprocin` reports its own missing and ambiguous names.
562        ObjectKind::RoutineName => return engine.resolve_regproc(name),
563        ObjectKind::Routine => engine.resolve_regprocedure_input(name)?,
564        ObjectKind::Role => engine.resolve_regrole(name)?,
565        ObjectKind::Namespace => engine.resolve_regnamespace(name)?,
566        ObjectKind::Collation => engine.resolve_regcollation(name)?,
567        ObjectKind::Type => engine.resolve_regtype_input(name)?,
568    };
569    control.check()?;
570    if oid.is_some() || matches!(kind, ObjectKind::Type) {
571        return Ok(oid);
572    }
573    let (sqlstate, message) = match kind {
574        ObjectKind::Relation => ("42P01", format!("relation \"{name}\" does not exist")),
575        ObjectKind::Routine => ("42883", format!("function \"{name}\" does not exist")),
576        ObjectKind::Role => ("42704", format!("role \"{name}\" does not exist")),
577        ObjectKind::Namespace => ("3F000", format!("schema \"{name}\" does not exist")),
578        ObjectKind::Collation => (
579            "42704",
580            format!("collation \"{name}\" for encoding \"UTF8\" does not exist"),
581        ),
582        ObjectKind::Type | ObjectKind::RoutineName => {
583            unreachable!("regtype and regproc input return before reporting a missing object")
584        }
585    };
586    Err(SQLError::Routine {
587        sqlstate: sqlstate.into(),
588        message,
589    })
590}
591
592/// Whether an array cast needs the catalog for its elements: domains, enums, composites and the OID alias types whose input functions read catalog names. `regrole[]` has its own path above.
593fn requires_catalog_array_cast(ty: &ColumnType) -> bool {
594    match ty {
595        ColumnType::Domain { .. }
596        | ColumnType::Regtype
597        | ColumnType::Regclass
598        | ColumnType::Regproc
599        | ColumnType::Regprocedure
600        | ColumnType::Regcollation
601        | ColumnType::Regnamespace
602        | ColumnType::Enum(_)
603        | ColumnType::Composite(_)
604        | ColumnType::TimestampTz
605        | ColumnType::TimestampTzPrecision(_) => true,
606        ColumnType::Array(element) => requires_catalog_array_cast(element),
607        _ => false,
608    }
609}
610
611fn cast_catalog_array(
612    value: &Value,
613    source: Option<&str>,
614    target: &ColumnType,
615    engine: &dyn EngineHook,
616    control: &ProductionControl<'_>,
617) -> Result<Produced<Value>> {
618    if matches!(value, Value::Null) {
619        return Ok(control.finish(Value::Null, control.empty_reservation())?);
620    }
621    let source_element = source.map(|name| name.trim_end_matches("[]"));
622    let leaf = array_leaf_type(target);
623    let target_element = control.copy_text(&leaf.catalog_name())?;
624    let target_name = target.sql_name_with_control(control)?;
625    let output = cast_array(
626        value,
627        source_element,
628        &target_element,
629        &target_name,
630        Some(engine),
631        control,
632    )?;
633    let (Value::Array(array), memory) = output.into_parts() else {
634        return Err(SQLError::Internal(
635            "catalog array cast returned a non-array".into(),
636        ));
637    };
638    let oid = crate::catalog::type_metadata::pg_type_oid(leaf) as u32;
639    Ok(control.finish(Value::Array(array.with_element_type_oid(Some(oid))), memory)?)
640}
641
642fn cast_array(
643    value: &Value,
644    source: Option<&str>,
645    target_element: &str,
646    target_name: &str,
647    engine: Option<&dyn EngineHook>,
648    control: &ProductionControl<'_>,
649) -> Result<Produced<Value>> {
650    let parsed;
651    let array = match value {
652        Value::Array(array) => array,
653        Value::Str(text) => {
654            parsed = parse_pg_array_literal_with_control(text, control)?;
655            &parsed
656        }
657        other => {
658            return Err(SQLError::TypeMismatch(format!(
659                "CAST AS {target_name}: expected array, got {other:?}"
660            )))
661        }
662    };
663    let elements = cast_array_elements(array.elements(), source, target_element, engine, control)?;
664    let array = rebuild_array(array, elements, control)?
665        .ok_or_else(|| SQLError::TypeMismatch("array dimensions changed during cast".into()))?;
666    let (array, memory) = array.into_parts();
667    let oid = if target_element == "regrole" {
668        Some(crate::catalog::type_metadata::pg_type_oid(&ColumnType::Regrole) as u32)
669    } else {
670        array.element_type_oid()
671    };
672    Ok(control.finish(Value::Array(array.with_element_type_oid(oid)), memory)?)
673}
674
675fn cast_array_elements(
676    values: &[Value],
677    source: Option<&str>,
678    target: &str,
679    engine: Option<&dyn EngineHook>,
680    control: &ProductionControl<'_>,
681) -> Result<Produced<Vec<Value>>> {
682    let mut output = ProductionVec::new(*control);
683    output.reserve(values.len())?;
684    for value in values {
685        let value = match value {
686            Value::List(values) => {
687                let (values, memory) =
688                    cast_array_elements(values, source, target, engine, control)?.into_parts();
689                control.finish(Value::List(values), memory)?
690            }
691            value => cast_value_with_type_resolution_with_control(
692                value, source, target, engine, control,
693            )?,
694        };
695        output.push_produced(value)?;
696    }
697    Ok(output.finish()?)
698}
699
700pub(super) fn rebuild_array(
701    source: &ArrayValue,
702    elements: Produced<Vec<Value>>,
703    control: &ProductionControl<'_>,
704) -> Result<Option<Produced<ArrayValue>>> {
705    let mut bounds = ProductionVec::new(*control);
706    bounds.reserve(source.lower_bounds().len())?;
707    for bound in source.lower_bounds() {
708        bounds.push_copy(*bound)?;
709    }
710    ArrayValue::with_lower_bounds_with_control(elements, bounds.finish()?, control)?
711        .map(|array| {
712            let (array, memory) = array.into_parts();
713            control
714                .finish(
715                    array.with_element_type_oid(source.element_type_oid()),
716                    memory,
717                )
718                .map_err(Into::into)
719        })
720        .transpose()
721}
722
723#[cfg(test)]
724mod tests;