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_procedure_info, resolve_table_info,
7    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    EventTrigger,
20    Role,
21    Type,
22    Enum,
23    Column,
24    Variant,
25    Cursor,
26    PreparedStatement,
27    Channel,
28}
29
30#[derive(Debug)]
31pub struct DocumentSymbol {
32    pub name: String,
33    pub detail: Option<String>,
34    pub kind: DocumentSymbolKind,
35    /// Range used for determining when cursor is inside the symbol for showing
36    /// in the UI
37    pub full_range: TextRange,
38    /// Range selected when symbol is selected
39    pub focus_range: TextRange,
40    pub children: Vec<DocumentSymbol>,
41}
42
43pub fn document_symbols(file: &ast::SourceFile) -> Vec<DocumentSymbol> {
44    let binder = binder::bind(file);
45    let mut symbols = vec![];
46
47    for stmt in file.stmts() {
48        match stmt {
49            ast::Stmt::CreateSchema(create_schema) => {
50                if let Some(symbol) = create_schema_symbol(create_schema) {
51                    symbols.push(symbol);
52                }
53            }
54            ast::Stmt::CreateTable(create_table) => {
55                if let Some(symbol) = create_table_symbol(&binder, create_table) {
56                    symbols.push(symbol);
57                }
58            }
59            ast::Stmt::CreateFunction(create_function) => {
60                if let Some(symbol) = create_function_symbol(&binder, create_function) {
61                    symbols.push(symbol);
62                }
63            }
64            ast::Stmt::CreateAggregate(create_aggregate) => {
65                if let Some(symbol) = create_aggregate_symbol(&binder, create_aggregate) {
66                    symbols.push(symbol);
67                }
68            }
69            ast::Stmt::CreateProcedure(create_procedure) => {
70                if let Some(symbol) = create_procedure_symbol(&binder, create_procedure) {
71                    symbols.push(symbol);
72                }
73            }
74            ast::Stmt::CreateEventTrigger(create_event_trigger) => {
75                if let Some(symbol) = create_event_trigger_symbol(create_event_trigger) {
76                    symbols.push(symbol);
77                }
78            }
79            ast::Stmt::CreateRole(create_role) => {
80                if let Some(symbol) = create_role_symbol(create_role) {
81                    symbols.push(symbol);
82                }
83            }
84            ast::Stmt::CreateType(create_type) => {
85                if let Some(symbol) = create_type_symbol(&binder, create_type) {
86                    symbols.push(symbol);
87                }
88            }
89            ast::Stmt::CreateView(create_view) => {
90                if let Some(symbol) = create_view_symbol(&binder, create_view) {
91                    symbols.push(symbol);
92                }
93            }
94            ast::Stmt::CreateMaterializedView(create_view) => {
95                if let Some(symbol) = create_materialized_view_symbol(&binder, create_view) {
96                    symbols.push(symbol);
97                }
98            }
99            ast::Stmt::Declare(declare) => {
100                if let Some(symbol) = create_declare_cursor_symbol(declare) {
101                    symbols.push(symbol);
102                }
103            }
104            ast::Stmt::Prepare(prepare) => {
105                if let Some(symbol) = create_prepare_symbol(prepare) {
106                    symbols.push(symbol);
107                }
108            }
109            ast::Stmt::Select(select) => {
110                symbols.extend(cte_table_symbols(select));
111            }
112            ast::Stmt::SelectInto(select_into) => {
113                symbols.extend(cte_table_symbols(select_into));
114            }
115            ast::Stmt::Insert(insert) => {
116                symbols.extend(cte_table_symbols(insert));
117            }
118            ast::Stmt::Update(update) => {
119                symbols.extend(cte_table_symbols(update));
120            }
121            ast::Stmt::Delete(delete) => {
122                symbols.extend(cte_table_symbols(delete));
123            }
124            ast::Stmt::Listen(listen) => {
125                if let Some(symbol) = create_listen_symbol(listen) {
126                    symbols.push(symbol);
127                }
128            }
129            ast::Stmt::Notify(notify) => {
130                if let Some(symbol) = create_notify_symbol(notify) {
131                    symbols.push(symbol);
132                }
133            }
134            ast::Stmt::Unlisten(unlisten) => {
135                if let Some(symbol) = create_unlisten_symbol(unlisten) {
136                    symbols.push(symbol);
137                }
138            }
139
140            _ => {}
141        }
142    }
143
144    symbols
145}
146
147fn cte_table_symbols(stmt: impl ast::HasWithClause) -> Vec<DocumentSymbol> {
148    let Some(with_clause) = stmt.with_clause() else {
149        return vec![];
150    };
151
152    with_clause
153        .with_tables()
154        .filter_map(create_cte_table_symbol)
155        .collect()
156}
157
158fn create_cte_table_symbol(with_table: ast::WithTable) -> Option<DocumentSymbol> {
159    let name_node = with_table.name()?;
160    let name = name_node.syntax().text().to_string();
161
162    let full_range = with_table.syntax().text_range();
163    let focus_range = name_node.syntax().text_range();
164
165    symbols_from_column_list(
166        with_table.column_list(),
167        name,
168        full_range,
169        focus_range,
170        DocumentSymbolKind::Table,
171    )
172}
173
174fn create_schema_symbol(create_schema: ast::CreateSchema) -> Option<DocumentSymbol> {
175    let (name, focus_range) = if let Some(name_node) = create_schema.name() {
176        (
177            name_node.syntax().text().to_string(),
178            name_node.syntax().text_range(),
179        )
180    } else if let Some(name) = create_schema.role().and_then(|r| r.name()) {
181        (name.syntax().text().to_string(), name.syntax().text_range())
182    } else {
183        return None;
184    };
185
186    let full_range = create_schema.syntax().text_range();
187
188    Some(DocumentSymbol {
189        name,
190        detail: None,
191        kind: DocumentSymbolKind::Schema,
192        full_range,
193        focus_range,
194        children: vec![],
195    })
196}
197
198fn create_table_symbol(
199    binder: &binder::Binder,
200    create_table: ast::CreateTable,
201) -> Option<DocumentSymbol> {
202    let path = create_table.path()?;
203    let segment = path.segment()?;
204    let name_node = segment.name()?;
205
206    let (schema, table_name) = resolve_table_info(binder, &path)?;
207    let name = format!("{}.{}", schema.0, table_name);
208
209    let full_range = create_table.syntax().text_range();
210    let focus_range = name_node.syntax().text_range();
211
212    let mut children = vec![];
213    if let Some(table_arg_list) = create_table.table_arg_list() {
214        for arg in table_arg_list.args() {
215            if let ast::TableArg::Column(column) = arg
216                && let Some(column_symbol) = create_column_symbol(column)
217            {
218                children.push(column_symbol);
219            }
220        }
221    }
222
223    Some(DocumentSymbol {
224        name,
225        detail: None,
226        kind: DocumentSymbolKind::Table,
227        full_range,
228        focus_range,
229        children,
230    })
231}
232
233fn create_view_symbol(
234    binder: &binder::Binder,
235    create_view: ast::CreateView,
236) -> Option<DocumentSymbol> {
237    let path = create_view.path()?;
238    let segment = path.segment()?;
239    let name_node = segment.name()?;
240
241    let (schema, view_name) = resolve_view_info(binder, &path)?;
242    let name = format!("{}.{}", schema.0, view_name);
243
244    let full_range = create_view.syntax().text_range();
245    let focus_range = name_node.syntax().text_range();
246
247    symbols_from_column_list(
248        create_view.column_list(),
249        name,
250        full_range,
251        focus_range,
252        DocumentSymbolKind::View,
253    )
254}
255
256fn symbols_from_column_list(
257    column_list: Option<ast::ColumnList>,
258    name: String,
259    full_range: TextRange,
260    focus_range: TextRange,
261    kind: DocumentSymbolKind,
262) -> Option<DocumentSymbol> {
263    let mut children = vec![];
264    if let Some(column_list) = column_list {
265        for column in column_list.columns() {
266            if let Some(column_symbol) = create_column_symbol(column) {
267                children.push(column_symbol);
268            }
269        }
270    }
271
272    Some(DocumentSymbol {
273        name,
274        detail: None,
275        kind,
276        full_range,
277        focus_range,
278        children,
279    })
280}
281
282// TODO: combine with create_view_symbol
283fn create_materialized_view_symbol(
284    binder: &binder::Binder,
285    create_view: ast::CreateMaterializedView,
286) -> Option<DocumentSymbol> {
287    let path = create_view.path()?;
288    let segment = path.segment()?;
289    let name_node = segment.name()?;
290
291    let (schema, view_name) = resolve_view_info(binder, &path)?;
292    let name = format!("{}.{}", schema.0, view_name);
293
294    let full_range = create_view.syntax().text_range();
295    let focus_range = name_node.syntax().text_range();
296
297    symbols_from_column_list(
298        create_view.column_list(),
299        name,
300        full_range,
301        focus_range,
302        DocumentSymbolKind::MaterializedView,
303    )
304}
305
306fn create_function_symbol(
307    binder: &binder::Binder,
308    create_function: ast::CreateFunction,
309) -> Option<DocumentSymbol> {
310    let path = create_function.path()?;
311    let segment = path.segment()?;
312    let name_node = segment.name()?;
313
314    let (schema, function_name) = resolve_function_info(binder, &path)?;
315    let name = format!("{}.{}", schema.0, function_name);
316
317    let full_range = create_function.syntax().text_range();
318    let focus_range = name_node.syntax().text_range();
319
320    Some(DocumentSymbol {
321        name,
322        detail: None,
323        kind: DocumentSymbolKind::Function,
324        full_range,
325        focus_range,
326        children: vec![],
327    })
328}
329
330fn create_aggregate_symbol(
331    binder: &binder::Binder,
332    create_aggregate: ast::CreateAggregate,
333) -> Option<DocumentSymbol> {
334    let path = create_aggregate.path()?;
335    let segment = path.segment()?;
336    let name_node = segment.name()?;
337
338    let (schema, aggregate_name) = resolve_aggregate_info(binder, &path)?;
339    let name = format!("{}.{}", schema.0, aggregate_name);
340
341    let full_range = create_aggregate.syntax().text_range();
342    let focus_range = name_node.syntax().text_range();
343
344    Some(DocumentSymbol {
345        name,
346        detail: None,
347        kind: DocumentSymbolKind::Aggregate,
348        full_range,
349        focus_range,
350        children: vec![],
351    })
352}
353
354fn create_procedure_symbol(
355    binder: &binder::Binder,
356    create_procedure: ast::CreateProcedure,
357) -> Option<DocumentSymbol> {
358    let path = create_procedure.path()?;
359    let segment = path.segment()?;
360    let name_node = segment.name()?;
361
362    let (schema, procedure_name) = resolve_procedure_info(binder, &path)?;
363    let name = format!("{}.{}", schema.0, procedure_name);
364
365    let full_range = create_procedure.syntax().text_range();
366    let focus_range = name_node.syntax().text_range();
367
368    Some(DocumentSymbol {
369        name,
370        detail: None,
371        kind: DocumentSymbolKind::Procedure,
372        full_range,
373        focus_range,
374        children: vec![],
375    })
376}
377
378fn create_event_trigger_symbol(
379    create_event_trigger: ast::CreateEventTrigger,
380) -> Option<DocumentSymbol> {
381    let name_node = create_event_trigger.name()?;
382    let name = name_node.syntax().text().to_string();
383
384    let full_range = create_event_trigger.syntax().text_range();
385    let focus_range = name_node.syntax().text_range();
386
387    Some(DocumentSymbol {
388        name,
389        detail: None,
390        kind: DocumentSymbolKind::EventTrigger,
391        full_range,
392        focus_range,
393        children: vec![],
394    })
395}
396
397fn create_role_symbol(create_role: ast::CreateRole) -> Option<DocumentSymbol> {
398    let name_node = create_role.name()?;
399    let name = name_node.syntax().text().to_string();
400
401    let full_range = create_role.syntax().text_range();
402    let focus_range = name_node.syntax().text_range();
403
404    Some(DocumentSymbol {
405        name,
406        detail: None,
407        kind: DocumentSymbolKind::Role,
408        full_range,
409        focus_range,
410        children: vec![],
411    })
412}
413
414fn create_type_symbol(
415    binder: &binder::Binder,
416    create_type: ast::CreateType,
417) -> Option<DocumentSymbol> {
418    let path = create_type.path()?;
419    let segment = path.segment()?;
420    let name_node = segment.name()?;
421
422    let (schema, type_name) = resolve_type_info(binder, &path)?;
423    let name = format!("{}.{}", schema.0, type_name);
424
425    let full_range = create_type.syntax().text_range();
426    let focus_range = name_node.syntax().text_range();
427
428    let mut children = vec![];
429    if let Some(variant_list) = create_type.variant_list() {
430        for variant in variant_list.variants() {
431            if let Some(variant_symbol) = create_variant_symbol(variant) {
432                children.push(variant_symbol);
433            }
434        }
435    } else if let Some(column_list) = create_type.column_list() {
436        for column in column_list.columns() {
437            if let Some(column_symbol) = create_column_symbol(column) {
438                children.push(column_symbol);
439            }
440        }
441    }
442
443    Some(DocumentSymbol {
444        name,
445        detail: None,
446        kind: if create_type.variant_list().is_some() {
447            DocumentSymbolKind::Enum
448        } else {
449            DocumentSymbolKind::Type
450        },
451        full_range,
452        focus_range,
453        children,
454    })
455}
456
457fn create_column_symbol(column: ast::Column) -> Option<DocumentSymbol> {
458    let name_node = column.name()?;
459    let name = name_node.syntax().text().to_string();
460
461    let detail = column.ty().map(|t| t.syntax().text().to_string());
462
463    let full_range = column.syntax().text_range();
464    let focus_range = name_node.syntax().text_range();
465
466    Some(DocumentSymbol {
467        name,
468        detail,
469        kind: DocumentSymbolKind::Column,
470        full_range,
471        focus_range,
472        children: vec![],
473    })
474}
475
476fn create_variant_symbol(variant: ast::Variant) -> Option<DocumentSymbol> {
477    let literal = variant.literal()?;
478    let name = extract_string_literal(&literal)?;
479
480    let full_range = variant.syntax().text_range();
481    let focus_range = literal.syntax().text_range();
482
483    Some(DocumentSymbol {
484        name,
485        detail: None,
486        kind: DocumentSymbolKind::Variant,
487        full_range,
488        focus_range,
489        children: vec![],
490    })
491}
492
493fn create_declare_cursor_symbol(declare: ast::Declare) -> Option<DocumentSymbol> {
494    let name_node = declare.name()?;
495    let name = name_node.syntax().text().to_string();
496
497    let full_range = declare.syntax().text_range();
498    let focus_range = name_node.syntax().text_range();
499
500    Some(DocumentSymbol {
501        name,
502        detail: None,
503        kind: DocumentSymbolKind::Cursor,
504        full_range,
505        focus_range,
506        children: vec![],
507    })
508}
509
510fn create_prepare_symbol(prepare: ast::Prepare) -> Option<DocumentSymbol> {
511    let name_node = prepare.name()?;
512    let name = name_node.syntax().text().to_string();
513
514    let full_range = prepare.syntax().text_range();
515    let focus_range = name_node.syntax().text_range();
516
517    Some(DocumentSymbol {
518        name,
519        detail: None,
520        kind: DocumentSymbolKind::PreparedStatement,
521        full_range,
522        focus_range,
523        children: vec![],
524    })
525}
526
527fn create_listen_symbol(listen: ast::Listen) -> Option<DocumentSymbol> {
528    let name_node = listen.name()?;
529    let name = name_node.syntax().text().to_string();
530
531    let full_range = listen.syntax().text_range();
532    let focus_range = name_node.syntax().text_range();
533
534    Some(DocumentSymbol {
535        name,
536        detail: Some("listen".to_string()),
537        kind: DocumentSymbolKind::Channel,
538        full_range,
539        focus_range,
540        children: vec![],
541    })
542}
543
544fn create_notify_symbol(notify: ast::Notify) -> Option<DocumentSymbol> {
545    let name_node = notify.name_ref()?;
546    let name = name_node.syntax().text().to_string();
547
548    let full_range = notify.syntax().text_range();
549    let focus_range = name_node.syntax().text_range();
550
551    Some(DocumentSymbol {
552        name,
553        detail: Some("notify".to_string()),
554        kind: DocumentSymbolKind::Channel,
555        full_range,
556        focus_range,
557        children: vec![],
558    })
559}
560
561fn create_unlisten_symbol(unlisten: ast::Unlisten) -> Option<DocumentSymbol> {
562    let name_node = unlisten.name_ref()?;
563    let name = name_node.syntax().text().to_string();
564
565    let full_range = unlisten.syntax().text_range();
566    let focus_range = name_node.syntax().text_range();
567
568    Some(DocumentSymbol {
569        name,
570        detail: Some("unlisten".to_string()),
571        kind: DocumentSymbolKind::Channel,
572        full_range,
573        focus_range,
574        children: vec![],
575    })
576}
577
578#[cfg(test)]
579mod tests {
580    use super::*;
581    use annotate_snippets::{
582        AnnotationKind, Group, Level, Renderer, Snippet, renderer::DecorStyle,
583    };
584    use insta::assert_snapshot;
585
586    fn symbols_not_found(sql: &str) {
587        let parse = ast::SourceFile::parse(sql);
588        let file = parse.tree();
589        let symbols = document_symbols(&file);
590        if !symbols.is_empty() {
591            panic!("Symbols found. If this is expected, use `symbols` instead.")
592        }
593    }
594
595    fn symbols(sql: &str) -> String {
596        let parse = ast::SourceFile::parse(sql);
597        let file = parse.tree();
598        let symbols = document_symbols(&file);
599        if symbols.is_empty() {
600            panic!("No symbols found. If this is expected, use `symbols_not_found` instead.")
601        }
602
603        let mut output = vec![];
604        for symbol in symbols {
605            let group = symbol_to_group(&symbol, sql);
606            output.push(group);
607        }
608        Renderer::plain()
609            .decor_style(DecorStyle::Unicode)
610            .render(&output)
611            .to_string()
612    }
613
614    fn symbol_to_group<'a>(symbol: &DocumentSymbol, sql: &'a str) -> Group<'a> {
615        let kind = match symbol.kind {
616            DocumentSymbolKind::Schema => "schema",
617            DocumentSymbolKind::Table => "table",
618            DocumentSymbolKind::View => "view",
619            DocumentSymbolKind::MaterializedView => "materialized view",
620            DocumentSymbolKind::Function => "function",
621            DocumentSymbolKind::Aggregate => "aggregate",
622            DocumentSymbolKind::Procedure => "procedure",
623            DocumentSymbolKind::EventTrigger => "event trigger",
624            DocumentSymbolKind::Role => "role",
625            DocumentSymbolKind::Type => "type",
626            DocumentSymbolKind::Enum => "enum",
627            DocumentSymbolKind::Column => "column",
628            DocumentSymbolKind::Variant => "variant",
629            DocumentSymbolKind::Cursor => "cursor",
630            DocumentSymbolKind::PreparedStatement => "prepared statement",
631            DocumentSymbolKind::Channel => "channel",
632        };
633
634        let title = if let Some(detail) = &symbol.detail {
635            format!("{}: {} {}", kind, symbol.name, detail)
636        } else {
637            format!("{}: {}", kind, symbol.name)
638        };
639
640        let snippet = Snippet::source(sql)
641            .fold(true)
642            .annotation(
643                AnnotationKind::Primary
644                    .span(symbol.focus_range.into())
645                    .label("focus range"),
646            )
647            .annotation(
648                AnnotationKind::Context
649                    .span(symbol.full_range.into())
650                    .label("full range"),
651            );
652
653        let mut group = Level::INFO.primary_title(title.clone()).element(snippet);
654
655        if !symbol.children.is_empty() {
656            let child_labels: Vec<String> = symbol
657                .children
658                .iter()
659                .map(|child| {
660                    let kind = match child.kind {
661                        DocumentSymbolKind::Column => "column",
662                        DocumentSymbolKind::Variant => "variant",
663                        _ => unreachable!("only columns and variants can be children"),
664                    };
665                    if let Some(detail) = &child.detail {
666                        format!("{}: {} {}", kind, child.name, detail)
667                    } else {
668                        format!("{}: {}", kind, child.name)
669                    }
670                })
671                .collect();
672
673            let mut children_snippet = Snippet::source(sql).fold(true);
674
675            for (i, child) in symbol.children.iter().enumerate() {
676                children_snippet = children_snippet
677                    .annotation(
678                        AnnotationKind::Context
679                            .span(child.full_range.into())
680                            .label(format!("full range for `{}`", child_labels[i].clone())),
681                    )
682                    .annotation(
683                        AnnotationKind::Primary
684                            .span(child.focus_range.into())
685                            .label("focus range"),
686                    );
687            }
688
689            group = group.element(children_snippet);
690        }
691
692        group
693    }
694
695    #[test]
696    fn create_table() {
697        assert_snapshot!(symbols("
698create table users (
699  id int,
700  email citext
701);"), @r"
702        info: table: public.users
703          ╭▸ 
704        2 │   create table users (
705          │   │            ━━━━━ focus range
706          │ ┌─┘
707          │ │
708        3 │ │   id int,
709        4 │ │   email citext
710        5 │ │ );
711          │ └─┘ full range
712713714        3 │     id int,
715          │     ┯━────
716          │     │
717          │     full range for `column: id int`
718          │     focus range
719        4 │     email citext
720          │     ┯━━━━───────
721          │     │
722          │     full range for `column: email citext`
723          ╰╴    focus range
724        ");
725    }
726
727    #[test]
728    fn create_schema() {
729        assert_snapshot!(symbols("
730create schema foo;
731"), @r"
732        info: schema: foo
733          ╭▸ 
734        2 │ create schema foo;
735          │ ┬─────────────┯━━
736          │ │             │
737          │ │             focus range
738          ╰╴full range
739        ");
740    }
741
742    #[test]
743    fn create_schema_authorization() {
744        assert_snapshot!(symbols("
745create schema authorization foo;
746"), @r"
747        info: schema: foo
748          ╭▸ 
749        2 │ create schema authorization foo;
750          │ ┬───────────────────────────┯━━
751          │ │                           │
752          │ │                           focus range
753          ╰╴full range
754        ");
755    }
756
757    #[test]
758    fn listen_notify_unlisten() {
759        assert_snapshot!(symbols("
760listen updates;
761notify updates;
762unlisten updates;
763unlisten *;
764"), @r"
765        info: channel: updates listen
766          ╭▸ 
767        2 │ listen updates;
768          │ ┬──────┯━━━━━━
769          │ │      │
770          │ │      focus range
771          │ full range
772          ╰╴
773        info: channel: updates notify
774          ╭▸ 
775        3 │ notify updates;
776          │ ┬──────┯━━━━━━
777          │ │      │
778          │ │      focus range
779          ╰╴full range
780        info: channel: updates unlisten
781          ╭▸ 
782        4 │ unlisten updates;
783          │ ┬────────┯━━━━━━
784          │ │        │
785          │ │        focus range
786          ╰╴full range
787        ");
788    }
789
790    #[test]
791    fn create_function() {
792        assert_snapshot!(
793            symbols("create function hello() returns void as $$ select 1; $$ language sql;"),
794            @r"
795        info: function: public.hello
796          ╭▸ 
797        1 │ create function hello() returns void as $$ select 1; $$ language sql;
798          │ ┬───────────────┯━━━━───────────────────────────────────────────────
799          │ │               │
800          │ │               focus range
801          ╰╴full range
802        "
803        );
804    }
805
806    #[test]
807    fn create_materialized_view() {
808        assert_snapshot!(
809            symbols("create materialized view reports as select 1;"),
810            @r"
811        info: materialized view: public.reports
812          ╭▸ 
813        1 │ create materialized view reports as select 1;
814          │ ┬────────────────────────┯━━━━━━────────────
815          │ │                        │
816          │ │                        focus range
817          ╰╴full range
818        "
819        );
820    }
821
822    #[test]
823    fn create_aggregate() {
824        assert_snapshot!(
825            symbols("create aggregate myavg(int) (sfunc = int4_avg_accum, stype = _int8);"),
826            @r"
827        info: aggregate: public.myavg
828          ╭▸ 
829        1 │ create aggregate myavg(int) (sfunc = int4_avg_accum, stype = _int8);
830          │ ┬────────────────┯━━━━─────────────────────────────────────────────
831          │ │                │
832          │ │                focus range
833          ╰╴full range
834        "
835        );
836    }
837
838    #[test]
839    fn create_procedure() {
840        assert_snapshot!(
841            symbols("create procedure hello() language sql as $$ select 1; $$;"),
842            @r"
843        info: procedure: public.hello
844          ╭▸ 
845        1 │ create procedure hello() language sql as $$ select 1; $$;
846          │ ┬────────────────┯━━━━──────────────────────────────────
847          │ │                │
848          │ │                focus range
849          ╰╴full range
850        "
851        );
852    }
853
854    #[test]
855    fn create_event_trigger() {
856        assert_snapshot!(
857            symbols("create event trigger et on ddl_command_start execute function f();"),
858            @r"
859        info: event trigger: et
860          ╭▸ 
861        1 │ create event trigger et on ddl_command_start execute function f();
862          │ ┬────────────────────┯━──────────────────────────────────────────
863          │ │                    │
864          │ │                    focus range
865          ╰╴full range
866        "
867        );
868    }
869
870    #[test]
871    fn create_role() {
872        assert_snapshot!(symbols("
873create role reader;
874"), @r"
875        info: role: reader
876          ╭▸ 
877        2 │ create role reader;
878          │ ┬───────────┯━━━━━
879          │ │           │
880          │ │           focus range
881          ╰╴full range
882        ");
883    }
884
885    #[test]
886    fn multiple_symbols() {
887        assert_snapshot!(symbols("
888create table users (id int);
889create table posts (id int);
890create function get_user(user_id int) returns void as $$ select 1; $$ language sql;
891"), @r"
892        info: table: public.users
893          ╭▸ 
894        2 │ create table users (id int);
895          │ ┬────────────┯━━━━─────────
896          │ │            │
897          │ │            focus range
898          │ full range
899900901        2 │ create table users (id int);
902          │                     ┯━────
903          │                     │
904          │                     full range for `column: id int`
905          │                     focus range
906          ╰╴
907        info: table: public.posts
908          ╭▸ 
909        3 │ create table posts (id int);
910          │ ┬────────────┯━━━━─────────
911          │ │            │
912          │ │            focus range
913          │ full range
914915916        3 │ create table posts (id int);
917          │                     ┯━────
918          │                     │
919          │                     full range for `column: id int`
920          ╰╴                    focus range
921        info: function: public.get_user
922          ╭▸ 
923        4 │ create function get_user(user_id int) returns void as $$ select 1; $$ language sql;
924          │ ┬───────────────┯━━━━━━━──────────────────────────────────────────────────────────
925          │ │               │
926          │ │               focus range
927          ╰╴full range
928        ");
929    }
930
931    #[test]
932    fn qualified_names() {
933        assert_snapshot!(symbols("
934create table public.users (id int);
935create function my_schema.hello() returns void as $$ select 1; $$ language sql;
936"), @r"
937        info: table: public.users
938          ╭▸ 
939        2 │ create table public.users (id int);
940          │ ┬───────────────────┯━━━━─────────
941          │ │                   │
942          │ │                   focus range
943          │ full range
944945946        2 │ create table public.users (id int);
947          │                            ┯━────
948          │                            │
949          │                            full range for `column: id int`
950          │                            focus range
951          ╰╴
952        info: function: my_schema.hello
953          ╭▸ 
954        3 │ create function my_schema.hello() returns void as $$ select 1; $$ language sql;
955          │ ┬─────────────────────────┯━━━━───────────────────────────────────────────────
956          │ │                         │
957          │ │                         focus range
958          ╰╴full range
959        ");
960    }
961
962    #[test]
963    fn create_type() {
964        assert_snapshot!(
965            symbols("create type status as enum ('active', 'inactive');"),
966            @r"
967        info: enum: public.status
968          ╭▸ 
969        1 │ create type status as enum ('active', 'inactive');
970          │ ┬───────────┯━━━━━───────────────────────────────
971          │ │           │
972          │ │           focus range
973          │ full range
974975976        1 │ create type status as enum ('active', 'inactive');
977          │                             ┯━━━━━━━  ┯━━━━━━━━━
978          │                             │         │
979          │                             │         full range for `variant: inactive`
980          │                             │         focus range
981          │                             full range for `variant: active`
982          ╰╴                            focus range
983        "
984        );
985    }
986
987    #[test]
988    fn create_type_composite() {
989        assert_snapshot!(
990            symbols("create type person as (name text, age int);"),
991            @r"
992        info: type: public.person
993          ╭▸ 
994        1 │ create type person as (name text, age int);
995          │ ┬───────────┯━━━━━────────────────────────
996          │ │           │
997          │ │           focus range
998          │ full range
99910001001        1 │ create type person as (name text, age int);
1002          │                        ┯━━━─────  ┯━━────
1003          │                        │          │
1004          │                        │          full range for `column: age int`
1005          │                        │          focus range
1006          │                        full range for `column: name text`
1007          ╰╴                       focus range
1008        "
1009        );
1010    }
1011
1012    #[test]
1013    fn create_type_composite_multiple_columns() {
1014        assert_snapshot!(
1015            symbols("create type address as (street text, city text, zip varchar(10));"),
1016            @r"
1017        info: type: public.address
1018          ╭▸ 
1019        1 │ create type address as (street text, city text, zip varchar(10));
1020          │ ┬───────────┯━━━━━━─────────────────────────────────────────────
1021          │ │           │
1022          │ │           focus range
1023          │ full range
102410251026        1 │ create type address as (street text, city text, zip varchar(10));
1027          │                         ┯━━━━━─────  ┯━━━─────  ┯━━────────────
1028          │                         │            │          │
1029          │                         │            │          full range for `column: zip varchar(10)`
1030          │                         │            │          focus range
1031          │                         │            full range for `column: city text`
1032          │                         │            focus range
1033          │                         full range for `column: street text`
1034          ╰╴                        focus range
1035        "
1036        );
1037    }
1038
1039    #[test]
1040    fn create_type_with_schema() {
1041        assert_snapshot!(
1042            symbols("create type myschema.status as enum ('active', 'inactive');"),
1043            @r"
1044        info: enum: myschema.status
1045          ╭▸ 
1046        1 │ create type myschema.status as enum ('active', 'inactive');
1047          │ ┬────────────────────┯━━━━━───────────────────────────────
1048          │ │                    │
1049          │ │                    focus range
1050          │ full range
105110521053        1 │ create type myschema.status as enum ('active', 'inactive');
1054          │                                      ┯━━━━━━━  ┯━━━━━━━━━
1055          │                                      │         │
1056          │                                      │         full range for `variant: inactive`
1057          │                                      │         focus range
1058          │                                      full range for `variant: active`
1059          ╰╴                                     focus range
1060        "
1061        );
1062    }
1063
1064    #[test]
1065    fn create_type_enum_multiple_variants() {
1066        assert_snapshot!(
1067            symbols("create type priority as enum ('low', 'medium', 'high', 'urgent');"),
1068            @r"
1069        info: enum: public.priority
1070          ╭▸ 
1071        1 │ create type priority as enum ('low', 'medium', 'high', 'urgent');
1072          │ ┬───────────┯━━━━━━━────────────────────────────────────────────
1073          │ │           │
1074          │ │           focus range
1075          │ full range
107610771078        1 │ create type priority as enum ('low', 'medium', 'high', 'urgent');
1079          │                               ┯━━━━  ┯━━━━━━━  ┯━━━━━  ┯━━━━━━━
1080          │                               │      │         │       │
1081          │                               │      │         │       full range for `variant: urgent`
1082          │                               │      │         │       focus range
1083          │                               │      │         full range for `variant: high`
1084          │                               │      │         focus range
1085          │                               │      full range for `variant: medium`
1086          │                               │      focus range
1087          │                               full range for `variant: low`
1088          ╰╴                              focus range
1089        "
1090        );
1091    }
1092
1093    #[test]
1094    fn declare_cursor() {
1095        assert_snapshot!(symbols("
1096declare c scroll cursor for select * from t;
1097"), @r"
1098        info: cursor: c
1099          ╭▸ 
1100        2 │ declare c scroll cursor for select * from t;
1101          │ ┬───────┯──────────────────────────────────
1102          │ │       │
1103          │ │       focus range
1104          ╰╴full range
1105        ");
1106    }
1107
1108    #[test]
1109    fn prepare_statement() {
1110        assert_snapshot!(symbols("
1111prepare stmt as select 1;
1112"), @r"
1113        info: prepared statement: stmt
1114          ╭▸ 
1115        2 │ prepare stmt as select 1;
1116          │ ┬───────┯━━━────────────
1117          │ │       │
1118          │ │       focus range
1119          ╰╴full range
1120        ");
1121    }
1122
1123    #[test]
1124    fn empty_file() {
1125        symbols_not_found("")
1126    }
1127
1128    #[test]
1129    fn non_create_statements() {
1130        symbols_not_found("select * from users;")
1131    }
1132
1133    #[test]
1134    fn cte_table() {
1135        assert_snapshot!(
1136            symbols("
1137with recent_users as (
1138  select id, email as user_email
1139  from users
1140)
1141select * from recent_users;
1142"),
1143            @r"
1144        info: table: recent_users
1145          ╭▸ 
1146        2 │   with recent_users as (
1147          │        │━━━━━━━━━━━
1148          │        │
1149          │ ┌──────focus range
1150          │ │
1151        3 │ │   select id, email as user_email
1152        4 │ │   from users
1153        5 │ │ )
1154          ╰╴└─┘ full range
1155        "
1156        );
1157    }
1158
1159    #[test]
1160    fn cte_table_with_column_list() {
1161        assert_snapshot!(
1162            symbols("
1163with t(a, b, c) as (
1164  select 1, 2, 3
1165)
1166select * from t;
1167"),
1168            @r"
1169        info: table: t
1170          ╭▸ 
1171        2 │   with t(a, b, c) as (
1172          │        ━ focus range
1173          │ ┌──────┘
1174          │ │
1175        3 │ │   select 1, 2, 3
1176        4 │ │ )
1177          │ └─┘ full range
117811791180        2 │   with t(a, b, c) as (
1181          │          ┯  ┯  ┯
1182          │          │  │  │
1183          │          │  │  full range for `column: c`
1184          │          │  │  focus range
1185          │          │  full range for `column: b`
1186          │          │  focus range
1187          │          full range for `column: a`
1188          ╰╴         focus range
1189        "
1190        );
1191    }
1192}