Skip to main content

squawk_ide/
column_name.rs

1use squawk_syntax::{
2    SyntaxKind, SyntaxNode,
3    ast::{self, AstNode},
4};
5
6use crate::quote::normalize_identifier;
7
8#[derive(Clone, Debug, PartialEq)]
9pub(crate) enum ColumnName {
10    Column(String),
11    /// There's a fallback mechanism that we need to propagate through the
12    /// expressions/types.
13    //
14    /// We can see this with:
15    /// ```sql
16    /// select case when true then 'a' else now()::text end;
17    /// -- column named `now`, propagating the function name
18    /// -- vs
19    /// select case when true then 'a' else 'b' end;
20    /// -- column named `case`
21    /// ```
22    UnknownColumn(Option<String>),
23    Star,
24}
25
26impl ColumnName {
27    // Get the alias, otherwise infer the column name.
28    pub(crate) fn from_target(target: ast::Target) -> Option<(ColumnName, SyntaxNode)> {
29        if let Some(as_name) = target.as_name()
30            && let Some(name_node) = as_name.name()
31        {
32            let text = name_node.text();
33            let normalized = normalize_identifier(&text);
34            return Some((ColumnName::Column(normalized), name_node.syntax().clone()));
35        }
36        Self::inferred_from_target(target)
37    }
38
39    // Ignore any aliases, just infer the what the column name.
40    pub(crate) fn inferred_from_target(target: ast::Target) -> Option<(ColumnName, SyntaxNode)> {
41        if let Some(expr) = target.expr()
42            && let Some(name) = name_from_expr(expr, false)
43        {
44            return Some(name);
45        } else if target.star_token().is_some() {
46            return Some((ColumnName::Star, target.syntax().clone()));
47        }
48        None
49    }
50
51    fn new(name: String, unknown_column: bool) -> ColumnName {
52        if unknown_column {
53            ColumnName::UnknownColumn(Some(name))
54        } else {
55            ColumnName::Column(name)
56        }
57    }
58
59    pub(crate) fn to_string(&self) -> Option<String> {
60        match self {
61            ColumnName::Column(string) => Some(string.to_string()),
62            ColumnName::Star => None,
63            ColumnName::UnknownColumn(c) => {
64                Some(c.clone().unwrap_or_else(|| "?column?".to_string()))
65            }
66        }
67    }
68}
69
70fn name_from_type(ty: ast::Type, unknown_column: bool) -> Option<(ColumnName, SyntaxNode)> {
71    match ty {
72        ast::Type::PathType(path_type) => {
73            if let Some(name_ref) = path_type
74                .path()
75                .and_then(|x| x.segment())
76                .and_then(|x| x.name_ref())
77            {
78                return name_from_name_ref(name_ref, true, path_type.arg_list().as_ref()).map(
79                    |(column, node)| {
80                        let column = match column {
81                            ColumnName::Column(c) => ColumnName::new(c, unknown_column),
82                            _ => column,
83                        };
84                        (column, node)
85                    },
86                );
87            }
88        }
89        ast::Type::BitType(bit_type) => {
90            let name = if bit_type.varying_token().is_some() {
91                "varbit"
92            } else {
93                "bit"
94            };
95            return Some((
96                ColumnName::new(name.to_string(), unknown_column),
97                bit_type.syntax().clone(),
98            ));
99        }
100        ast::Type::CharType(char_type) => {
101            let name = if char_type.varchar_token().is_some() || char_type.varying_token().is_some()
102            {
103                "varchar"
104            } else {
105                "bpchar"
106            };
107            return Some((
108                ColumnName::new(name.to_string(), unknown_column),
109                char_type.syntax().clone(),
110            ));
111        }
112        ast::Type::DoubleType(double_type) => {
113            return Some((
114                ColumnName::new("float8".to_string(), unknown_column),
115                double_type.syntax().clone(),
116            ));
117        }
118        ast::Type::IntervalType(interval_type) => {
119            return Some((
120                ColumnName::new("interval".to_string(), unknown_column),
121                interval_type.syntax().clone(),
122            ));
123        }
124        ast::Type::TimeType(time_type) => {
125            let mut name = if time_type.timestamp_token().is_some() {
126                "timestamp".to_owned()
127            } else {
128                "time".to_owned()
129            };
130            if let Some(ast::Timezone::WithTimezone(_)) = time_type.timezone() {
131                // time -> timetz
132                // timestamp -> timestamptz
133                name.push_str("tz");
134            };
135            return Some((
136                ColumnName::new(name.to_string(), unknown_column),
137                time_type.syntax().clone(),
138            ));
139        }
140        ast::Type::ArrayType(array_type) => {
141            if let Some(inner_ty) = array_type.ty() {
142                return name_from_type(inner_ty, unknown_column);
143            }
144        }
145        // we shouldn't ever hit this since the following isn't valid syntax:
146        // select cast('foo' as t.a%TYPE);
147        ast::Type::PercentType(_) => return None,
148        ast::Type::ExprType(expr_type) => {
149            if let Some(expr) = expr_type.expr() {
150                return name_from_expr(expr, true).map(|(column, node)| {
151                    let column = match column {
152                        ColumnName::Column(c) => ColumnName::new(c, unknown_column),
153                        _ => column,
154                    };
155                    (column, node)
156                });
157            }
158        }
159    }
160    None
161}
162
163fn name_from_name_ref(
164    name_ref: ast::NameRef,
165    in_type: bool,
166    arg_list: Option<&ast::ArgList>,
167) -> Option<(ColumnName, SyntaxNode)> {
168    if in_type {
169        for node in name_ref.syntax().children_with_tokens() {
170            match node.kind() {
171                SyntaxKind::BIGINT_KW => {
172                    return Some((
173                        ColumnName::Column("int8".to_owned()),
174                        name_ref.syntax().clone(),
175                    ));
176                }
177                SyntaxKind::BOOLEAN_KW => {
178                    return Some((
179                        ColumnName::Column("bool".to_owned()),
180                        name_ref.syntax().clone(),
181                    ));
182                }
183                SyntaxKind::DEC_KW | SyntaxKind::DECIMAL_KW => {
184                    return Some((
185                        ColumnName::Column("numeric".to_owned()),
186                        name_ref.syntax().clone(),
187                    ));
188                }
189                SyntaxKind::FLOAT_KW => {
190                    let precision = arg_list.and_then(|arg| {
191                        arg.args_().find_map(|arg| {
192                            if let ast::Expr::Literal(lit) = arg.expr()? {
193                                lit.syntax().text().to_string().parse::<u32>().ok()
194                            } else {
195                                None
196                            }
197                        })
198                    });
199                    let name = if matches!(precision, Some(p) if p <= 24) {
200                        "float4"
201                    } else {
202                        "float8"
203                    };
204                    return Some((
205                        ColumnName::Column(name.to_owned()),
206                        name_ref.syntax().clone(),
207                    ));
208                }
209                SyntaxKind::INT_KW | SyntaxKind::INTEGER_KW => {
210                    return Some((
211                        ColumnName::Column("int4".to_owned()),
212                        name_ref.syntax().clone(),
213                    ));
214                }
215                SyntaxKind::SMALLINT_KW => {
216                    return Some((
217                        ColumnName::Column("int2".to_owned()),
218                        name_ref.syntax().clone(),
219                    ));
220                }
221                SyntaxKind::REAL_KW => {
222                    return Some((
223                        ColumnName::Column("float4".to_owned()),
224                        name_ref.syntax().clone(),
225                    ));
226                }
227                _ => (),
228            }
229        }
230    }
231    let text = name_ref.text();
232    let normalized = normalize_identifier(&text);
233    return Some((ColumnName::Column(normalized), name_ref.syntax().clone()));
234}
235
236/*
237TODO:
238
239unnest(anyarray, anyarray [, ... ]) → setof anyelement, anyelement [, ... ]
240
241select * from unnest(ARRAY[1,2], ARRAY['foo','bar','baz']) →
242 unnset | unnset
243--------+-----
244      1 | foo
245      2 | bar
246        | baz
247*/
248
249// NOTE: we have to have this in_type param because we parse some casts as exprs
250// instead of types.
251fn name_from_expr(expr: ast::Expr, in_type: bool) -> Option<(ColumnName, SyntaxNode)> {
252    let node = expr.syntax().clone();
253    match expr {
254        ast::Expr::ArrayExpr(_) => {
255            return Some((ColumnName::Column("array".to_string()), node));
256        }
257        ast::Expr::BetweenExpr(_) => {
258            return Some((ColumnName::UnknownColumn(None), node));
259        }
260        ast::Expr::BinExpr(bin_expr) => match bin_expr.op() {
261            Some(ast::BinOp::AtTimeZone(_)) => {
262                return Some((ColumnName::Column("timezone".to_string()), node));
263            }
264            Some(ast::BinOp::Overlaps(_)) => {
265                return Some((ColumnName::Column("overlaps".to_string()), node));
266            }
267            _ => return Some((ColumnName::UnknownColumn(None), node)),
268        },
269        ast::Expr::CallExpr(call_expr) => {
270            if let Some(exists_fn) = call_expr.exists_fn() {
271                return Some((
272                    ColumnName::Column("exists".to_string()),
273                    exists_fn.syntax().clone(),
274                ));
275            }
276            if let Some(extract_fn) = call_expr.extract_fn() {
277                return Some((
278                    ColumnName::Column("extract".to_string()),
279                    extract_fn.syntax().clone(),
280                ));
281            }
282            if let Some(json_exists_fn) = call_expr.json_exists_fn() {
283                return Some((
284                    ColumnName::Column("json_exists".to_string()),
285                    json_exists_fn.syntax().clone(),
286                ));
287            }
288            if let Some(json_array_fn) = call_expr.json_array_fn() {
289                return Some((
290                    ColumnName::Column("json_array".to_string()),
291                    json_array_fn.syntax().clone(),
292                ));
293            }
294            if let Some(json_object_fn) = call_expr.json_object_fn() {
295                return Some((
296                    ColumnName::Column("json_object".to_string()),
297                    json_object_fn.syntax().clone(),
298                ));
299            }
300            if let Some(json_object_agg_fn) = call_expr.json_object_agg_fn() {
301                return Some((
302                    ColumnName::Column("json_objectagg".to_string()),
303                    json_object_agg_fn.syntax().clone(),
304                ));
305            }
306            if let Some(json_array_agg_fn) = call_expr.json_array_agg_fn() {
307                return Some((
308                    ColumnName::Column("json_arrayagg".to_string()),
309                    json_array_agg_fn.syntax().clone(),
310                ));
311            }
312            if let Some(json_query_fn) = call_expr.json_query_fn() {
313                return Some((
314                    ColumnName::Column("json_query".to_string()),
315                    json_query_fn.syntax().clone(),
316                ));
317            }
318            if let Some(json_scalar_fn) = call_expr.json_scalar_fn() {
319                return Some((
320                    ColumnName::Column("json_scalar".to_string()),
321                    json_scalar_fn.syntax().clone(),
322                ));
323            }
324            if let Some(json_serialize_fn) = call_expr.json_serialize_fn() {
325                return Some((
326                    ColumnName::Column("json_serialize".to_string()),
327                    json_serialize_fn.syntax().clone(),
328                ));
329            }
330            if let Some(json_value_fn) = call_expr.json_value_fn() {
331                return Some((
332                    ColumnName::Column("json_value".to_string()),
333                    json_value_fn.syntax().clone(),
334                ));
335            }
336            if let Some(json_fn) = call_expr.json_fn() {
337                return Some((
338                    ColumnName::Column("json".to_string()),
339                    json_fn.syntax().clone(),
340                ));
341            }
342            if let Some(substring_fn) = call_expr.substring_fn() {
343                return Some((
344                    ColumnName::Column("substring".to_string()),
345                    substring_fn.syntax().clone(),
346                ));
347            }
348            if let Some(position_fn) = call_expr.position_fn() {
349                return Some((
350                    ColumnName::Column("position".to_string()),
351                    position_fn.syntax().clone(),
352                ));
353            }
354            if let Some(overlay_fn) = call_expr.overlay_fn() {
355                return Some((
356                    ColumnName::Column("overlay".to_string()),
357                    overlay_fn.syntax().clone(),
358                ));
359            }
360            if let Some(trim_fn) = call_expr.trim_fn() {
361                let name = if trim_fn.leading_token().is_some() {
362                    "ltrim"
363                } else if trim_fn.trailing_token().is_some() {
364                    "rtrim"
365                } else {
366                    "btrim"
367                };
368                return Some((
369                    ColumnName::Column(name.to_string()),
370                    trim_fn.syntax().clone(),
371                ));
372            }
373            if let Some(xml_root_fn) = call_expr.xml_root_fn() {
374                return Some((
375                    ColumnName::Column("xml_root".to_string()),
376                    xml_root_fn.syntax().clone(),
377                ));
378            }
379            if let Some(xml_serialize_fn) = call_expr.xml_serialize_fn() {
380                return Some((
381                    ColumnName::Column("xml_serialize".to_string()),
382                    xml_serialize_fn.syntax().clone(),
383                ));
384            }
385            if let Some(xml_element_fn) = call_expr.xml_element_fn() {
386                return Some((
387                    ColumnName::Column("xml_element".to_string()),
388                    xml_element_fn.syntax().clone(),
389                ));
390            }
391            if let Some(xml_forest_fn) = call_expr.xml_forest_fn() {
392                return Some((
393                    ColumnName::Column("xml_forest".to_string()),
394                    xml_forest_fn.syntax().clone(),
395                ));
396            }
397            if let Some(xml_exists_fn) = call_expr.xml_exists_fn() {
398                return Some((
399                    ColumnName::Column("xml_exists".to_string()),
400                    xml_exists_fn.syntax().clone(),
401                ));
402            }
403            if let Some(xml_parse_fn) = call_expr.xml_parse_fn() {
404                return Some((
405                    ColumnName::Column("xml_parse".to_string()),
406                    xml_parse_fn.syntax().clone(),
407                ));
408            }
409            if let Some(xml_pi_fn) = call_expr.xml_pi_fn() {
410                return Some((
411                    ColumnName::Column("xml_pi".to_string()),
412                    xml_pi_fn.syntax().clone(),
413                ));
414            }
415            if let Some(collation_for_fn) = call_expr.collation_for_fn() {
416                return Some((
417                    ColumnName::Column("pg_collation_for".to_string()),
418                    collation_for_fn.syntax().clone(),
419                ));
420            }
421            if let Some(func_name) = call_expr.expr() {
422                match func_name {
423                    ast::Expr::ArrayExpr(_)
424                    | ast::Expr::BetweenExpr(_)
425                    | ast::Expr::ParenExpr(_)
426                    | ast::Expr::BinExpr(_)
427                    | ast::Expr::CallExpr(_)
428                    | ast::Expr::CaseExpr(_)
429                    | ast::Expr::CastExpr(_)
430                    | ast::Expr::Literal(_)
431                    | ast::Expr::PostfixExpr(_)
432                    | ast::Expr::PrefixExpr(_)
433                    | ast::Expr::TupleExpr(_)
434                    | ast::Expr::IndexExpr(_)
435                    | ast::Expr::SliceExpr(_) => unreachable!("not possible in the grammar"),
436                    ast::Expr::FieldExpr(field_expr) => {
437                        if let Some(name_ref) = field_expr.field() {
438                            return name_from_name_ref(name_ref, in_type, None);
439                        }
440                    }
441                    ast::Expr::NameRef(name_ref) => {
442                        return name_from_name_ref(name_ref, in_type, None);
443                    }
444                }
445            }
446        }
447        ast::Expr::CaseExpr(case) => {
448            if let Some(else_clause) = case.else_clause()
449                && let Some(expr) = else_clause.expr()
450                && let Some((column, node)) = name_from_expr(expr, in_type)
451            {
452                if !matches!(column, ColumnName::UnknownColumn(_)) {
453                    return Some((column, node));
454                }
455            }
456            return Some((ColumnName::Column("case".to_string()), node));
457        }
458        ast::Expr::CastExpr(cast_expr) => {
459            let mut unknown_column = false;
460            if let Some(expr) = cast_expr.expr()
461                && let Some((column, node)) = name_from_expr(expr, in_type)
462            {
463                match column {
464                    ColumnName::Column(_) => return Some((column, node)),
465                    ColumnName::UnknownColumn(_) => unknown_column = true,
466                    ColumnName::Star => (),
467                }
468            }
469            if let Some(ty) = cast_expr.ty() {
470                return name_from_type(ty, unknown_column);
471            }
472        }
473        ast::Expr::FieldExpr(field_expr) => {
474            if let Some(name_ref) = field_expr.field() {
475                return name_from_name_ref(name_ref, in_type, None);
476            }
477        }
478        ast::Expr::IndexExpr(index_expr) => {
479            if let Some(base) = index_expr.base() {
480                return name_from_expr(base, in_type);
481            }
482        }
483        ast::Expr::SliceExpr(slice_expr) => {
484            if let Some(base) = slice_expr.base() {
485                return name_from_expr(base, in_type);
486            }
487        }
488        ast::Expr::Literal(_) | ast::Expr::PrefixExpr(_) => {
489            return Some((ColumnName::UnknownColumn(None), node));
490        }
491        ast::Expr::PostfixExpr(postfix_expr) => match postfix_expr.op() {
492            Some(ast::PostfixOp::AtLocal(_)) => {
493                return Some((ColumnName::Column("timezone".to_string()), node));
494            }
495            Some(ast::PostfixOp::IsNormalized(_)) => {
496                return Some((ColumnName::Column("is_normalized".to_string()), node));
497            }
498            _ => return Some((ColumnName::UnknownColumn(None), node)),
499        },
500        ast::Expr::NameRef(name_ref) => {
501            return name_from_name_ref(name_ref, in_type, None);
502        }
503        ast::Expr::ParenExpr(paren_expr) => {
504            if let Some(expr) = paren_expr.expr() {
505                return name_from_expr(expr, in_type);
506            } else if let Some(select) = paren_expr.select()
507                && let Some(mut targets) = select
508                    .select_clause()
509                    .and_then(|x| x.target_list())
510                    .map(|x| x.targets())
511                && let Some(target) = targets.next()
512            {
513                return ColumnName::from_target(target);
514            }
515        }
516        ast::Expr::TupleExpr(_) => {
517            return Some((ColumnName::Column("row".to_string()), node));
518        }
519    }
520    None
521}
522
523#[test]
524fn examples() {
525    use insta::assert_snapshot;
526
527    // array
528    assert_snapshot!(name("array(select 1)"), @"array");
529    assert_snapshot!(name("array[1, 2, 3]"), @"array");
530
531    // unknown columns
532    assert_snapshot!(name("1 between 0 and 10"), @"?column?");
533    assert_snapshot!(name("1 + 2"), @"?column?");
534    assert_snapshot!(name("42"), @"?column?");
535    assert_snapshot!(name("'string'"), @"?column?");
536    // prefix
537    assert_snapshot!(name("-42"), @"?column?");
538    assert_snapshot!(name("|/ 42"), @"?column?");
539    // postfix
540    assert_snapshot!(name("x is null"), @"?column?");
541    assert_snapshot!(name("x is not null"), @"?column?");
542    assert_snapshot!(name("'foo' is normalized"), @"is_normalized");
543    assert_snapshot!(name("'foo' is not normalized"), @"?column?");
544    assert_snapshot!(name("now() at local"), @"timezone");
545    // bin expr
546    assert_snapshot!(name("now() at time zone 'America/Chicago'"), @"timezone");
547    assert_snapshot!(
548        name("(DATE '2001-02-16', DATE '2001-12-21') OVERLAPS (DATE '2001-10-30', DATE '2002-10-30')"),
549        @"overlaps"
550    );
551    // paren expr
552    assert_snapshot!(name("(1 * 2)"), @"?column?");
553    assert_snapshot!(name("(select 1 as a)"), @"a");
554
555    // func
556    assert_snapshot!(name("count(*)"), @"count");
557    assert_snapshot!(name("schema.func_name(1)"), @"func_name");
558
559    // special funcs
560    assert_snapshot!(name("collation for ('bar')"), @"pg_collation_for");
561    assert_snapshot!(name("extract(year from now())"), @"extract");
562    assert_snapshot!(name("exists(select 1)"), @"exists");
563    assert_snapshot!(name(r#"json_exists('{"a":1}', '$.a')"#), @"json_exists");
564    assert_snapshot!(name("json_array(1, 2)"), @"json_array");
565    assert_snapshot!(name("json_object('a': 1)"), @"json_object");
566    assert_snapshot!(name("json_objectagg('a': 1)"), @"json_objectagg");
567    assert_snapshot!(name("json_arrayagg(1)"), @"json_arrayagg");
568    assert_snapshot!(name(r#"json_query('{"a":1}', '$.a')"#), @"json_query");
569    assert_snapshot!(name("json_scalar(1)"), @"json_scalar");
570    assert_snapshot!(name(r#"json_serialize('{"a":1}')"#), @"json_serialize");
571    assert_snapshot!(name(r#"json_value('{"a":1}', '$.a')"#), @"json_value");
572    assert_snapshot!(name(r#"json('{"a":1}')"#), @"json");
573    assert_snapshot!(name("substring('hello' from 2 for 3)"), @"substring");
574    assert_snapshot!(name("position('a' in 'abc')"), @"position");
575    assert_snapshot!(name("overlay('hello' placing 'X' from 2)"), @"overlay");
576    assert_snapshot!(name("trim('  hi  ')"), @"btrim");
577    assert_snapshot!(name("trim(leading ' ' from '  hi  ')"), @"ltrim");
578    assert_snapshot!(name("trim(trailing ' ' from '  hi  ')"), @"rtrim");
579    assert_snapshot!(name("trim(both ' ' from '  hi  ')"), @"btrim");
580    assert_snapshot!(name("xmlroot('<a/>', version '1.0')"), @"xml_root");
581    assert_snapshot!(name("xmlserialize(document '<a/>' as text)"), @"xml_serialize");
582    assert_snapshot!(name("xmlelement(name foo, 'bar')"), @"xml_element");
583    assert_snapshot!(name("xmlforest('bar' as foo)"), @"xml_forest");
584    assert_snapshot!(name("xmlexists('//a' passing '<a/>')"), @"xml_exists");
585    assert_snapshot!(name("xmlparse(document '<a/>')"), @"xml_parse");
586    assert_snapshot!(name("xmlpi(name foo, 'bar')"), @"xml_pi");
587
588    // index
589    assert_snapshot!(name("foo[bar]"), @"foo");
590    assert_snapshot!(name("foo[1]"), @"foo");
591
592    // column
593    assert_snapshot!(name("database.schema.table.column"), @"column");
594    assert_snapshot!(name("t.a"), @"a");
595    assert_snapshot!(name("col_name"), @"col_name");
596    assert_snapshot!(name("(c)"), @"c");
597
598    // case
599    assert_snapshot!(name("case when true then 'foo' end"), @"case");
600    assert_snapshot!(name("case when true then 'foo' else now()::text end"), @"now");
601    assert_snapshot!(name("case when true then 'foo' else 'bar' end"), @"case");
602    assert_snapshot!(name("case when true then 'foo' else '1'::bigint::text end"), @"case");
603
604    // casts
605    assert_snapshot!(name("now()::text"), @"now");
606    assert_snapshot!(name("cast(col_name as text)"), @"col_name");
607    assert_snapshot!(name("col_name::text"), @"col_name");
608    assert_snapshot!(name("col_name::int::text"), @"col_name");
609    assert_snapshot!(name("'1'::bigint"), @"int8");
610    assert_snapshot!(name("'1'::decimal"), @"numeric");
611    assert_snapshot!(name("'1'::boolean"), @"bool");
612    assert_snapshot!(name("'1'::int"), @"int4");
613    assert_snapshot!(name("'1'::smallint"), @"int2");
614    assert_snapshot!(name("'{{1, 2}, {3, 4}}'::bigint[][]"), @"int8");
615    assert_snapshot!(name("'{{1, 2}, {3, 4}}'::int[][]"), @"int4");
616    assert_snapshot!(name("'{{1, 2}, {3, 4}}'::smallint[]"), @"int2");
617    assert_snapshot!(name("pg_catalog.varchar(100) '{1}'"), @"varchar");
618    assert_snapshot!(name("'{1}'::integer[];"), @"int4");
619    assert_snapshot!(name("'{1}'::pg_catalog.varchar(1)[]::integer[];"), @"int4");
620    assert_snapshot!(name("'1'::bigint::smallint"), @"int2");
621
622    // alias
623    // with quoting
624    assert_snapshot!(name(r#"'foo' as "FOO""#), @"FOO");
625    assert_snapshot!(name(r#"'foo' as "foo""#), @"foo");
626    // without quoting
627    assert_snapshot!(name(r#"'foo' as FOO"#), @"foo");
628    assert_snapshot!(name(r#"'foo' as foo"#), @"foo");
629
630    // tuple
631    assert_snapshot!(name("(1, 2, 3)"), @"row");
632    assert_snapshot!(name("(1, 2, 3)::address"), @"row");
633
634    // composite type
635    assert_snapshot!(name("(x).city"), @"city");
636
637    // array types
638    assert_snapshot!(name("'{{1, 2}, {3, 4}}'::int[]"), @"int4");
639    assert_snapshot!(name("cast('{foo}' as text[])"), @"text");
640
641    // bit types
642    assert_snapshot!(name("cast('1010' as bit varying(10))"), @"varbit");
643    assert_snapshot!(name("cast('1010' as bit varying)"), @"varbit");
644    assert_snapshot!(name("cast('1010' as bit)"), @"bit");
645
646    // decimal
647    assert_snapshot!(name("cast('1010' as dec)"), @"numeric");
648    assert_snapshot!(name("cast('1010' as dec(10))"), @"numeric");
649    assert_snapshot!(name("cast('1010' as decimal)"), @"numeric");
650    assert_snapshot!(name("cast('1010' as decimal(10))"), @"numeric");
651
652    // char types
653    assert_snapshot!(name("cast('hello' as character varying(10))"), @"varchar");
654    assert_snapshot!(name("cast('hello' as char varying(5))"), @"varchar");
655    assert_snapshot!(name("cast('hello' as nchar varying(10))"), @"varchar");
656    assert_snapshot!(name("cast('hello' as char(5))"), @"bpchar");
657    assert_snapshot!(name("cast('hello' as character)"), @"bpchar");
658    assert_snapshot!(name("cast('hello' as bpchar)"), @"bpchar");
659    assert_snapshot!(name("cast('hello' as nchar(10))"), @"bpchar");
660
661    assert_snapshot!(name(r#"cast('hello' as "char")"#), @"char");
662
663    // double types
664    assert_snapshot!(name("cast(1.5 as double precision)"), @"float8");
665    // real
666    assert_snapshot!(name("cast(1.5 as real)"), @"float4");
667    assert_snapshot!(name("cast(1.5 as float(8))"), @"float4");
668    assert_snapshot!(name("cast(2.5 as float(25))"), @"float8");
669
670    // interval types
671    assert_snapshot!(name("cast('1 hour' as interval hour to minute)"), @"interval");
672
673    // percent types
674    assert_snapshot!(name("cast(foo as schema.%TYPE)"), @"foo");
675
676    // time types
677    assert_snapshot!(name("cast('12:00:00' as time(6) without time zone)"), @"time");
678    assert_snapshot!(name("cast('12:00:00' as time(6) with time zone)"), @"timetz");
679    assert_snapshot!(name("cast('2024-01-01 12:00:00' as timestamp(6) with time zone)"), @"timestamptz");
680    assert_snapshot!(name("cast('2024-01-01 12:00:00' as timestamp(6) without time zone)"), @"timestamp");
681
682    #[track_caller]
683    fn name(sql: &str) -> String {
684        let sql = "select ".to_string() + sql;
685        let parse = squawk_syntax::SourceFile::parse(&sql);
686        assert_eq!(parse.errors(), vec![]);
687        let file = parse.tree();
688
689        let stmt = file.stmts().next().unwrap();
690        let ast::Stmt::Select(select) = stmt else {
691            unreachable!()
692        };
693
694        let target = select
695            .select_clause()
696            .and_then(|sc| sc.target_list())
697            .and_then(|tl| tl.targets().next())
698            .unwrap();
699
700        ColumnName::from_target(target)
701            .and_then(|x| x.0.to_string())
702            .unwrap()
703    }
704}