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