1use 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#[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#[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
46pub 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
89pub 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 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 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 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
533pub(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}