Skip to main content

uqa_sql/compiler/
types.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Column type and foreign-key constraint lowering helpers.
8
9use pg_query::protobuf::Node;
10use pg_query::NodeEnum;
11
12use crate::ast::{ColumnType, IntervalFields, RangeSubtype};
13use crate::error::{Result, SQLError};
14
15use super::tree::extract_string;
16
17mod declarations;
18mod object_names;
19pub(crate) use declarations::compile_retained_type_declaration;
20pub(crate) use declarations::compile_retained_type_reference;
21pub(super) use declarations::preserve_alter_type_declaration;
22pub use object_names::parse_regobject_name;
23pub(crate) use object_names::parse_regobject_name_with_control;
24
25/// Parser-normalized type identity used by `PostgreSQL`'s `regtype` and `regprocedure` input functions. Components retain the parser's distinction between aliases such as unquoted `integer` (normalized to `pg_catalog.int4`) and a quoted type named `"integer"`. Modifier values do not participate in OID lookup; their presence is retained for declaration validation.
26#[derive(Debug, Clone, PartialEq, Eq)]
27pub struct ParsedRegtypeName {
28    pub names: Vec<String>,
29    pub array_dimensions: usize,
30    pub has_type_modifiers: bool,
31}
32
33/// Parser-normalized routine name and optional exact input-type signature used by `PostgreSQL`'s `regproc` and `regprocedure` input functions.
34#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct ParsedRegprocedureName {
36    pub names: Vec<String>,
37    pub argument_types: Option<Vec<ParsedRegtypeName>>,
38}
39
40const POSTGRES_FUNCTION_MAX_ARGUMENTS: usize = 100;
41
42fn scanner_isspace(byte: u8) -> bool {
43    matches!(byte, b' ' | b'\t' | b'\n' | b'\r' | 0x0b | 0x0c)
44}
45
46/// Parse exactly one `PostgreSQL` type-name string without accepting adjacent SQL expressions. `None` is the soft-failure shape used for inputs such as an empty string or `SETOF integer`; lexical and type-name syntax errors are retained for the caller.
47pub fn parse_regtype_name(input: &str) -> Result<Option<ParsedRegtypeName>> {
48    if input.bytes().all(scanner_isspace) {
49        return Ok(None);
50    }
51    let parsed = crate::parser::parse_with_mode(input, pg_query::ParseMode::TypeName)?;
52    let [raw] = parsed.protobuf.stmts.as_slice() else {
53        return Ok(None);
54    };
55    let Some(NodeEnum::List(names)) = raw.stmt.as_ref().and_then(|node| node.node.as_ref()) else {
56        return Ok(None);
57    };
58    let names = names
59        .items
60        .iter()
61        .map(extract_string)
62        .collect::<Result<Vec<_>>>()?;
63    if names.is_empty() {
64        return Ok(None);
65    }
66    let scanned = crate::parser::scan(input)?;
67    let tokens = scanned
68        .tokens
69        .iter()
70        .filter_map(|token| pg_query::protobuf::Token::try_from(token.token).ok())
71        .collect::<Vec<_>>();
72    if tokens.contains(&pg_query::protobuf::Token::Setof) {
73        return Ok(None);
74    }
75    let bracket_dimensions = tokens
76        .iter()
77        .filter(|token| **token == pg_query::protobuf::Token::Ascii91)
78        .count();
79    let array_dimensions = bracket_dimensions.max(usize::from(
80        tokens.contains(&pg_query::protobuf::Token::Array),
81    ));
82    Ok(Some(ParsedRegtypeName {
83        names,
84        array_dimensions,
85        has_type_modifiers: tokens.contains(&pg_query::protobuf::Token::Ascii40),
86    }))
87}
88
89/// Parse one routine-name string with either an omitted signature (`regproc`) or an exact signature (`regprocedure`). The caller owns the soft-error and cross-database policy because the two SQL input functions intentionally differ from ordinary DDL.
90pub fn parse_regprocedure_name(input: &str) -> Result<Option<ParsedRegprocedureName>> {
91    let mut in_quote = false;
92    let left_parenthesis = input.bytes().enumerate().find_map(|(offset, byte)| {
93        if byte == b'"' {
94            in_quote = !in_quote;
95            None
96        } else if byte == b'(' && !in_quote {
97            Some(offset)
98        } else {
99            None
100        }
101    });
102    let Some(left_parenthesis) = left_parenthesis else {
103        return parse_regobject_name(input)
104            .map(|names| {
105                Some(ParsedRegprocedureName {
106                    names,
107                    argument_types: None,
108                })
109            })
110            .ok_or_else(|| regprocedure_syntax_error("expected a left parenthesis".into()));
111    };
112    let Some(names) = parse_regobject_name(&input[..left_parenthesis]) else {
113        return Ok(None);
114    };
115
116    let bytes = input.as_bytes();
117    let mut end = bytes.len();
118    while end > left_parenthesis + 1 && scanner_isspace(bytes[end - 1]) {
119        end -= 1;
120    }
121    if end <= left_parenthesis + 1 || bytes[end - 1] != b')' {
122        return Err(regprocedure_syntax_error(format!(
123            "expected a right parenthesis in routine identity \"{input}\""
124        )));
125    }
126    let arguments = &input[left_parenthesis + 1..end - 1];
127    let argument_types = parse_regprocedure_argument_types(input, arguments)?;
128
129    Ok(Some(ParsedRegprocedureName {
130        names,
131        argument_types: Some(argument_types),
132    }))
133}
134
135fn parse_regprocedure_argument_types(
136    input: &str,
137    arguments: &str,
138) -> Result<Vec<ParsedRegtypeName>> {
139    let argument_bytes = arguments.as_bytes();
140    let mut argument_types = Vec::new();
141    let mut offset = 0usize;
142    let mut had_comma = false;
143    loop {
144        while argument_bytes
145            .get(offset)
146            .is_some_and(|byte| scanner_isspace(*byte))
147        {
148            offset += 1;
149        }
150        if offset == argument_bytes.len() {
151            if had_comma {
152                return Err(regprocedure_syntax_error(format!(
153                    "expected a type name in routine identity \"{input}\""
154                )));
155            }
156            break;
157        }
158
159        let start = offset;
160        let mut quoted = false;
161        let mut nesting = 0i32;
162        while let Some(byte) = argument_bytes.get(offset).copied() {
163            if byte == b'"' {
164                quoted = !quoted;
165            } else if byte == b',' && !quoted && nesting == 0 {
166                break;
167            } else if !quoted {
168                match byte {
169                    b'(' | b'[' => nesting += 1,
170                    b')' | b']' => nesting -= 1,
171                    _ => {}
172                }
173            }
174            offset += 1;
175        }
176        if quoted || nesting != 0 {
177            return Err(regprocedure_syntax_error(format!(
178                "improper type name in routine identity \"{input}\""
179            )));
180        }
181        let mut type_end = offset;
182        while type_end > start && scanner_isspace(argument_bytes[type_end - 1]) {
183            type_end -= 1;
184        }
185        let Some(type_name) = parse_regtype_name(&arguments[start..type_end])? else {
186            return Err(SQLError::Parse(format!(
187                "invalid type name in routine identity \"{input}\""
188            )));
189        };
190        if argument_types.len() == POSTGRES_FUNCTION_MAX_ARGUMENTS {
191            return Err(SQLError::Routine {
192                sqlstate: "54023".into(),
193                message: format!("too many arguments in routine identity \"{input}\""),
194            });
195        }
196        argument_types.push(type_name);
197        had_comma = argument_bytes.get(offset) == Some(&b',');
198        if had_comma {
199            offset += 1;
200        }
201    }
202
203    Ok(argument_types)
204}
205
206fn regprocedure_syntax_error(message: String) -> SQLError {
207    SQLError::Routine {
208        sqlstate: "22P02".into(),
209        message,
210    }
211}
212
213pub(super) fn compile_foreign_key_action(raw: &str) -> Result<crate::ast::ForeignKeyAction> {
214    use crate::ast::ForeignKeyAction;
215    match raw.as_bytes().first().copied() {
216        None | Some(0) | Some(b'a') => Ok(ForeignKeyAction::NoAction),
217        Some(b'r') => Ok(ForeignKeyAction::Restrict),
218        Some(b'c') => Ok(ForeignKeyAction::Cascade),
219        Some(b'n') => Ok(ForeignKeyAction::SetNull),
220        Some(b'd') => Ok(ForeignKeyAction::SetDefault),
221        Some(other) => Err(SQLError::Unsupported(format!(
222            "unsupported FOREIGN KEY action byte {other:?}"
223        ))),
224    }
225}
226
227pub(super) fn compile_foreign_key_match(raw: &str) -> Result<crate::ast::ForeignKeyMatch> {
228    use crate::ast::ForeignKeyMatch;
229    match raw.as_bytes().first().copied() {
230        None | Some(0) | Some(b's') => Ok(ForeignKeyMatch::Simple),
231        Some(b'f') => Ok(ForeignKeyMatch::Full),
232        Some(b'p') => Err(SQLError::Unsupported(
233            "FOREIGN KEY MATCH PARTIAL is not implemented by PostgreSQL".into(),
234        )),
235        Some(other) => Err(SQLError::Unsupported(format!(
236            "unsupported FOREIGN KEY match byte {other:?}"
237        ))),
238    }
239}
240
241pub(super) fn validate_foreign_key_set_columns(
242    local_columns: &[String],
243    set_columns: &[String],
244    raw_delete_action: &str,
245) -> Result<()> {
246    if set_columns.is_empty() {
247        return Ok(());
248    }
249    let action = compile_foreign_key_action(raw_delete_action)?;
250    if !matches!(
251        action,
252        crate::ast::ForeignKeyAction::SetNull | crate::ast::ForeignKeyAction::SetDefault
253    ) {
254        return Err(SQLError::Unsupported(
255            "FOREIGN KEY column lists are only valid for ON DELETE SET NULL/DEFAULT".into(),
256        ));
257    }
258    for col in set_columns {
259        if !local_columns.iter().any(|local| local == col) {
260            return Err(SQLError::Unsupported(format!(
261                "FOREIGN KEY SET column `{col}` is not part of the local key"
262            )));
263        }
264    }
265    Ok(())
266}
267
268pub(super) fn raw_type_name(col: &pg_query::protobuf::ColumnDef) -> Result<Option<String>> {
269    let Some(type_name) = col.type_name.as_ref() else {
270        return Ok(None);
271    };
272    let names = type_name
273        .names
274        .iter()
275        .map(extract_string)
276        .collect::<Result<Vec<_>>>()?;
277    Ok(names.last().map(|name| name.to_lowercase()))
278}
279
280pub(super) fn compile_type_name(col: &pg_query::protobuf::ColumnDef) -> Result<ColumnType> {
281    let Some(type_name) = col.type_name.as_ref() else {
282        return Err(SQLError::Internal(format!(
283            "column `{}` has no type",
284            col.colname
285        )));
286    };
287    compile_pg_type_name(type_name, &col.colname)
288}
289
290#[expect(
291    clippy::too_many_lines,
292    reason = "ordered PostgreSQL lowering preserves syntax and error precedence"
293)]
294pub(super) fn compile_pg_type_name(
295    type_name: &pg_query::protobuf::TypeName,
296    column_name: &str,
297) -> Result<ColumnType> {
298    let names = type_name
299        .names
300        .iter()
301        .map(extract_string)
302        .collect::<Result<Vec<_>>>()?;
303    let raw = names
304        .last()
305        .ok_or_else(|| {
306            SQLError::Internal(format!(
307                "type name for `{column_name}` has no name components"
308            ))
309        })?
310        .to_lowercase();
311    let bind_named = names
312        .first()
313        .is_some_and(|schema| names.len() > 1 && schema != "pg_catalog")
314        || names.last().is_some_and(|name| *name != raw)
315        || matches!(raw.as_str(), "integer" | "smallint" | "bigint" | "boolean");
316    let base = if bind_named {
317        compile_named_type(&names, type_name)
318    } else {
319        match raw.as_str() {
320            "smallint" | "int2" | "smallserial" | "serial2" => Ok(ColumnType::SmallInteger),
321            "int" | "int4" | "integer" | "serial" | "serial4" => Ok(ColumnType::Integer),
322            "bigint" | "int8" | "bigserial" | "serial8" => Ok(ColumnType::BigInteger),
323            "oid" => Ok(ColumnType::Oid),
324            "xid" => Ok(ColumnType::Xid),
325            "void" => Ok(ColumnType::Void),
326            "text" => Ok(ColumnType::Text),
327            "refcursor" => Ok(ColumnType::RefCursor),
328            "name" => Ok(ColumnType::Name),
329            "uuid" => Ok(ColumnType::Uuid),
330            "varchar" | "character varying" => {
331                if type_name.typmods.len() > 1 {
332                    return Err(SQLError::TypeMismatch(format!(
333                        "CHARACTER VARYING accepts at most one length modifier, got {}",
334                        type_name.typmods.len()
335                    )));
336                }
337                let length = type_name
338                    .typmods
339                    .first()
340                    .map(|node| expect_positive_character_length(node, "varchar"))
341                    .transpose()?;
342                Ok(ColumnType::Varchar(length))
343            }
344            "char" => Ok(ColumnType::InternalChar),
345            "character" | "bpchar" => {
346                if type_name.typmods.len() > 1 {
347                    return Err(SQLError::TypeMismatch(format!(
348                        "CHARACTER accepts at most one length modifier, got {}",
349                        type_name.typmods.len()
350                    )));
351                }
352                let length = type_name
353                    .typmods
354                    .first()
355                    .map(|node| expect_positive_character_length(node, "bpchar"))
356                    .transpose()?
357                    .unwrap_or(1);
358                Ok(ColumnType::Character(length))
359            }
360            "bool" | "boolean" => Ok(ColumnType::Boolean),
361            "real" | "float4" => Ok(ColumnType::Real),
362            "float8" | "double" | "double precision" => Ok(ColumnType::DoublePrecision),
363            "numeric" | "decimal" => {
364                if type_name.typmods.len() > 2 {
365                    return Err(SQLError::Routine {
366                        sqlstate: "22023".into(),
367                        message: "invalid NUMERIC type modifier".into(),
368                    });
369                }
370                let mut typmods_iter = type_name.typmods.iter();
371                let precision = typmods_iter
372                    .next()
373                    .map(|n| {
374                        let value = expect_integer_const(n)?;
375                        if !(1..=1000).contains(&value) {
376                            return Err(SQLError::Routine {
377                                sqlstate: "22023".into(),
378                                message: format!(
379                                    "NUMERIC precision {value} must be between 1 and 1000"
380                                ),
381                            });
382                        }
383                        Ok(value as u32)
384                    })
385                    .transpose()?;
386                let scale = typmods_iter
387                    .next()
388                    .map(|n| {
389                        let value = expect_integer_const(n)?;
390                        if !(-1000..=1000).contains(&value) {
391                            return Err(SQLError::Routine {
392                                sqlstate: "22023".into(),
393                                message: format!(
394                                    "NUMERIC scale {value} must be between -1000 and 1000"
395                                ),
396                            });
397                        }
398                        Ok(value as i32)
399                    })
400                    .transpose()?;
401                // PostgreSQL semantics: NUMERIC(precision) without an
402                // explicit scale defaults to scale=0, rounding to integers.
403                let scale = scale.or(precision.map(|_| 0));
404                Ok(ColumnType::Numeric { precision, scale })
405            }
406            "date" => Ok(ColumnType::Date),
407            "time" | "time without time zone" => Ok(ColumnType::Time),
408            "timetz" | "time with time zone" => Ok(ColumnType::TimeTz),
409            "timestamp" | "datetime" | "timestamp without time zone" => Ok(ColumnType::Timestamp),
410            "timestamptz" | "timestamp with time zone" => Ok(ColumnType::TimestampTz),
411            "interval" => {
412                let fields = type_name
413                    .typmods
414                    .first()
415                    .map(expect_integer_const)
416                    .transpose()?
417                    .unwrap_or(32767);
418                let fields = IntervalFields::from_modifier_mask(fields)
419                    .ok_or_else(|| SQLError::TypeMismatch("invalid interval fields".into()))?;
420                let precision = type_name
421                    .typmods
422                    .get(1)
423                    .map(expect_integer_const)
424                    .transpose()?;
425                ColumnType::with_interval_modifiers(fields, precision)
426            }
427            "int4range" => Ok(ColumnType::Range(RangeSubtype::Integer)),
428            "int8range" => Ok(ColumnType::Range(RangeSubtype::BigInteger)),
429            "numrange" => Ok(ColumnType::Range(RangeSubtype::Numeric)),
430            "daterange" => Ok(ColumnType::Range(RangeSubtype::Date)),
431            "tsrange" => Ok(ColumnType::Range(RangeSubtype::Timestamp)),
432            "tstzrange" => Ok(ColumnType::Range(RangeSubtype::TimestampTz)),
433            "int4multirange" => Ok(ColumnType::Multirange(RangeSubtype::Integer)),
434            "int8multirange" => Ok(ColumnType::Multirange(RangeSubtype::BigInteger)),
435            "nummultirange" => Ok(ColumnType::Multirange(RangeSubtype::Numeric)),
436            "datemultirange" => Ok(ColumnType::Multirange(RangeSubtype::Date)),
437            "tsmultirange" => Ok(ColumnType::Multirange(RangeSubtype::Timestamp)),
438            "tstzmultirange" => Ok(ColumnType::Multirange(RangeSubtype::TimestampTz)),
439            "json" => Ok(ColumnType::Json),
440            "jsonb" => Ok(ColumnType::JsonB),
441            "bytea" => Ok(ColumnType::Bytea),
442            "regproc" => Ok(ColumnType::Regproc),
443            "regprocedure" => Ok(ColumnType::Regprocedure),
444            "regclass" => Ok(ColumnType::Regclass),
445            "regcollation" => Ok(ColumnType::Regcollation),
446            "regnamespace" => Ok(ColumnType::Regnamespace),
447            "regrole" => Ok(ColumnType::Regrole),
448            "regtype" => Ok(ColumnType::Regtype),
449            "pg_node_tree" => Ok(ColumnType::PgNodeTree),
450            "aclitem" => Ok(ColumnType::AclItem),
451            "int2vector" => Ok(ColumnType::Int2Vector),
452            "oidvector" => Ok(ColumnType::OidVector),
453            "anyarray" => Ok(ColumnType::AnyArray),
454            "record" => Ok(ColumnType::Record),
455            "vector" => {
456                // VECTOR(N): the dimension is the only typmod argument.
457                let [arg] = type_name.typmods.as_slice() else {
458                    return Err(SQLError::Unsupported(
459                        "VECTOR requires exactly one dimension".into(),
460                    ));
461                };
462                let raw_dim = expect_integer_const(arg)?;
463                let dim = u32::try_from(raw_dim).map_err(|_| {
464                    SQLError::TypeMismatch(format!(
465                        "VECTOR dimension must be between 1 and {}, got {raw_dim}",
466                        u32::MAX
467                    ))
468                })?;
469                if dim == 0 {
470                    return Err(SQLError::TypeMismatch(
471                        "VECTOR dimension must be greater than zero".into(),
472                    ));
473                }
474                Ok(ColumnType::Vector(dim))
475            }
476            "tensor" => {
477                // TENSOR(N): an array of N-dimensional vectors.
478                let [arg] = type_name.typmods.as_slice() else {
479                    return Err(SQLError::Unsupported(
480                        "TENSOR requires exactly one dimension".into(),
481                    ));
482                };
483                let raw_dim = expect_integer_const(arg)?;
484                let dim = u32::try_from(raw_dim).map_err(|_| {
485                    SQLError::TypeMismatch(format!(
486                        "TENSOR dimension must be between 1 and {}, got {raw_dim}",
487                        u32::MAX
488                    ))
489                })?;
490                if dim == 0 {
491                    return Err(SQLError::TypeMismatch(
492                        "TENSOR dimension must be greater than zero".into(),
493                    ));
494                }
495                Ok(ColumnType::Tensor(dim))
496            }
497            _ => compile_named_type(&names, type_name),
498        }
499    }?;
500    let base = if matches!(
501        base,
502        ColumnType::Time | ColumnType::TimeTz | ColumnType::Timestamp | ColumnType::TimestampTz
503    ) {
504        if type_name.typmods.len() > 1 {
505            return Err(SQLError::TypeMismatch(
506                "invalid temporal type modifier".into(),
507            ));
508        }
509        base.with_temporal_precision(
510            type_name
511                .typmods
512                .first()
513                .map(expect_integer_const)
514                .transpose()?,
515        )?
516    } else {
517        base
518    };
519    if matches!(base, ColumnType::Void | ColumnType::PgNodeTree)
520        && !type_name.array_bounds.is_empty()
521    {
522        return Err(SQLError::Routine {
523            sqlstate: "42704".into(),
524            message: format!("type \"{}[]\" does not exist", base.sql_name()),
525        });
526    }
527    Ok(type_name
528        .array_bounds
529        .iter()
530        .fold(base, |element, _| ColumnType::Array(Box::new(element))))
531}
532
533/// Resolve a type reference without interpreting column-only serial pseudo-types.
534pub(super) fn compile_pg_type_reference(
535    type_name: &pg_query::protobuf::TypeName,
536    context: &str,
537) -> Result<ColumnType> {
538    let names = type_name
539        .names
540        .iter()
541        .map(extract_string)
542        .collect::<Result<Vec<_>>>()?;
543    if names.last().is_some_and(|name| {
544        matches!(
545            name.as_str(),
546            "serial" | "serial2" | "serial4" | "serial8" | "smallserial" | "bigserial"
547        )
548    }) {
549        let base = compile_named_type(&names, type_name)?;
550        return Ok(type_name
551            .array_bounds
552            .iter()
553            .fold(base, |element, _| ColumnType::Array(Box::new(element))));
554    }
555    compile_pg_type_name(type_name, context)
556}
557
558fn compile_named_type(
559    names: &[String],
560    type_name: &pg_query::protobuf::TypeName,
561) -> Result<ColumnType> {
562    let mut name = names
563        .iter()
564        .map(|name| format!("\"{}\"", name.replace('"', "\"\"")))
565        .collect::<Vec<_>>()
566        .join(".");
567    if !type_name.typmods.is_empty() {
568        let modifiers = type_name
569            .typmods
570            .iter()
571            .map(expect_integer_const)
572            .collect::<Result<Vec<_>>>()?;
573        name.push('(');
574        name.push_str(
575            &modifiers
576                .iter()
577                .map(ToString::to_string)
578                .collect::<Vec<_>>()
579                .join(","),
580        );
581        name.push(')');
582    }
583    Ok(ColumnType::Named(name))
584}
585
586fn expect_positive_character_length(node: &Node, type_name: &str) -> Result<u32> {
587    let length = expect_integer_const(node)?;
588    u32::try_from(length)
589        .ok()
590        .filter(|length| *length > 0)
591        .ok_or_else(|| SQLError::Routine {
592            sqlstate: "22023".into(),
593            message: format!("length for type {type_name} must be at least 1"),
594        })
595}
596
597fn expect_integer_const(node: &Node) -> Result<i64> {
598    let Some(inner) = node.node.as_ref() else {
599        return Err(SQLError::Internal("missing const node".into()));
600    };
601    match inner {
602        NodeEnum::AConst(c) => match &c.val {
603            Some(pg_query::protobuf::a_const::Val::Ival(i)) => Ok(i64::from(i.ival)),
604            Some(pg_query::protobuf::a_const::Val::Fval(f)) => {
605                f.fval.parse::<i64>().map_err(|_| {
606                    SQLError::TypeMismatch(format!(
607                        "type modifier must be an integer, got `{}`",
608                        f.fval
609                    ))
610                })
611            }
612            other => Err(SQLError::Internal(format!(
613                "expected integer constant, got {other:?}"
614            ))),
615        },
616        _ => Err(SQLError::Internal(format!(
617            "expected A_Const, got {inner:?}"
618        ))),
619    }
620}