1use pg_query::NodeEnum;
9use pg_query::protobuf::{CreateRangeStmt, DefElem, Node, TypeName};
10
11use crate::identifier::{Identifier, QualifiedName};
12use crate::ir::user_type::{UserType, UserTypeKind};
13use crate::parse::builder::shared;
14use crate::parse::error::{ParseError, SourceLocation};
15
16#[allow(clippy::too_many_lines)] pub(crate) fn build_range(
25 stmt: &CreateRangeStmt,
26 default_schema: Option<&Identifier>,
27 location: &SourceLocation,
28) -> Result<UserType, ParseError> {
29 let qname = shared::qname_from_string_list(&stmt.type_name, default_schema, location)?;
30
31 let mut subtype: Option<QualifiedName> = None;
32 let mut subtype_opclass: Option<QualifiedName> = None;
33 let mut collation: Option<QualifiedName> = None;
34 let mut canonical: Option<QualifiedName> = None;
35 let mut subtype_diff: Option<QualifiedName> = None;
36 let mut multirange_type_name: Option<Identifier> = None;
37
38 for node in &stmt.params {
39 let Some(NodeEnum::DefElem(de)) = node.node.as_ref() else {
40 return Err(ParseError::Structural {
41 location: location.clone(),
42 message: format!(
43 "CREATE TYPE {qname} AS RANGE: unexpected non-DefElem in option list",
44 ),
45 });
46 };
47 match de.defname.as_str() {
48 "subtype" => {
49 if subtype.is_some() {
50 return Err(ParseError::Structural {
51 location: location.clone(),
52 message: format!(
53 "CREATE TYPE {qname} AS RANGE: duplicate `subtype` option",
54 ),
55 });
56 }
57 subtype = Some(qualified_name_from_defelem_typename(
58 de,
59 "subtype",
60 &qname,
61 default_schema,
62 location,
63 )?);
64 }
65 "subtype_opclass" => {
66 subtype_opclass = Some(qualified_name_from_defelem_typename(
67 de,
68 "subtype_opclass",
69 &qname,
70 default_schema,
71 location,
72 )?);
73 }
74 "collation" => {
75 collation = Some(qualified_name_from_defelem_typename(
76 de,
77 "collation",
78 &qname,
79 default_schema,
80 location,
81 )?);
82 }
83 "canonical" => {
84 canonical = Some(qualified_name_from_defelem_typename(
85 de,
86 "canonical",
87 &qname,
88 default_schema,
89 location,
90 )?);
91 }
92 "subtype_diff" => {
93 subtype_diff = Some(qualified_name_from_defelem_typename(
94 de,
95 "subtype_diff",
96 &qname,
97 default_schema,
98 location,
99 )?);
100 }
101 "multirange_type_name" => {
102 let qn = qualified_name_from_defelem_typename(
107 de,
108 "multirange_type_name",
109 &qname,
110 default_schema,
111 location,
112 )?;
113 if qn.schema != qname.schema {
114 return Err(ParseError::Structural {
115 location: location.clone(),
116 message: format!(
117 "CREATE TYPE {qname} AS RANGE: multirange_type_name must live in \
118 the same schema as the range type ({})",
119 qname.schema.as_str(),
120 ),
121 });
122 }
123 multirange_type_name = Some(qn.name);
124 }
125 other => {
126 return Err(ParseError::Structural {
127 location: location.clone(),
128 message: format!("CREATE TYPE {qname} AS RANGE: unknown option `{other}`"),
129 });
130 }
131 }
132 }
133
134 let subtype = subtype.ok_or_else(|| ParseError::Structural {
135 location: location.clone(),
136 message: format!("CREATE TYPE {qname} AS RANGE: missing required `subtype` option"),
137 })?;
138
139 Ok(UserType {
140 qname,
141 kind: UserTypeKind::Range {
142 subtype,
143 subtype_opclass,
144 collation,
145 canonical,
146 subtype_diff,
147 multirange_type_name,
148 },
149 comment: None,
150 owner: None,
151 grants: vec![],
152 })
153}
154
155fn qualified_name_from_defelem_typename(
161 de: &DefElem,
162 option_name: &str,
163 range_qname: &QualifiedName,
164 default_schema: Option<&Identifier>,
165 location: &SourceLocation,
166) -> Result<QualifiedName, ParseError> {
167 let arg_node =
168 de.arg.as_ref().ok_or_else(|| ParseError::Structural {
169 location: location.clone(),
170 message: format!(
171 "CREATE TYPE {range_qname} AS RANGE: option `{option_name}` has no value",
172 ),
173 })?;
174 let inner = arg_node
175 .node
176 .as_ref()
177 .ok_or_else(|| ParseError::Structural {
178 location: location.clone(),
179 message: format!(
180 "CREATE TYPE {range_qname} AS RANGE: option `{option_name}` value node empty",
181 ),
182 })?;
183 let typename: &TypeName = match inner {
184 NodeEnum::TypeName(t) => t,
185 _ => {
186 return Err(ParseError::Structural {
187 location: location.clone(),
188 message: format!(
189 "CREATE TYPE {range_qname} AS RANGE: option `{option_name}` value must be a \
190 type/function name, not {kind}",
191 kind = node_kind_label(inner),
192 ),
193 });
194 }
195 };
196 qname_from_typename_strings(&typename.names, default_schema, location)
197}
198
199fn qname_from_typename_strings(
203 nodes: &[Node],
204 default_schema: Option<&Identifier>,
205 location: &SourceLocation,
206) -> Result<QualifiedName, ParseError> {
207 let parts: Vec<&str> = nodes
208 .iter()
209 .filter_map(|n| match n.node.as_ref() {
210 Some(NodeEnum::String(s)) => Some(s.sval.as_str()),
211 _ => None,
212 })
213 .collect();
214 match parts.as_slice() {
215 [name] => {
216 if is_builtin_scalar(name) {
219 Ok(QualifiedName::new(
220 shared::ident("pg_catalog", location)?,
221 shared::ident(name, location)?,
222 ))
223 } else {
224 let schema =
225 default_schema
226 .cloned()
227 .ok_or_else(|| ParseError::UnqualifiedName {
228 location: location.clone(),
229 })?;
230 Ok(QualifiedName::new(schema, shared::ident(name, location)?))
231 }
232 }
233 [schema, name] => Ok(QualifiedName::new(
234 shared::ident(schema, location)?,
235 shared::ident(name, location)?,
236 )),
237 _ => Err(ParseError::Structural {
238 location: location.clone(),
239 message: format!(
240 "expected 1 or 2 String nodes in qualified name, got {}",
241 nodes.len(),
242 ),
243 }),
244 }
245}
246
247fn is_builtin_scalar(name: &str) -> bool {
251 matches!(
252 name,
253 "int2"
254 | "int4"
255 | "int8"
256 | "smallint"
257 | "integer"
258 | "bigint"
259 | "numeric"
260 | "decimal"
261 | "real"
262 | "double precision"
263 | "float4"
264 | "float8"
265 | "text"
266 | "varchar"
267 | "char"
268 | "bpchar"
269 | "bytea"
270 | "date"
271 | "time"
272 | "timestamp"
273 | "timestamptz"
274 | "interval"
275 | "uuid"
276 | "bool"
277 | "boolean"
278 | "jsonb"
279 | "json"
280 | "int4_ops"
281 | "int8_ops"
282 | "text_ops"
283 | "timestamp_ops"
284 | "timestamptz_ops"
285 | "date_ops"
286 | "numeric_ops"
287 | "C"
288 | "POSIX"
289 | "default"
290 )
291}
292
293const fn node_kind_label(node: &NodeEnum) -> &'static str {
294 match node {
295 NodeEnum::String(_) => "String",
296 NodeEnum::TypeName(_) => "TypeName",
297 NodeEnum::Integer(_) => "Integer",
298 NodeEnum::Float(_) => "Float",
299 NodeEnum::AConst(_) => "AConst",
300 _ => "other",
301 }
302}
303
304#[cfg(test)]
305mod tests {
306 use super::*;
307 use std::path::PathBuf;
308
309 fn loc() -> SourceLocation {
310 SourceLocation::new(PathBuf::from("test.sql"), 1, 1)
311 }
312
313 fn parse_range(sql: &str) -> CreateRangeStmt {
314 let parsed = pg_query::parse(sql).expect("parses");
315 let node = parsed
316 .protobuf
317 .stmts
318 .into_iter()
319 .next()
320 .and_then(|r| r.stmt)
321 .and_then(|n| n.node)
322 .expect("stmt");
323 let NodeEnum::CreateRangeStmt(s) = node else {
324 panic!("expected CreateRangeStmt, got: {node:?}");
325 };
326 s
327 }
328
329 fn build(sql: &str) -> UserType {
330 let stmt = parse_range(sql);
331 build_range(&stmt, None, &loc()).expect("build_range")
332 }
333
334 fn try_build(sql: &str) -> Result<UserType, ParseError> {
335 let stmt = parse_range(sql);
336 build_range(&stmt, None, &loc())
337 }
338
339 #[test]
340 fn parse_range_with_subtype_only() {
341 let ut = build("CREATE TYPE app.tsrange_co AS RANGE (subtype = timestamptz);");
342 assert_eq!(ut.qname.to_string(), "app.tsrange_co");
343 let UserTypeKind::Range {
344 subtype,
345 subtype_opclass,
346 collation,
347 canonical,
348 subtype_diff,
349 multirange_type_name,
350 } = &ut.kind
351 else {
352 panic!("not range kind");
353 };
354 assert_eq!(subtype.to_string(), "pg_catalog.timestamptz");
355 assert!(subtype_opclass.is_none());
356 assert!(collation.is_none());
357 assert!(canonical.is_none());
358 assert!(subtype_diff.is_none());
359 assert!(multirange_type_name.is_none());
360 }
361
362 #[test]
363 fn parse_range_subtype_in_pg_catalog() {
364 let ut = build("CREATE TYPE app.r AS RANGE (subtype = int4);");
365 let UserTypeKind::Range { subtype, .. } = &ut.kind else {
366 panic!("not range");
367 };
368 assert_eq!(subtype.schema.as_str(), "pg_catalog");
369 assert_eq!(subtype.name.as_str(), "int4");
370 }
371
372 #[test]
373 fn parse_range_with_opclass_and_multirange_name() {
374 let ut = build(
375 "CREATE TYPE app.r AS RANGE (subtype = int4, subtype_opclass = int4_ops, multirange_type_name = app.r_mr);",
376 );
377 let UserTypeKind::Range {
378 subtype_opclass,
379 multirange_type_name,
380 ..
381 } = &ut.kind
382 else {
383 panic!("not range");
384 };
385 assert_eq!(
386 subtype_opclass.as_ref().map(ToString::to_string),
387 Some("pg_catalog.int4_ops".into())
388 );
389 assert_eq!(
390 multirange_type_name
391 .as_ref()
392 .map(|i| i.as_str().to_string()),
393 Some("r_mr".into()),
394 );
395 }
396
397 #[test]
398 fn parse_range_rejects_unknown_option() {
399 let err =
400 try_build("CREATE TYPE app.bad AS RANGE (subtype = int4, bogus = 1);").unwrap_err();
401 let msg = match &err {
402 ParseError::Structural { message, .. } => message.clone(),
403 other => panic!("expected Structural, got {other:?}"),
404 };
405 assert!(msg.contains("bogus"), "expected bad key in: {msg}");
406 }
407
408 #[test]
409 fn parse_range_rejects_missing_subtype() {
410 let err = try_build("CREATE TYPE app.bad AS RANGE (multirange_type_name = app.bad_mr);")
411 .unwrap_err();
412 let msg = match &err {
413 ParseError::Structural { message, .. } => message.clone(),
414 other => panic!("expected Structural, got {other:?}"),
415 };
416 assert!(msg.contains("subtype"), "expected subtype in: {msg}");
417 }
418
419 #[test]
420 fn parse_range_rejects_duplicate_subtype() {
421 let err = try_build("CREATE TYPE app.bad AS RANGE (subtype = int4, subtype = int8);")
422 .unwrap_err();
423 assert!(matches!(err, ParseError::Structural { .. }));
424 }
425
426 #[test]
427 fn parse_range_rejects_multirange_in_different_schema() {
428 let err = try_build(
429 "CREATE TYPE app.r AS RANGE (subtype = int4, multirange_type_name = other.r_mr);",
430 )
431 .unwrap_err();
432 assert!(matches!(err, ParseError::Structural { .. }));
433 }
434
435 #[test]
436 fn parse_range_unqualified_with_default_schema() {
437 let stmt = parse_range("CREATE TYPE r AS RANGE (subtype = int4);");
438 let app = Identifier::from_unquoted("app").unwrap();
439 let ut = build_range(&stmt, Some(&app), &loc()).unwrap();
440 assert_eq!(ut.qname.to_string(), "app.r");
441 }
442
443 #[test]
444 fn parse_range_unqualified_without_schema_errors() {
445 let stmt = parse_range("CREATE TYPE r AS RANGE (subtype = int4);");
446 let err = build_range(&stmt, None, &loc()).unwrap_err();
447 assert!(matches!(err, ParseError::UnqualifiedName { .. }));
448 }
449}