squawk_ide/
document_symbols.rs

1use rowan::TextRange;
2use squawk_syntax::ast::{self, AstNode};
3
4use crate::binder::{self, extract_string_literal};
5use crate::resolve::{
6    resolve_aggregate_info, resolve_function_info, resolve_materialized_view_info,
7    resolve_procedure_info, resolve_table_info, resolve_type_info, resolve_view_info,
8};
9
10#[derive(Debug)]
11pub enum DocumentSymbolKind {
12    Schema,
13    Table,
14    View,
15    MaterializedView,
16    Function,
17    Aggregate,
18    Procedure,
19    Type,
20    Enum,
21    Column,
22    Variant,
23}
24
25#[derive(Debug)]
26pub struct DocumentSymbol {
27    pub name: String,
28    pub detail: Option<String>,
29    pub kind: DocumentSymbolKind,
30    /// Range used for determining when cursor is inside the symbol for showing
31    /// in the UI
32    pub full_range: TextRange,
33    /// Range selected when symbol is selected
34    pub focus_range: TextRange,
35    pub children: Vec<DocumentSymbol>,
36}
37
38pub fn document_symbols(file: &ast::SourceFile) -> Vec<DocumentSymbol> {
39    let binder = binder::bind(file);
40    let mut symbols = vec![];
41
42    for stmt in file.stmts() {
43        match stmt {
44            ast::Stmt::CreateSchema(create_schema) => {
45                if let Some(symbol) = create_schema_symbol(create_schema) {
46                    symbols.push(symbol);
47                }
48            }
49            ast::Stmt::CreateTable(create_table) => {
50                if let Some(symbol) = create_table_symbol(&binder, create_table) {
51                    symbols.push(symbol);
52                }
53            }
54            ast::Stmt::CreateFunction(create_function) => {
55                if let Some(symbol) = create_function_symbol(&binder, create_function) {
56                    symbols.push(symbol);
57                }
58            }
59            ast::Stmt::CreateAggregate(create_aggregate) => {
60                if let Some(symbol) = create_aggregate_symbol(&binder, create_aggregate) {
61                    symbols.push(symbol);
62                }
63            }
64            ast::Stmt::CreateProcedure(create_procedure) => {
65                if let Some(symbol) = create_procedure_symbol(&binder, create_procedure) {
66                    symbols.push(symbol);
67                }
68            }
69            ast::Stmt::CreateType(create_type) => {
70                if let Some(symbol) = create_type_symbol(&binder, create_type) {
71                    symbols.push(symbol);
72                }
73            }
74            ast::Stmt::CreateView(create_view) => {
75                if let Some(symbol) = create_view_symbol(&binder, create_view) {
76                    symbols.push(symbol);
77                }
78            }
79            ast::Stmt::CreateMaterializedView(create_view) => {
80                if let Some(symbol) = create_materialized_view_symbol(&binder, create_view) {
81                    symbols.push(symbol);
82                }
83            }
84            ast::Stmt::Select(select) => {
85                symbols.extend(cte_table_symbols(select));
86            }
87            ast::Stmt::SelectInto(select_into) => {
88                symbols.extend(cte_table_symbols(select_into));
89            }
90            ast::Stmt::Insert(insert) => {
91                symbols.extend(cte_table_symbols(insert));
92            }
93            ast::Stmt::Update(update) => {
94                symbols.extend(cte_table_symbols(update));
95            }
96            ast::Stmt::Delete(delete) => {
97                symbols.extend(cte_table_symbols(delete));
98            }
99
100            _ => {}
101        }
102    }
103
104    symbols
105}
106
107fn cte_table_symbols(stmt: impl ast::HasWithClause) -> Vec<DocumentSymbol> {
108    let Some(with_clause) = stmt.with_clause() else {
109        return vec![];
110    };
111
112    with_clause
113        .with_tables()
114        .filter_map(create_cte_table_symbol)
115        .collect()
116}
117
118fn create_cte_table_symbol(with_table: ast::WithTable) -> Option<DocumentSymbol> {
119    let name_node = with_table.name()?;
120    let name = name_node.syntax().text().to_string();
121
122    let full_range = with_table.syntax().text_range();
123    let focus_range = name_node.syntax().text_range();
124
125    let mut children = vec![];
126    if let Some(column_list) = with_table.column_list() {
127        for column in column_list.columns() {
128            if let Some(column_symbol) = create_column_symbol(column) {
129                children.push(column_symbol);
130            }
131        }
132    }
133
134    Some(DocumentSymbol {
135        name,
136        detail: None,
137        kind: DocumentSymbolKind::Table,
138        full_range,
139        focus_range,
140        children,
141    })
142}
143
144fn create_schema_symbol(create_schema: ast::CreateSchema) -> Option<DocumentSymbol> {
145    let (name, focus_range) = if let Some(name_node) = create_schema.name() {
146        (
147            name_node.syntax().text().to_string(),
148            name_node.syntax().text_range(),
149        )
150    } else if let Some(schema_name_ref) = create_schema
151        .schema_authorization()
152        .and_then(|authorization| authorization.role())
153        .and_then(|role| role.name_ref())
154    {
155        (
156            schema_name_ref.syntax().text().to_string(),
157            schema_name_ref.syntax().text_range(),
158        )
159    } else {
160        return None;
161    };
162
163    let full_range = create_schema.syntax().text_range();
164
165    Some(DocumentSymbol {
166        name,
167        detail: None,
168        kind: DocumentSymbolKind::Schema,
169        full_range,
170        focus_range,
171        children: vec![],
172    })
173}
174
175fn create_table_symbol(
176    binder: &binder::Binder,
177    create_table: ast::CreateTable,
178) -> Option<DocumentSymbol> {
179    let path = create_table.path()?;
180    let segment = path.segment()?;
181    let name_node = segment.name()?;
182
183    let (schema, table_name) = resolve_table_info(binder, &path)?;
184    let name = format!("{}.{}", schema.0, table_name);
185
186    let full_range = create_table.syntax().text_range();
187    let focus_range = name_node.syntax().text_range();
188
189    let mut children = vec![];
190    if let Some(table_arg_list) = create_table.table_arg_list() {
191        for arg in table_arg_list.args() {
192            if let ast::TableArg::Column(column) = arg
193                && let Some(column_symbol) = create_column_symbol(column)
194            {
195                children.push(column_symbol);
196            }
197        }
198    }
199
200    Some(DocumentSymbol {
201        name,
202        detail: None,
203        kind: DocumentSymbolKind::Table,
204        full_range,
205        focus_range,
206        children,
207    })
208}
209
210fn create_view_symbol(
211    binder: &binder::Binder,
212    create_view: ast::CreateView,
213) -> Option<DocumentSymbol> {
214    let path = create_view.path()?;
215    let segment = path.segment()?;
216    let name_node = segment.name()?;
217
218    let (schema, view_name) = resolve_view_info(binder, &path)?;
219    let name = format!("{}.{}", schema.0, view_name);
220
221    let full_range = create_view.syntax().text_range();
222    let focus_range = name_node.syntax().text_range();
223
224    let mut children = vec![];
225    if let Some(column_list) = create_view.column_list() {
226        for column in column_list.columns() {
227            if let Some(column_symbol) = create_column_symbol(column) {
228                children.push(column_symbol);
229            }
230        }
231    }
232
233    Some(DocumentSymbol {
234        name,
235        detail: None,
236        kind: DocumentSymbolKind::View,
237        full_range,
238        focus_range,
239        children,
240    })
241}
242
243fn create_materialized_view_symbol(
244    binder: &binder::Binder,
245    create_view: ast::CreateMaterializedView,
246) -> Option<DocumentSymbol> {
247    let path = create_view.path()?;
248    let segment = path.segment()?;
249    let name_node = segment.name()?;
250
251    let (schema, view_name) = resolve_materialized_view_info(binder, &path)?;
252    let name = format!("{}.{}", schema.0, view_name);
253
254    let full_range = create_view.syntax().text_range();
255    let focus_range = name_node.syntax().text_range();
256
257    let mut children = vec![];
258    if let Some(column_list) = create_view.column_list() {
259        for column in column_list.columns() {
260            if let Some(column_symbol) = create_column_symbol(column) {
261                children.push(column_symbol);
262            }
263        }
264    }
265
266    Some(DocumentSymbol {
267        name,
268        detail: None,
269        kind: DocumentSymbolKind::MaterializedView,
270        full_range,
271        focus_range,
272        children,
273    })
274}
275
276fn create_function_symbol(
277    binder: &binder::Binder,
278    create_function: ast::CreateFunction,
279) -> Option<DocumentSymbol> {
280    let path = create_function.path()?;
281    let segment = path.segment()?;
282    let name_node = segment.name()?;
283
284    let (schema, function_name) = resolve_function_info(binder, &path)?;
285    let name = format!("{}.{}", schema.0, function_name);
286
287    let full_range = create_function.syntax().text_range();
288    let focus_range = name_node.syntax().text_range();
289
290    Some(DocumentSymbol {
291        name,
292        detail: None,
293        kind: DocumentSymbolKind::Function,
294        full_range,
295        focus_range,
296        children: vec![],
297    })
298}
299
300fn create_aggregate_symbol(
301    binder: &binder::Binder,
302    create_aggregate: ast::CreateAggregate,
303) -> Option<DocumentSymbol> {
304    let path = create_aggregate.path()?;
305    let segment = path.segment()?;
306    let name_node = segment.name()?;
307
308    let (schema, aggregate_name) = resolve_aggregate_info(binder, &path)?;
309    let name = format!("{}.{}", schema.0, aggregate_name);
310
311    let full_range = create_aggregate.syntax().text_range();
312    let focus_range = name_node.syntax().text_range();
313
314    Some(DocumentSymbol {
315        name,
316        detail: None,
317        kind: DocumentSymbolKind::Aggregate,
318        full_range,
319        focus_range,
320        children: vec![],
321    })
322}
323
324fn create_procedure_symbol(
325    binder: &binder::Binder,
326    create_procedure: ast::CreateProcedure,
327) -> Option<DocumentSymbol> {
328    let path = create_procedure.path()?;
329    let segment = path.segment()?;
330    let name_node = segment.name()?;
331
332    let (schema, procedure_name) = resolve_procedure_info(binder, &path)?;
333    let name = format!("{}.{}", schema.0, procedure_name);
334
335    let full_range = create_procedure.syntax().text_range();
336    let focus_range = name_node.syntax().text_range();
337
338    Some(DocumentSymbol {
339        name,
340        detail: None,
341        kind: DocumentSymbolKind::Procedure,
342        full_range,
343        focus_range,
344        children: vec![],
345    })
346}
347
348fn create_type_symbol(
349    binder: &binder::Binder,
350    create_type: ast::CreateType,
351) -> Option<DocumentSymbol> {
352    let path = create_type.path()?;
353    let segment = path.segment()?;
354    let name_node = segment.name()?;
355
356    let (schema, type_name) = resolve_type_info(binder, &path)?;
357    let name = format!("{}.{}", schema.0, type_name);
358
359    let full_range = create_type.syntax().text_range();
360    let focus_range = name_node.syntax().text_range();
361
362    let mut children = vec![];
363    if let Some(variant_list) = create_type.variant_list() {
364        for variant in variant_list.variants() {
365            if let Some(variant_symbol) = create_variant_symbol(variant) {
366                children.push(variant_symbol);
367            }
368        }
369    } else if let Some(column_list) = create_type.column_list() {
370        for column in column_list.columns() {
371            if let Some(column_symbol) = create_column_symbol(column) {
372                children.push(column_symbol);
373            }
374        }
375    }
376
377    Some(DocumentSymbol {
378        name,
379        detail: None,
380        kind: if create_type.variant_list().is_some() {
381            DocumentSymbolKind::Enum
382        } else {
383            DocumentSymbolKind::Type
384        },
385        full_range,
386        focus_range,
387        children,
388    })
389}
390
391fn create_column_symbol(column: ast::Column) -> Option<DocumentSymbol> {
392    let name_node = column.name()?;
393    let name = name_node.syntax().text().to_string();
394
395    let detail = column.ty().map(|t| t.syntax().text().to_string());
396
397    let full_range = column.syntax().text_range();
398    let focus_range = name_node.syntax().text_range();
399
400    Some(DocumentSymbol {
401        name,
402        detail,
403        kind: DocumentSymbolKind::Column,
404        full_range,
405        focus_range,
406        children: vec![],
407    })
408}
409
410fn create_variant_symbol(variant: ast::Variant) -> Option<DocumentSymbol> {
411    let literal = variant.literal()?;
412    let name = extract_string_literal(&literal)?;
413
414    let full_range = variant.syntax().text_range();
415    let focus_range = literal.syntax().text_range();
416
417    Some(DocumentSymbol {
418        name,
419        detail: None,
420        kind: DocumentSymbolKind::Variant,
421        full_range,
422        focus_range,
423        children: vec![],
424    })
425}
426
427#[cfg(test)]
428mod tests {
429    use super::*;
430    use annotate_snippets::{
431        AnnotationKind, Group, Level, Renderer, Snippet, renderer::DecorStyle,
432    };
433    use insta::assert_snapshot;
434
435    fn symbols_not_found(sql: &str) {
436        let parse = ast::SourceFile::parse(sql);
437        let file = parse.tree();
438        let symbols = document_symbols(&file);
439        if !symbols.is_empty() {
440            panic!("Symbols found. If this is expected, use `symbols` instead.")
441        }
442    }
443
444    fn symbols(sql: &str) -> String {
445        let parse = ast::SourceFile::parse(sql);
446        let file = parse.tree();
447        let symbols = document_symbols(&file);
448        if symbols.is_empty() {
449            panic!("No symbols found. If this is expected, use `symbols_not_found` instead.")
450        }
451
452        let mut output = vec![];
453        for symbol in symbols {
454            let group = symbol_to_group(&symbol, sql);
455            output.push(group);
456        }
457        Renderer::plain()
458            .decor_style(DecorStyle::Unicode)
459            .render(&output)
460            .to_string()
461    }
462
463    fn symbol_to_group<'a>(symbol: &DocumentSymbol, sql: &'a str) -> Group<'a> {
464        let kind = match symbol.kind {
465            DocumentSymbolKind::Schema => "schema",
466            DocumentSymbolKind::Table => "table",
467            DocumentSymbolKind::View => "view",
468            DocumentSymbolKind::MaterializedView => "materialized view",
469            DocumentSymbolKind::Function => "function",
470            DocumentSymbolKind::Aggregate => "aggregate",
471            DocumentSymbolKind::Procedure => "procedure",
472            DocumentSymbolKind::Type => "type",
473            DocumentSymbolKind::Enum => "enum",
474            DocumentSymbolKind::Column => "column",
475            DocumentSymbolKind::Variant => "variant",
476        };
477
478        let title = if let Some(detail) = &symbol.detail {
479            format!("{}: {} {}", kind, symbol.name, detail)
480        } else {
481            format!("{}: {}", kind, symbol.name)
482        };
483
484        let snippet = Snippet::source(sql)
485            .fold(true)
486            .annotation(
487                AnnotationKind::Primary
488                    .span(symbol.focus_range.into())
489                    .label("focus range"),
490            )
491            .annotation(
492                AnnotationKind::Context
493                    .span(symbol.full_range.into())
494                    .label("full range"),
495            );
496
497        let mut group = Level::INFO.primary_title(title.clone()).element(snippet);
498
499        if !symbol.children.is_empty() {
500            let child_labels: Vec<String> = symbol
501                .children
502                .iter()
503                .map(|child| {
504                    let kind = match child.kind {
505                        DocumentSymbolKind::Column => "column",
506                        DocumentSymbolKind::Variant => "variant",
507                        _ => unreachable!("only columns and variants can be children"),
508                    };
509                    if let Some(detail) = &child.detail {
510                        format!("{}: {} {}", kind, child.name, detail)
511                    } else {
512                        format!("{}: {}", kind, child.name)
513                    }
514                })
515                .collect();
516
517            let mut children_snippet = Snippet::source(sql).fold(true);
518
519            for (i, child) in symbol.children.iter().enumerate() {
520                children_snippet = children_snippet
521                    .annotation(
522                        AnnotationKind::Context
523                            .span(child.full_range.into())
524                            .label(format!("full range for `{}`", child_labels[i].clone())),
525                    )
526                    .annotation(
527                        AnnotationKind::Primary
528                            .span(child.focus_range.into())
529                            .label("focus range"),
530                    );
531            }
532
533            group = group.element(children_snippet);
534        }
535
536        group
537    }
538
539    #[test]
540    fn create_table() {
541        assert_snapshot!(symbols("
542create table users (
543  id int,
544  email citext
545);"), @r"
546        info: table: public.users
547          ╭▸ 
548        2 │   create table users (
549          │   │            ━━━━━ focus range
550          │ ┌─┘
551          │ │
552        3 │ │   id int,
553        4 │ │   email citext
554        5 │ │ );
555          │ └─┘ full range
556557558        3 │     id int,
559          │     ┯━────
560          │     │
561          │     full range for `column: id int`
562          │     focus range
563        4 │     email citext
564          │     ┯━━━━───────
565          │     │
566          │     full range for `column: email citext`
567          ╰╴    focus range
568        ");
569    }
570
571    #[test]
572    fn create_schema() {
573        assert_snapshot!(symbols("
574create schema foo;
575"), @r"
576        info: schema: foo
577          ╭▸ 
578        2 │ create schema foo;
579          │ ┬─────────────┯━━
580          │ │             │
581          │ │             focus range
582          ╰╴full range
583        ");
584    }
585
586    #[test]
587    fn create_schema_authorization() {
588        assert_snapshot!(symbols("
589create schema authorization foo;
590"), @r"
591        info: schema: foo
592          ╭▸ 
593        2 │ create schema authorization foo;
594          │ ┬───────────────────────────┯━━
595          │ │                           │
596          │ │                           focus range
597          ╰╴full range
598        ");
599    }
600
601    #[test]
602    fn create_function() {
603        assert_snapshot!(
604            symbols("create function hello() returns void as $$ select 1; $$ language sql;"),
605            @r"
606        info: function: public.hello
607          ╭▸ 
608        1 │ create function hello() returns void as $$ select 1; $$ language sql;
609          │ ┬───────────────┯━━━━───────────────────────────────────────────────
610          │ │               │
611          │ │               focus range
612          ╰╴full range
613        "
614        );
615    }
616
617    #[test]
618    fn create_materialized_view() {
619        assert_snapshot!(
620            symbols("create materialized view reports as select 1;"),
621            @r"
622        info: materialized view: public.reports
623          ╭▸ 
624        1 │ create materialized view reports as select 1;
625          │ ┬────────────────────────┯━━━━━━────────────
626          │ │                        │
627          │ │                        focus range
628          ╰╴full range
629        "
630        );
631    }
632
633    #[test]
634    fn create_aggregate() {
635        assert_snapshot!(
636            symbols("create aggregate myavg(int) (sfunc = int4_avg_accum, stype = _int8);"),
637            @r"
638        info: aggregate: public.myavg
639          ╭▸ 
640        1 │ create aggregate myavg(int) (sfunc = int4_avg_accum, stype = _int8);
641          │ ┬────────────────┯━━━━─────────────────────────────────────────────
642          │ │                │
643          │ │                focus range
644          ╰╴full range
645        "
646        );
647    }
648
649    #[test]
650    fn create_procedure() {
651        assert_snapshot!(
652            symbols("create procedure hello() language sql as $$ select 1; $$;"),
653            @r"
654        info: procedure: public.hello
655          ╭▸ 
656        1 │ create procedure hello() language sql as $$ select 1; $$;
657          │ ┬────────────────┯━━━━──────────────────────────────────
658          │ │                │
659          │ │                focus range
660          ╰╴full range
661        "
662        );
663    }
664
665    #[test]
666    fn multiple_symbols() {
667        assert_snapshot!(symbols("
668create table users (id int);
669create table posts (id int);
670create function get_user(user_id int) returns void as $$ select 1; $$ language sql;
671"), @r"
672        info: table: public.users
673          ╭▸ 
674        2 │ create table users (id int);
675          │ ┬────────────┯━━━━─────────
676          │ │            │
677          │ │            focus range
678          │ full range
679680681        2 │ create table users (id int);
682          │                     ┯━────
683          │                     │
684          │                     full range for `column: id int`
685          │                     focus range
686          ╰╴
687        info: table: public.posts
688          ╭▸ 
689        3 │ create table posts (id int);
690          │ ┬────────────┯━━━━─────────
691          │ │            │
692          │ │            focus range
693          │ full range
694695696        3 │ create table posts (id int);
697          │                     ┯━────
698          │                     │
699          │                     full range for `column: id int`
700          ╰╴                    focus range
701        info: function: public.get_user
702          ╭▸ 
703        4 │ create function get_user(user_id int) returns void as $$ select 1; $$ language sql;
704          │ ┬───────────────┯━━━━━━━──────────────────────────────────────────────────────────
705          │ │               │
706          │ │               focus range
707          ╰╴full range
708        ");
709    }
710
711    #[test]
712    fn qualified_names() {
713        assert_snapshot!(symbols("
714create table public.users (id int);
715create function my_schema.hello() returns void as $$ select 1; $$ language sql;
716"), @r"
717        info: table: public.users
718          ╭▸ 
719        2 │ create table public.users (id int);
720          │ ┬───────────────────┯━━━━─────────
721          │ │                   │
722          │ │                   focus range
723          │ full range
724725726        2 │ create table public.users (id int);
727          │                            ┯━────
728          │                            │
729          │                            full range for `column: id int`
730          │                            focus range
731          ╰╴
732        info: function: my_schema.hello
733          ╭▸ 
734        3 │ create function my_schema.hello() returns void as $$ select 1; $$ language sql;
735          │ ┬─────────────────────────┯━━━━───────────────────────────────────────────────
736          │ │                         │
737          │ │                         focus range
738          ╰╴full range
739        ");
740    }
741
742    #[test]
743    fn create_type() {
744        assert_snapshot!(
745            symbols("create type status as enum ('active', 'inactive');"),
746            @r"
747        info: enum: public.status
748          ╭▸ 
749        1 │ create type status as enum ('active', 'inactive');
750          │ ┬───────────┯━━━━━───────────────────────────────
751          │ │           │
752          │ │           focus range
753          │ full range
754755756        1 │ create type status as enum ('active', 'inactive');
757          │                             ┯━━━━━━━  ┯━━━━━━━━━
758          │                             │         │
759          │                             │         full range for `variant: inactive`
760          │                             │         focus range
761          │                             full range for `variant: active`
762          ╰╴                            focus range
763        "
764        );
765    }
766
767    #[test]
768    fn create_type_composite() {
769        assert_snapshot!(
770            symbols("create type person as (name text, age int);"),
771            @r"
772        info: type: public.person
773          ╭▸ 
774        1 │ create type person as (name text, age int);
775          │ ┬───────────┯━━━━━────────────────────────
776          │ │           │
777          │ │           focus range
778          │ full range
779780781        1 │ create type person as (name text, age int);
782          │                        ┯━━━─────  ┯━━────
783          │                        │          │
784          │                        │          full range for `column: age int`
785          │                        │          focus range
786          │                        full range for `column: name text`
787          ╰╴                       focus range
788        "
789        );
790    }
791
792    #[test]
793    fn create_type_composite_multiple_columns() {
794        assert_snapshot!(
795            symbols("create type address as (street text, city text, zip varchar(10));"),
796            @r"
797        info: type: public.address
798          ╭▸ 
799        1 │ create type address as (street text, city text, zip varchar(10));
800          │ ┬───────────┯━━━━━━─────────────────────────────────────────────
801          │ │           │
802          │ │           focus range
803          │ full range
804805806        1 │ create type address as (street text, city text, zip varchar(10));
807          │                         ┯━━━━━─────  ┯━━━─────  ┯━━────────────
808          │                         │            │          │
809          │                         │            │          full range for `column: zip varchar(10)`
810          │                         │            │          focus range
811          │                         │            full range for `column: city text`
812          │                         │            focus range
813          │                         full range for `column: street text`
814          ╰╴                        focus range
815        "
816        );
817    }
818
819    #[test]
820    fn create_type_with_schema() {
821        assert_snapshot!(
822            symbols("create type myschema.status as enum ('active', 'inactive');"),
823            @r"
824        info: enum: myschema.status
825          ╭▸ 
826        1 │ create type myschema.status as enum ('active', 'inactive');
827          │ ┬────────────────────┯━━━━━───────────────────────────────
828          │ │                    │
829          │ │                    focus range
830          │ full range
831832833        1 │ create type myschema.status as enum ('active', 'inactive');
834          │                                      ┯━━━━━━━  ┯━━━━━━━━━
835          │                                      │         │
836          │                                      │         full range for `variant: inactive`
837          │                                      │         focus range
838          │                                      full range for `variant: active`
839          ╰╴                                     focus range
840        "
841        );
842    }
843
844    #[test]
845    fn create_type_enum_multiple_variants() {
846        assert_snapshot!(
847            symbols("create type priority as enum ('low', 'medium', 'high', 'urgent');"),
848            @r"
849        info: enum: public.priority
850          ╭▸ 
851        1 │ create type priority as enum ('low', 'medium', 'high', 'urgent');
852          │ ┬───────────┯━━━━━━━────────────────────────────────────────────
853          │ │           │
854          │ │           focus range
855          │ full range
856857858        1 │ create type priority as enum ('low', 'medium', 'high', 'urgent');
859          │                               ┯━━━━  ┯━━━━━━━  ┯━━━━━  ┯━━━━━━━
860          │                               │      │         │       │
861          │                               │      │         │       full range for `variant: urgent`
862          │                               │      │         │       focus range
863          │                               │      │         full range for `variant: high`
864          │                               │      │         focus range
865          │                               │      full range for `variant: medium`
866          │                               │      focus range
867          │                               full range for `variant: low`
868          ╰╴                              focus range
869        "
870        );
871    }
872
873    #[test]
874    fn empty_file() {
875        symbols_not_found("")
876    }
877
878    #[test]
879    fn non_create_statements() {
880        symbols_not_found("select * from users;")
881    }
882
883    #[test]
884    fn cte_table() {
885        assert_snapshot!(
886            symbols("
887with recent_users as (
888  select id, email as user_email
889  from users
890)
891select * from recent_users;
892"),
893            @r"
894        info: table: recent_users
895          ╭▸ 
896        2 │   with recent_users as (
897          │        │━━━━━━━━━━━
898          │        │
899          │ ┌──────focus range
900          │ │
901        3 │ │   select id, email as user_email
902        4 │ │   from users
903        5 │ │ )
904          ╰╴└─┘ full range
905        "
906        );
907    }
908
909    #[test]
910    fn cte_table_with_column_list() {
911        assert_snapshot!(
912            symbols("
913with t(a, b, c) as (
914  select 1, 2, 3
915)
916select * from t;
917"),
918            @r"
919        info: table: t
920          ╭▸ 
921        2 │   with t(a, b, c) as (
922          │        ━ focus range
923          │ ┌──────┘
924          │ │
925        3 │ │   select 1, 2, 3
926        4 │ │ )
927          │ └─┘ full range
928929930        2 │   with t(a, b, c) as (
931          │          ┯  ┯  ┯
932          │          │  │  │
933          │          │  │  full range for `column: c`
934          │          │  │  focus range
935          │          │  full range for `column: b`
936          │          │  focus range
937          │          full range for `column: a`
938          ╰╴         focus range
939        "
940        );
941    }
942}