Skip to main content

squawk_ide/
document_symbols.rs

1use rowan::TextRange;
2use salsa::Database as Db;
3use squawk_syntax::ast::{self, AstNode};
4
5use crate::binder::{self, extract_string_literal};
6use crate::db::{File, parse};
7use crate::resolve::{
8    resolve_aggregate_info, resolve_function_info, resolve_procedure_info, resolve_sequence_info,
9    resolve_table_info, resolve_type_info, resolve_view_info,
10};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum DocumentSymbolKind {
14    Schema,
15    Table,
16    View,
17    MaterializedView,
18    Function,
19    Aggregate,
20    Procedure,
21    EventTrigger,
22    Role,
23    Policy,
24    Type,
25    Enum,
26    Index,
27    Domain,
28    Sequence,
29    Trigger,
30    Tablespace,
31    Database,
32    Server,
33    Extension,
34    Column,
35    Variant,
36    Cursor,
37    PreparedStatement,
38    Channel,
39}
40
41#[derive(Debug, Clone, PartialEq, Eq)]
42pub struct DocumentSymbol {
43    pub name: String,
44    pub detail: Option<String>,
45    pub kind: DocumentSymbolKind,
46    /// Range used for determining when cursor is inside the symbol for showing
47    /// in the UI
48    pub full_range: TextRange,
49    /// Range selected when symbol is selected
50    pub focus_range: TextRange,
51    pub children: Vec<DocumentSymbol>,
52}
53
54#[salsa::tracked]
55pub fn document_symbols(db: &dyn Db, file: File) -> Vec<DocumentSymbol> {
56    let parse = parse(db, file);
57    let source_file = parse.tree();
58
59    // TODO: we should salsa this
60    let binder = binder::bind(&source_file);
61    let mut symbols = vec![];
62
63    for stmt in source_file.stmts() {
64        match stmt {
65            ast::Stmt::CreateSchema(create_schema) => {
66                if let Some(symbol) = create_schema_symbol(create_schema) {
67                    symbols.push(symbol);
68                }
69            }
70            ast::Stmt::CreateTable(create_table) => {
71                if let Some(symbol) = create_table_symbol(&binder, create_table) {
72                    symbols.push(symbol);
73                }
74            }
75            ast::Stmt::CreateTableAs(create_table_as) => {
76                if let Some(symbol) = create_table_as_symbol(&binder, create_table_as) {
77                    symbols.push(symbol);
78                }
79            }
80            ast::Stmt::CreateForeignTable(create_foreign_table) => {
81                if let Some(symbol) = create_table_symbol(&binder, create_foreign_table) {
82                    symbols.push(symbol);
83                }
84            }
85            ast::Stmt::CreateFunction(create_function) => {
86                if let Some(symbol) = create_function_symbol(&binder, create_function) {
87                    symbols.push(symbol);
88                }
89            }
90            ast::Stmt::CreateAggregate(create_aggregate) => {
91                if let Some(symbol) = create_aggregate_symbol(&binder, create_aggregate) {
92                    symbols.push(symbol);
93                }
94            }
95            ast::Stmt::CreateProcedure(create_procedure) => {
96                if let Some(symbol) = create_procedure_symbol(&binder, create_procedure) {
97                    symbols.push(symbol);
98                }
99            }
100            ast::Stmt::CreateIndex(create_index) => {
101                if let Some(symbol) = create_index_symbol(create_index) {
102                    symbols.push(symbol);
103                }
104            }
105            ast::Stmt::CreateDomain(create_domain) => {
106                if let Some(symbol) = create_domain_symbol(&binder, create_domain) {
107                    symbols.push(symbol);
108                }
109            }
110            ast::Stmt::CreateSequence(create_sequence) => {
111                if let Some(symbol) = create_sequence_symbol(&binder, create_sequence) {
112                    symbols.push(symbol);
113                }
114            }
115            ast::Stmt::CreateTrigger(create_trigger) => {
116                if let Some(symbol) = create_trigger_symbol(create_trigger) {
117                    symbols.push(symbol);
118                }
119            }
120            ast::Stmt::CreateEventTrigger(create_event_trigger) => {
121                if let Some(symbol) = create_event_trigger_symbol(create_event_trigger) {
122                    symbols.push(symbol);
123                }
124            }
125            ast::Stmt::CreateTablespace(create_tablespace) => {
126                if let Some(symbol) = create_tablespace_symbol(create_tablespace) {
127                    symbols.push(symbol);
128                }
129            }
130            ast::Stmt::CreateDatabase(create_database) => {
131                if let Some(symbol) = create_database_symbol(create_database) {
132                    symbols.push(symbol);
133                }
134            }
135            ast::Stmt::CreateServer(create_server) => {
136                if let Some(symbol) = create_server_symbol(create_server) {
137                    symbols.push(symbol);
138                }
139            }
140            ast::Stmt::CreateExtension(create_extension) => {
141                if let Some(symbol) = create_extension_symbol(create_extension) {
142                    symbols.push(symbol);
143                }
144            }
145            ast::Stmt::CreateRole(create_role) => {
146                if let Some(symbol) = create_role_symbol(create_role) {
147                    symbols.push(symbol);
148                }
149            }
150            ast::Stmt::CreatePolicy(create_policy) => {
151                if let Some(symbol) = create_policy_symbol(create_policy) {
152                    symbols.push(symbol);
153                }
154            }
155            ast::Stmt::CreateType(create_type) => {
156                if let Some(symbol) = create_type_symbol(&binder, create_type) {
157                    symbols.push(symbol);
158                }
159            }
160            ast::Stmt::CreateView(create_view) => {
161                if let Some(symbol) = create_view_symbol(&binder, create_view) {
162                    symbols.push(symbol);
163                }
164            }
165            ast::Stmt::CreateMaterializedView(create_view) => {
166                if let Some(symbol) = create_materialized_view_symbol(&binder, create_view) {
167                    symbols.push(symbol);
168                }
169            }
170            ast::Stmt::Declare(declare) => {
171                if let Some(symbol) = create_declare_cursor_symbol(declare) {
172                    symbols.push(symbol);
173                }
174            }
175            ast::Stmt::Prepare(prepare) => {
176                if let Some(symbol) = create_prepare_symbol(prepare) {
177                    symbols.push(symbol);
178                }
179            }
180            ast::Stmt::Select(select) => {
181                symbols.extend(cte_table_symbols(select));
182            }
183            ast::Stmt::SelectInto(select_into) => {
184                symbols.extend(cte_table_symbols(select_into));
185            }
186            ast::Stmt::Insert(insert) => {
187                symbols.extend(cte_table_symbols(insert));
188            }
189            ast::Stmt::Update(update) => {
190                symbols.extend(cte_table_symbols(update));
191            }
192            ast::Stmt::Delete(delete) => {
193                symbols.extend(cte_table_symbols(delete));
194            }
195            ast::Stmt::Listen(listen) => {
196                if let Some(symbol) = create_listen_symbol(listen) {
197                    symbols.push(symbol);
198                }
199            }
200            ast::Stmt::Notify(notify) => {
201                if let Some(symbol) = create_notify_symbol(notify) {
202                    symbols.push(symbol);
203                }
204            }
205            ast::Stmt::Unlisten(unlisten) => {
206                if let Some(symbol) = create_unlisten_symbol(unlisten) {
207                    symbols.push(symbol);
208                }
209            }
210
211            _ => (),
212        }
213    }
214
215    symbols
216}
217
218fn cte_table_symbols(stmt: impl ast::HasWithClause) -> Vec<DocumentSymbol> {
219    let Some(with_clause) = stmt.with_clause() else {
220        return vec![];
221    };
222
223    with_clause
224        .with_tables()
225        .filter_map(create_cte_table_symbol)
226        .collect()
227}
228
229fn create_cte_table_symbol(with_table: ast::WithTable) -> Option<DocumentSymbol> {
230    let name_node = with_table.name()?;
231    let name = name_node.syntax().text().to_string();
232
233    let full_range = with_table.syntax().text_range();
234    let focus_range = name_node.syntax().text_range();
235
236    symbols_from_column_list(
237        with_table.column_list(),
238        name,
239        full_range,
240        focus_range,
241        DocumentSymbolKind::Table,
242    )
243}
244
245fn create_schema_symbol(create_schema: ast::CreateSchema) -> Option<DocumentSymbol> {
246    let (name, focus_range) = if let Some(name_node) = create_schema.name() {
247        (
248            name_node.syntax().text().to_string(),
249            name_node.syntax().text_range(),
250        )
251    } else if let Some(name) = create_schema.role().and_then(|r| r.name()) {
252        (name.syntax().text().to_string(), name.syntax().text_range())
253    } else {
254        return None;
255    };
256
257    let full_range = create_schema.syntax().text_range();
258
259    Some(DocumentSymbol {
260        name,
261        detail: None,
262        kind: DocumentSymbolKind::Schema,
263        full_range,
264        focus_range,
265        children: vec![],
266    })
267}
268
269fn create_table_symbol(
270    binder: &binder::Binder,
271    create_table: impl ast::HasCreateTable,
272) -> Option<DocumentSymbol> {
273    let path = create_table.path()?;
274    let segment = path.segment()?;
275    let name_node = segment.name()?;
276
277    let (schema, table_name) = resolve_table_info(binder, &path)?;
278    let name = format!("{}.{}", schema.0, table_name);
279
280    let full_range = create_table.syntax().text_range();
281    let focus_range = name_node.syntax().text_range();
282
283    let mut children = vec![];
284    if let Some(table_arg_list) = create_table.table_arg_list() {
285        for arg in table_arg_list.args() {
286            if let ast::TableArg::Column(column) = arg
287                && let Some(column_symbol) = create_column_symbol(column)
288            {
289                children.push(column_symbol);
290            }
291        }
292    }
293
294    Some(DocumentSymbol {
295        name,
296        detail: None,
297        kind: DocumentSymbolKind::Table,
298        full_range,
299        focus_range,
300        children,
301    })
302}
303
304fn create_table_as_symbol(
305    binder: &binder::Binder,
306    create_table_as: ast::CreateTableAs,
307) -> Option<DocumentSymbol> {
308    let path = create_table_as.path()?;
309    let segment = path.segment()?;
310    let name_node = if let Some(name) = segment.name() {
311        name.syntax().clone()
312    } else {
313        return None;
314    };
315
316    let (schema, table_name) = resolve_table_info(binder, &path)?;
317    let name = format!("{}.{}", schema.0, table_name);
318
319    let full_range = create_table_as.syntax().text_range();
320    let focus_range = name_node.text_range();
321
322    Some(DocumentSymbol {
323        name,
324        detail: None,
325        kind: DocumentSymbolKind::Table,
326        full_range,
327        focus_range,
328        // TODO: infer the column names, we need the same for views without
329        // explicit column lists
330        children: vec![],
331    })
332}
333
334fn create_view_symbol(
335    binder: &binder::Binder,
336    create_view: ast::CreateView,
337) -> Option<DocumentSymbol> {
338    let path = create_view.path()?;
339    let segment = path.segment()?;
340    let name_node = segment.name()?;
341
342    let (schema, view_name) = resolve_view_info(binder, &path)?;
343    let name = format!("{}.{}", schema.0, view_name);
344
345    let full_range = create_view.syntax().text_range();
346    let focus_range = name_node.syntax().text_range();
347
348    symbols_from_column_list(
349        create_view.column_list(),
350        name,
351        full_range,
352        focus_range,
353        DocumentSymbolKind::View,
354    )
355}
356
357fn symbols_from_column_list(
358    column_list: Option<ast::ColumnList>,
359    name: String,
360    full_range: TextRange,
361    focus_range: TextRange,
362    kind: DocumentSymbolKind,
363) -> Option<DocumentSymbol> {
364    let mut children = vec![];
365    if let Some(column_list) = column_list {
366        for column in column_list.columns() {
367            if let Some(column_symbol) = create_column_symbol(column) {
368                children.push(column_symbol);
369            }
370        }
371    }
372
373    Some(DocumentSymbol {
374        name,
375        detail: None,
376        kind,
377        full_range,
378        focus_range,
379        children,
380    })
381}
382
383// TODO: combine with create_view_symbol
384fn create_materialized_view_symbol(
385    binder: &binder::Binder,
386    create_view: ast::CreateMaterializedView,
387) -> Option<DocumentSymbol> {
388    let path = create_view.path()?;
389    let segment = path.segment()?;
390    let name_node = segment.name()?;
391
392    let (schema, view_name) = resolve_view_info(binder, &path)?;
393    let name = format!("{}.{}", schema.0, view_name);
394
395    let full_range = create_view.syntax().text_range();
396    let focus_range = name_node.syntax().text_range();
397
398    symbols_from_column_list(
399        create_view.column_list(),
400        name,
401        full_range,
402        focus_range,
403        DocumentSymbolKind::MaterializedView,
404    )
405}
406
407fn create_function_symbol(
408    binder: &binder::Binder,
409    create_function: ast::CreateFunction,
410) -> Option<DocumentSymbol> {
411    let path = create_function.path()?;
412    let segment = path.segment()?;
413    let name_node = segment.name()?;
414
415    let (schema, function_name) = resolve_function_info(binder, &path)?;
416    let name = format!("{}.{}", schema.0, function_name);
417
418    let full_range = create_function.syntax().text_range();
419    let focus_range = name_node.syntax().text_range();
420
421    Some(DocumentSymbol {
422        name,
423        detail: None,
424        kind: DocumentSymbolKind::Function,
425        full_range,
426        focus_range,
427        children: vec![],
428    })
429}
430
431fn create_aggregate_symbol(
432    binder: &binder::Binder,
433    create_aggregate: ast::CreateAggregate,
434) -> Option<DocumentSymbol> {
435    let path = create_aggregate.path()?;
436    let segment = path.segment()?;
437    let name_node = segment.name()?;
438
439    let (schema, aggregate_name) = resolve_aggregate_info(binder, &path)?;
440    let name = format!("{}.{}", schema.0, aggregate_name);
441
442    let full_range = create_aggregate.syntax().text_range();
443    let focus_range = name_node.syntax().text_range();
444
445    Some(DocumentSymbol {
446        name,
447        detail: None,
448        kind: DocumentSymbolKind::Aggregate,
449        full_range,
450        focus_range,
451        children: vec![],
452    })
453}
454
455fn create_procedure_symbol(
456    binder: &binder::Binder,
457    create_procedure: ast::CreateProcedure,
458) -> Option<DocumentSymbol> {
459    let path = create_procedure.path()?;
460    let segment = path.segment()?;
461    let name_node = segment.name()?;
462
463    let (schema, procedure_name) = resolve_procedure_info(binder, &path)?;
464    let name = format!("{}.{}", schema.0, procedure_name);
465
466    let full_range = create_procedure.syntax().text_range();
467    let focus_range = name_node.syntax().text_range();
468
469    Some(DocumentSymbol {
470        name,
471        detail: None,
472        kind: DocumentSymbolKind::Procedure,
473        full_range,
474        focus_range,
475        children: vec![],
476    })
477}
478
479fn create_index_symbol(create_index: ast::CreateIndex) -> Option<DocumentSymbol> {
480    let name_node = create_index.name()?;
481    let name = name_node.syntax().text().to_string();
482
483    let full_range = create_index.syntax().text_range();
484    let focus_range = name_node.syntax().text_range();
485
486    Some(DocumentSymbol {
487        name,
488        detail: None,
489        kind: DocumentSymbolKind::Index,
490        full_range,
491        focus_range,
492        children: vec![],
493    })
494}
495
496fn create_domain_symbol(
497    binder: &binder::Binder,
498    create_domain: ast::CreateDomain,
499) -> Option<DocumentSymbol> {
500    let path = create_domain.path()?;
501    let segment = path.segment()?;
502    let name_node = segment.name()?;
503
504    let (schema, domain_name) = resolve_type_info(binder, &path)?;
505    let name = format!("{}.{}", schema.0, domain_name);
506
507    let full_range = create_domain.syntax().text_range();
508    let focus_range = name_node.syntax().text_range();
509
510    Some(DocumentSymbol {
511        name,
512        detail: None,
513        kind: DocumentSymbolKind::Domain,
514        full_range,
515        focus_range,
516        children: vec![],
517    })
518}
519
520fn create_sequence_symbol(
521    binder: &binder::Binder,
522    create_sequence: ast::CreateSequence,
523) -> Option<DocumentSymbol> {
524    let path = create_sequence.path()?;
525    let segment = path.segment()?;
526    let name_node = segment.name()?;
527
528    let (schema, sequence_name) = resolve_sequence_info(binder, &path)?;
529    let name = format!("{}.{}", schema.0, sequence_name);
530
531    let full_range = create_sequence.syntax().text_range();
532    let focus_range = name_node.syntax().text_range();
533
534    Some(DocumentSymbol {
535        name,
536        detail: None,
537        kind: DocumentSymbolKind::Sequence,
538        full_range,
539        focus_range,
540        children: vec![],
541    })
542}
543
544fn create_trigger_symbol(create_trigger: ast::CreateTrigger) -> Option<DocumentSymbol> {
545    let name_node = create_trigger.name()?;
546    let name = name_node.syntax().text().to_string();
547
548    let full_range = create_trigger.syntax().text_range();
549    let focus_range = name_node.syntax().text_range();
550
551    Some(DocumentSymbol {
552        name,
553        detail: None,
554        kind: DocumentSymbolKind::Trigger,
555        full_range,
556        focus_range,
557        children: vec![],
558    })
559}
560
561fn create_event_trigger_symbol(
562    create_event_trigger: ast::CreateEventTrigger,
563) -> Option<DocumentSymbol> {
564    let name_node = create_event_trigger.name()?;
565    let name = name_node.syntax().text().to_string();
566
567    let full_range = create_event_trigger.syntax().text_range();
568    let focus_range = name_node.syntax().text_range();
569
570    Some(DocumentSymbol {
571        name,
572        detail: None,
573        kind: DocumentSymbolKind::EventTrigger,
574        full_range,
575        focus_range,
576        children: vec![],
577    })
578}
579
580fn create_tablespace_symbol(create_tablespace: ast::CreateTablespace) -> Option<DocumentSymbol> {
581    let name_node = create_tablespace.name()?;
582    let name = name_node.syntax().text().to_string();
583
584    let full_range = create_tablespace.syntax().text_range();
585    let focus_range = name_node.syntax().text_range();
586
587    Some(DocumentSymbol {
588        name,
589        detail: None,
590        kind: DocumentSymbolKind::Tablespace,
591        full_range,
592        focus_range,
593        children: vec![],
594    })
595}
596
597fn create_database_symbol(create_database: ast::CreateDatabase) -> Option<DocumentSymbol> {
598    let name_node = create_database.name()?;
599    let name = name_node.syntax().text().to_string();
600
601    let full_range = create_database.syntax().text_range();
602    let focus_range = name_node.syntax().text_range();
603
604    Some(DocumentSymbol {
605        name,
606        detail: None,
607        kind: DocumentSymbolKind::Database,
608        full_range,
609        focus_range,
610        children: vec![],
611    })
612}
613
614fn create_server_symbol(create_server: ast::CreateServer) -> Option<DocumentSymbol> {
615    let name_node = create_server.name()?;
616    let name = name_node.syntax().text().to_string();
617
618    let full_range = create_server.syntax().text_range();
619    let focus_range = name_node.syntax().text_range();
620
621    Some(DocumentSymbol {
622        name,
623        detail: None,
624        kind: DocumentSymbolKind::Server,
625        full_range,
626        focus_range,
627        children: vec![],
628    })
629}
630
631fn create_extension_symbol(create_extension: ast::CreateExtension) -> Option<DocumentSymbol> {
632    let name_node = create_extension.name()?;
633    let name = name_node.syntax().text().to_string();
634
635    let full_range = create_extension.syntax().text_range();
636    let focus_range = name_node.syntax().text_range();
637
638    Some(DocumentSymbol {
639        name,
640        detail: None,
641        kind: DocumentSymbolKind::Extension,
642        full_range,
643        focus_range,
644        children: vec![],
645    })
646}
647
648fn create_role_symbol(create_role: ast::CreateRole) -> Option<DocumentSymbol> {
649    let name_node = create_role.name()?;
650    let name = name_node.syntax().text().to_string();
651
652    let full_range = create_role.syntax().text_range();
653    let focus_range = name_node.syntax().text_range();
654
655    Some(DocumentSymbol {
656        name,
657        detail: None,
658        kind: DocumentSymbolKind::Role,
659        full_range,
660        focus_range,
661        children: vec![],
662    })
663}
664
665fn create_policy_symbol(create_policy: ast::CreatePolicy) -> Option<DocumentSymbol> {
666    let name_node = create_policy.name()?;
667    let name = name_node.syntax().text().to_string();
668
669    let full_range = create_policy.syntax().text_range();
670    let focus_range = name_node.syntax().text_range();
671
672    Some(DocumentSymbol {
673        name,
674        detail: None,
675        kind: DocumentSymbolKind::Policy,
676        full_range,
677        focus_range,
678        children: vec![],
679    })
680}
681
682fn create_type_symbol(
683    binder: &binder::Binder,
684    create_type: ast::CreateType,
685) -> Option<DocumentSymbol> {
686    let path = create_type.path()?;
687    let segment = path.segment()?;
688    let name_node = segment.name()?;
689
690    let (schema, type_name) = resolve_type_info(binder, &path)?;
691    let name = format!("{}.{}", schema.0, type_name);
692
693    let full_range = create_type.syntax().text_range();
694    let focus_range = name_node.syntax().text_range();
695
696    let mut children = vec![];
697    if let Some(variant_list) = create_type.variant_list() {
698        for variant in variant_list.variants() {
699            if let Some(variant_symbol) = create_variant_symbol(variant) {
700                children.push(variant_symbol);
701            }
702        }
703    } else if let Some(column_list) = create_type.column_list() {
704        for column in column_list.columns() {
705            if let Some(column_symbol) = create_column_symbol(column) {
706                children.push(column_symbol);
707            }
708        }
709    }
710
711    Some(DocumentSymbol {
712        name,
713        detail: None,
714        kind: if create_type.variant_list().is_some() {
715            DocumentSymbolKind::Enum
716        } else {
717            DocumentSymbolKind::Type
718        },
719        full_range,
720        focus_range,
721        children,
722    })
723}
724
725fn create_column_symbol(column: ast::Column) -> Option<DocumentSymbol> {
726    let name_node = column.name()?;
727    let name = name_node.syntax().text().to_string();
728
729    let detail = column.ty().map(|t| t.syntax().text().to_string());
730
731    let full_range = column.syntax().text_range();
732    let focus_range = name_node.syntax().text_range();
733
734    Some(DocumentSymbol {
735        name,
736        detail,
737        kind: DocumentSymbolKind::Column,
738        full_range,
739        focus_range,
740        children: vec![],
741    })
742}
743
744fn create_variant_symbol(variant: ast::Variant) -> Option<DocumentSymbol> {
745    let literal = variant.literal()?;
746    let name = extract_string_literal(&literal)?;
747
748    let full_range = variant.syntax().text_range();
749    let focus_range = literal.syntax().text_range();
750
751    Some(DocumentSymbol {
752        name,
753        detail: None,
754        kind: DocumentSymbolKind::Variant,
755        full_range,
756        focus_range,
757        children: vec![],
758    })
759}
760
761fn create_declare_cursor_symbol(declare: ast::Declare) -> Option<DocumentSymbol> {
762    let name_node = declare.name()?;
763    let name = name_node.syntax().text().to_string();
764
765    let full_range = declare.syntax().text_range();
766    let focus_range = name_node.syntax().text_range();
767
768    Some(DocumentSymbol {
769        name,
770        detail: None,
771        kind: DocumentSymbolKind::Cursor,
772        full_range,
773        focus_range,
774        children: vec![],
775    })
776}
777
778fn create_prepare_symbol(prepare: ast::Prepare) -> Option<DocumentSymbol> {
779    let name_node = prepare.name()?;
780    let name = name_node.syntax().text().to_string();
781
782    let full_range = prepare.syntax().text_range();
783    let focus_range = name_node.syntax().text_range();
784
785    Some(DocumentSymbol {
786        name,
787        detail: None,
788        kind: DocumentSymbolKind::PreparedStatement,
789        full_range,
790        focus_range,
791        children: vec![],
792    })
793}
794
795fn create_listen_symbol(listen: ast::Listen) -> Option<DocumentSymbol> {
796    let name_node = listen.name()?;
797    let name = name_node.syntax().text().to_string();
798
799    let full_range = listen.syntax().text_range();
800    let focus_range = name_node.syntax().text_range();
801
802    Some(DocumentSymbol {
803        name,
804        detail: Some("listen".to_string()),
805        kind: DocumentSymbolKind::Channel,
806        full_range,
807        focus_range,
808        children: vec![],
809    })
810}
811
812fn create_notify_symbol(notify: ast::Notify) -> Option<DocumentSymbol> {
813    let name_node = notify.name_ref()?;
814    let name = name_node.syntax().text().to_string();
815
816    let full_range = notify.syntax().text_range();
817    let focus_range = name_node.syntax().text_range();
818
819    Some(DocumentSymbol {
820        name,
821        detail: Some("notify".to_string()),
822        kind: DocumentSymbolKind::Channel,
823        full_range,
824        focus_range,
825        children: vec![],
826    })
827}
828
829fn create_unlisten_symbol(unlisten: ast::Unlisten) -> Option<DocumentSymbol> {
830    let name_node = unlisten.name_ref()?;
831    let name = name_node.syntax().text().to_string();
832
833    let full_range = unlisten.syntax().text_range();
834    let focus_range = name_node.syntax().text_range();
835
836    Some(DocumentSymbol {
837        name,
838        detail: Some("unlisten".to_string()),
839        kind: DocumentSymbolKind::Channel,
840        full_range,
841        focus_range,
842        children: vec![],
843    })
844}
845
846#[cfg(test)]
847mod tests {
848    use super::*;
849    use crate::db::{Database, File};
850    use annotate_snippets::{
851        AnnotationKind, Group, Level, Renderer, Snippet, renderer::DecorStyle,
852    };
853    use insta::assert_snapshot;
854
855    fn symbols_not_found(sql: &str) {
856        let db = Database::default();
857        let file = File::new(&db, sql.to_string().into());
858        let symbols = document_symbols(&db, file);
859        if !symbols.is_empty() {
860            panic!("Symbols found. If this is expected, use `symbols` instead.")
861        }
862    }
863
864    fn symbols(sql: &str) -> String {
865        let db = Database::default();
866        let file = File::new(&db, sql.to_string().into());
867        let symbols = document_symbols(&db, file);
868        if symbols.is_empty() {
869            panic!("No symbols found. If this is expected, use `symbols_not_found` instead.")
870        }
871
872        let mut output = vec![];
873        for symbol in symbols {
874            let group = symbol_to_group(&symbol, sql);
875            output.push(group);
876        }
877        Renderer::plain()
878            .decor_style(DecorStyle::Unicode)
879            .render(&output)
880            .to_string()
881    }
882
883    fn symbol_to_group<'a>(symbol: &DocumentSymbol, sql: &'a str) -> Group<'a> {
884        let kind = match symbol.kind {
885            DocumentSymbolKind::Schema => "schema",
886            DocumentSymbolKind::Table => "table",
887            DocumentSymbolKind::View => "view",
888            DocumentSymbolKind::MaterializedView => "materialized view",
889            DocumentSymbolKind::Function => "function",
890            DocumentSymbolKind::Aggregate => "aggregate",
891            DocumentSymbolKind::Procedure => "procedure",
892            DocumentSymbolKind::EventTrigger => "event trigger",
893            DocumentSymbolKind::Role => "role",
894            DocumentSymbolKind::Policy => "policy",
895            DocumentSymbolKind::Type => "type",
896            DocumentSymbolKind::Enum => "enum",
897            DocumentSymbolKind::Index => "index",
898            DocumentSymbolKind::Domain => "domain",
899            DocumentSymbolKind::Sequence => "sequence",
900            DocumentSymbolKind::Trigger => "trigger",
901            DocumentSymbolKind::Tablespace => "tablespace",
902            DocumentSymbolKind::Database => "database",
903            DocumentSymbolKind::Server => "server",
904            DocumentSymbolKind::Extension => "extension",
905            DocumentSymbolKind::Column => "column",
906            DocumentSymbolKind::Variant => "variant",
907            DocumentSymbolKind::Cursor => "cursor",
908            DocumentSymbolKind::PreparedStatement => "prepared statement",
909            DocumentSymbolKind::Channel => "channel",
910        };
911
912        let title = if let Some(detail) = &symbol.detail {
913            format!("{}: {} {}", kind, symbol.name, detail)
914        } else {
915            format!("{}: {}", kind, symbol.name)
916        };
917
918        let snippet = Snippet::source(sql)
919            .fold(true)
920            .annotation(
921                AnnotationKind::Primary
922                    .span(symbol.focus_range.into())
923                    .label("focus range"),
924            )
925            .annotation(
926                AnnotationKind::Context
927                    .span(symbol.full_range.into())
928                    .label("full range"),
929            );
930
931        let mut group = Level::INFO.primary_title(title.clone()).element(snippet);
932
933        if !symbol.children.is_empty() {
934            let child_labels: Vec<String> = symbol
935                .children
936                .iter()
937                .map(|child| {
938                    let kind = match child.kind {
939                        DocumentSymbolKind::Column => "column",
940                        DocumentSymbolKind::Variant => "variant",
941                        _ => unreachable!("only columns and variants can be children"),
942                    };
943                    if let Some(detail) = &child.detail {
944                        format!("{}: {} {}", kind, child.name, detail)
945                    } else {
946                        format!("{}: {}", kind, child.name)
947                    }
948                })
949                .collect();
950
951            let mut children_snippet = Snippet::source(sql).fold(true);
952
953            for (i, child) in symbol.children.iter().enumerate() {
954                children_snippet = children_snippet
955                    .annotation(
956                        AnnotationKind::Context
957                            .span(child.full_range.into())
958                            .label(format!("full range for `{}`", child_labels[i].clone())),
959                    )
960                    .annotation(
961                        AnnotationKind::Primary
962                            .span(child.focus_range.into())
963                            .label("focus range"),
964                    );
965            }
966
967            group = group.element(children_snippet);
968        }
969
970        group
971    }
972
973    #[test]
974    fn create_table() {
975        assert_snapshot!(symbols("
976create table users (
977  id int,
978  email citext
979);"), @r"
980        info: table: public.users
981          ╭▸ 
982        2 │   create table users (
983          │   │            ━━━━━ focus range
984          │ ┌─┘
985          │ │
986        3 │ │   id int,
987        4 │ │   email citext
988        5 │ │ );
989          │ └─┘ full range
990991992        3 │     id int,
993          │     ┯━────
994          │     │
995          │     full range for `column: id int`
996          │     focus range
997        4 │     email citext
998          │     ┯━━━━───────
999          │     │
1000          │     full range for `column: email citext`
1001          ╰╴    focus range
1002        ");
1003    }
1004
1005    #[test]
1006    fn create_table_as() {
1007        assert_snapshot!(symbols("
1008create table t as select 1 a;
1009"), @r"
1010        info: table: public.t
1011          ╭▸ 
1012        2 │ create table t as select 1 a;
1013          │ ┬────────────┯──────────────
1014          │ │            │
1015          │ │            focus range
1016          ╰╴full range
1017        ");
1018    }
1019
1020    #[test]
1021    fn create_schema() {
1022        assert_snapshot!(symbols("
1023create schema foo;
1024"), @r"
1025        info: schema: foo
1026          ╭▸ 
1027        2 │ create schema foo;
1028          │ ┬─────────────┯━━
1029          │ │             │
1030          │ │             focus range
1031          ╰╴full range
1032        ");
1033    }
1034
1035    #[test]
1036    fn create_schema_authorization() {
1037        assert_snapshot!(symbols("
1038create schema authorization foo;
1039"), @r"
1040        info: schema: foo
1041          ╭▸ 
1042        2 │ create schema authorization foo;
1043          │ ┬───────────────────────────┯━━
1044          │ │                           │
1045          │ │                           focus range
1046          ╰╴full range
1047        ");
1048    }
1049
1050    #[test]
1051    fn listen_notify_unlisten() {
1052        assert_snapshot!(symbols("
1053listen updates;
1054notify updates;
1055unlisten updates;
1056unlisten *;
1057"), @r"
1058        info: channel: updates listen
1059          ╭▸ 
1060        2 │ listen updates;
1061          │ ┬──────┯━━━━━━
1062          │ │      │
1063          │ │      focus range
1064          │ full range
1065          ╰╴
1066        info: channel: updates notify
1067          ╭▸ 
1068        3 │ notify updates;
1069          │ ┬──────┯━━━━━━
1070          │ │      │
1071          │ │      focus range
1072          ╰╴full range
1073        info: channel: updates unlisten
1074          ╭▸ 
1075        4 │ unlisten updates;
1076          │ ┬────────┯━━━━━━
1077          │ │        │
1078          │ │        focus range
1079          ╰╴full range
1080        ");
1081    }
1082
1083    #[test]
1084    fn create_function() {
1085        assert_snapshot!(
1086            symbols("create function hello() returns void as $$ select 1; $$ language sql;"),
1087            @r"
1088        info: function: public.hello
1089          ╭▸ 
1090        1 │ create function hello() returns void as $$ select 1; $$ language sql;
1091          │ ┬───────────────┯━━━━───────────────────────────────────────────────
1092          │ │               │
1093          │ │               focus range
1094          ╰╴full range
1095        "
1096        );
1097    }
1098
1099    #[test]
1100    fn create_materialized_view() {
1101        assert_snapshot!(
1102            symbols("create materialized view reports as select 1;"),
1103            @r"
1104        info: materialized view: public.reports
1105          ╭▸ 
1106        1 │ create materialized view reports as select 1;
1107          │ ┬────────────────────────┯━━━━━━────────────
1108          │ │                        │
1109          │ │                        focus range
1110          ╰╴full range
1111        "
1112        );
1113    }
1114
1115    #[test]
1116    fn create_aggregate() {
1117        assert_snapshot!(
1118            symbols("create aggregate myavg(int) (sfunc = int4_avg_accum, stype = _int8);"),
1119            @r"
1120        info: aggregate: public.myavg
1121          ╭▸ 
1122        1 │ create aggregate myavg(int) (sfunc = int4_avg_accum, stype = _int8);
1123          │ ┬────────────────┯━━━━─────────────────────────────────────────────
1124          │ │                │
1125          │ │                focus range
1126          ╰╴full range
1127        "
1128        );
1129    }
1130
1131    #[test]
1132    fn create_procedure() {
1133        assert_snapshot!(
1134            symbols("create procedure hello() language sql as $$ select 1; $$;"),
1135            @r"
1136        info: procedure: public.hello
1137          ╭▸ 
1138        1 │ create procedure hello() language sql as $$ select 1; $$;
1139          │ ┬────────────────┯━━━━──────────────────────────────────
1140          │ │                │
1141          │ │                focus range
1142          ╰╴full range
1143        "
1144        );
1145    }
1146
1147    #[test]
1148    fn create_index() {
1149        assert_snapshot!(symbols("
1150create index idx_users_email on users (email);
1151"), @r"
1152        info: index: idx_users_email
1153          ╭▸ 
1154        2 │ create index idx_users_email on users (email);
1155          │ ┬────────────┯━━━━━━━━━━━━━━─────────────────
1156          │ │            │
1157          │ │            focus range
1158          ╰╴full range
1159        ");
1160    }
1161
1162    #[test]
1163    fn create_domain() {
1164        assert_snapshot!(
1165            symbols("create domain email_addr as text;"),
1166            @r"
1167        info: domain: public.email_addr
1168          ╭▸ 
1169        1 │ create domain email_addr as text;
1170          │ ┬─────────────┯━━━━━━━━━────────
1171          │ │             │
1172          │ │             focus range
1173          ╰╴full range
1174        "
1175        );
1176    }
1177
1178    #[test]
1179    fn create_sequence() {
1180        assert_snapshot!(
1181            symbols("create sequence user_id_seq;"),
1182            @r"
1183        info: sequence: public.user_id_seq
1184          ╭▸ 
1185        1 │ create sequence user_id_seq;
1186          │ ┬───────────────┯━━━━━━━━━━
1187          │ │               │
1188          │ │               focus range
1189          ╰╴full range
1190        "
1191        );
1192    }
1193
1194    #[test]
1195    fn create_trigger() {
1196        assert_snapshot!(symbols("
1197create trigger update_timestamp
1198  before update on users
1199  execute function update_modified_column();
1200"), @r"
1201        info: trigger: update_timestamp
1202          ╭▸ 
1203        2 │   create trigger update_timestamp
1204          │   │              ━━━━━━━━━━━━━━━━ focus range
1205          │ ┌─┘
1206          │ │
1207        3 │ │   before update on users
1208        4 │ │   execute function update_modified_column();
1209          ╰╴└───────────────────────────────────────────┘ full range
1210        ");
1211    }
1212
1213    #[test]
1214    fn create_event_trigger() {
1215        assert_snapshot!(
1216            symbols("create event trigger et on ddl_command_start execute function f();"),
1217            @r"
1218        info: event trigger: et
1219          ╭▸ 
1220        1 │ create event trigger et on ddl_command_start execute function f();
1221          │ ┬────────────────────┯━──────────────────────────────────────────
1222          │ │                    │
1223          │ │                    focus range
1224          ╰╴full range
1225        "
1226        );
1227    }
1228
1229    #[test]
1230    fn create_tablespace() {
1231        assert_snapshot!(symbols("
1232create tablespace dbspace location '/data/dbs';
1233"), @r"
1234        info: tablespace: dbspace
1235          ╭▸ 
1236        2 │ create tablespace dbspace location '/data/dbs';
1237          │ ┬─────────────────┯━━━━━━─────────────────────
1238          │ │                 │
1239          │ │                 focus range
1240          ╰╴full range
1241        ");
1242    }
1243
1244    #[test]
1245    fn create_database() {
1246        assert_snapshot!(
1247            symbols("create database mydb;"),
1248            @r"
1249        info: database: mydb
1250          ╭▸ 
1251        1 │ create database mydb;
1252          │ ┬───────────────┯━━━
1253          │ │               │
1254          │ │               focus range
1255          ╰╴full range
1256        "
1257        );
1258    }
1259
1260    #[test]
1261    fn create_server() {
1262        assert_snapshot!(symbols("
1263create server myserver foreign data wrapper postgres_fdw;
1264"), @r"
1265        info: server: myserver
1266          ╭▸ 
1267        2 │ create server myserver foreign data wrapper postgres_fdw;
1268          │ ┬─────────────┯━━━━━━━──────────────────────────────────
1269          │ │             │
1270          │ │             focus range
1271          ╰╴full range
1272        ");
1273    }
1274
1275    #[test]
1276    fn create_extension() {
1277        assert_snapshot!(
1278            symbols("create extension pgcrypto;"),
1279            @r"
1280        info: extension: pgcrypto
1281          ╭▸ 
1282        1 │ create extension pgcrypto;
1283          │ ┬────────────────┯━━━━━━━
1284          │ │                │
1285          │ │                focus range
1286          ╰╴full range
1287        "
1288        );
1289    }
1290
1291    #[test]
1292    fn create_role() {
1293        assert_snapshot!(symbols("
1294create role reader;
1295"), @r"
1296        info: role: reader
1297          ╭▸ 
1298        2 │ create role reader;
1299          │ ┬───────────┯━━━━━
1300          │ │           │
1301          │ │           focus range
1302          ╰╴full range
1303        ");
1304    }
1305
1306    #[test]
1307    fn create_policy() {
1308        assert_snapshot!(symbols("
1309create policy allow_read on t;
1310"), @r"
1311        info: policy: allow_read
1312          ╭▸ 
1313        2 │ create policy allow_read on t;
1314          │ ┬─────────────┯━━━━━━━━━─────
1315          │ │             │
1316          │ │             focus range
1317          ╰╴full range
1318        ");
1319    }
1320
1321    #[test]
1322    fn multiple_symbols() {
1323        assert_snapshot!(symbols("
1324create table users (id int);
1325create table posts (id int);
1326create function get_user(user_id int) returns void as $$ select 1; $$ language sql;
1327"), @r"
1328        info: table: public.users
1329          ╭▸ 
1330        2 │ create table users (id int);
1331          │ ┬────────────┯━━━━─────────
1332          │ │            │
1333          │ │            focus range
1334          │ full range
133513361337        2 │ create table users (id int);
1338          │                     ┯━────
1339          │                     │
1340          │                     full range for `column: id int`
1341          │                     focus range
1342          ╰╴
1343        info: table: public.posts
1344          ╭▸ 
1345        3 │ create table posts (id int);
1346          │ ┬────────────┯━━━━─────────
1347          │ │            │
1348          │ │            focus range
1349          │ full range
135013511352        3 │ create table posts (id int);
1353          │                     ┯━────
1354          │                     │
1355          │                     full range for `column: id int`
1356          ╰╴                    focus range
1357        info: function: public.get_user
1358          ╭▸ 
1359        4 │ create function get_user(user_id int) returns void as $$ select 1; $$ language sql;
1360          │ ┬───────────────┯━━━━━━━──────────────────────────────────────────────────────────
1361          │ │               │
1362          │ │               focus range
1363          ╰╴full range
1364        ");
1365    }
1366
1367    #[test]
1368    fn qualified_names() {
1369        assert_snapshot!(symbols("
1370create table public.users (id int);
1371create function my_schema.hello() returns void as $$ select 1; $$ language sql;
1372"), @r"
1373        info: table: public.users
1374          ╭▸ 
1375        2 │ create table public.users (id int);
1376          │ ┬───────────────────┯━━━━─────────
1377          │ │                   │
1378          │ │                   focus range
1379          │ full range
138013811382        2 │ create table public.users (id int);
1383          │                            ┯━────
1384          │                            │
1385          │                            full range for `column: id int`
1386          │                            focus range
1387          ╰╴
1388        info: function: my_schema.hello
1389          ╭▸ 
1390        3 │ create function my_schema.hello() returns void as $$ select 1; $$ language sql;
1391          │ ┬─────────────────────────┯━━━━───────────────────────────────────────────────
1392          │ │                         │
1393          │ │                         focus range
1394          ╰╴full range
1395        ");
1396    }
1397
1398    #[test]
1399    fn create_type() {
1400        assert_snapshot!(
1401            symbols("create type status as enum ('active', 'inactive');"),
1402            @r"
1403        info: enum: public.status
1404          ╭▸ 
1405        1 │ create type status as enum ('active', 'inactive');
1406          │ ┬───────────┯━━━━━───────────────────────────────
1407          │ │           │
1408          │ │           focus range
1409          │ full range
141014111412        1 │ create type status as enum ('active', 'inactive');
1413          │                             ┯━━━━━━━  ┯━━━━━━━━━
1414          │                             │         │
1415          │                             │         full range for `variant: inactive`
1416          │                             │         focus range
1417          │                             full range for `variant: active`
1418          ╰╴                            focus range
1419        "
1420        );
1421    }
1422
1423    #[test]
1424    fn create_type_composite() {
1425        assert_snapshot!(
1426            symbols("create type person as (name text, age int);"),
1427            @r"
1428        info: type: public.person
1429          ╭▸ 
1430        1 │ create type person as (name text, age int);
1431          │ ┬───────────┯━━━━━────────────────────────
1432          │ │           │
1433          │ │           focus range
1434          │ full range
143514361437        1 │ create type person as (name text, age int);
1438          │                        ┯━━━─────  ┯━━────
1439          │                        │          │
1440          │                        │          full range for `column: age int`
1441          │                        │          focus range
1442          │                        full range for `column: name text`
1443          ╰╴                       focus range
1444        "
1445        );
1446    }
1447
1448    #[test]
1449    fn create_type_composite_multiple_columns() {
1450        assert_snapshot!(
1451            symbols("create type address as (street text, city text, zip varchar(10));"),
1452            @r"
1453        info: type: public.address
1454          ╭▸ 
1455        1 │ create type address as (street text, city text, zip varchar(10));
1456          │ ┬───────────┯━━━━━━─────────────────────────────────────────────
1457          │ │           │
1458          │ │           focus range
1459          │ full range
146014611462        1 │ create type address as (street text, city text, zip varchar(10));
1463          │                         ┯━━━━━─────  ┯━━━─────  ┯━━────────────
1464          │                         │            │          │
1465          │                         │            │          full range for `column: zip varchar(10)`
1466          │                         │            │          focus range
1467          │                         │            full range for `column: city text`
1468          │                         │            focus range
1469          │                         full range for `column: street text`
1470          ╰╴                        focus range
1471        "
1472        );
1473    }
1474
1475    #[test]
1476    fn create_type_with_schema() {
1477        assert_snapshot!(
1478            symbols("create type myschema.status as enum ('active', 'inactive');"),
1479            @r"
1480        info: enum: myschema.status
1481          ╭▸ 
1482        1 │ create type myschema.status as enum ('active', 'inactive');
1483          │ ┬────────────────────┯━━━━━───────────────────────────────
1484          │ │                    │
1485          │ │                    focus range
1486          │ full range
148714881489        1 │ create type myschema.status as enum ('active', 'inactive');
1490          │                                      ┯━━━━━━━  ┯━━━━━━━━━
1491          │                                      │         │
1492          │                                      │         full range for `variant: inactive`
1493          │                                      │         focus range
1494          │                                      full range for `variant: active`
1495          ╰╴                                     focus range
1496        "
1497        );
1498    }
1499
1500    #[test]
1501    fn create_type_enum_multiple_variants() {
1502        assert_snapshot!(
1503            symbols("create type priority as enum ('low', 'medium', 'high', 'urgent');"),
1504            @r"
1505        info: enum: public.priority
1506          ╭▸ 
1507        1 │ create type priority as enum ('low', 'medium', 'high', 'urgent');
1508          │ ┬───────────┯━━━━━━━────────────────────────────────────────────
1509          │ │           │
1510          │ │           focus range
1511          │ full range
151215131514        1 │ create type priority as enum ('low', 'medium', 'high', 'urgent');
1515          │                               ┯━━━━  ┯━━━━━━━  ┯━━━━━  ┯━━━━━━━
1516          │                               │      │         │       │
1517          │                               │      │         │       full range for `variant: urgent`
1518          │                               │      │         │       focus range
1519          │                               │      │         full range for `variant: high`
1520          │                               │      │         focus range
1521          │                               │      full range for `variant: medium`
1522          │                               │      focus range
1523          │                               full range for `variant: low`
1524          ╰╴                              focus range
1525        "
1526        );
1527    }
1528
1529    #[test]
1530    fn declare_cursor() {
1531        assert_snapshot!(symbols("
1532declare c scroll cursor for select * from t;
1533"), @r"
1534        info: cursor: c
1535          ╭▸ 
1536        2 │ declare c scroll cursor for select * from t;
1537          │ ┬───────┯──────────────────────────────────
1538          │ │       │
1539          │ │       focus range
1540          ╰╴full range
1541        ");
1542    }
1543
1544    #[test]
1545    fn prepare_statement() {
1546        assert_snapshot!(symbols("
1547prepare stmt as select 1;
1548"), @r"
1549        info: prepared statement: stmt
1550          ╭▸ 
1551        2 │ prepare stmt as select 1;
1552          │ ┬───────┯━━━────────────
1553          │ │       │
1554          │ │       focus range
1555          ╰╴full range
1556        ");
1557    }
1558
1559    #[test]
1560    fn empty_file() {
1561        symbols_not_found("")
1562    }
1563
1564    #[test]
1565    fn non_create_statements() {
1566        symbols_not_found("select * from users;")
1567    }
1568
1569    #[test]
1570    fn cte_table() {
1571        assert_snapshot!(
1572            symbols("
1573with recent_users as (
1574  select id, email as user_email
1575  from users
1576)
1577select * from recent_users;
1578"),
1579            @r"
1580        info: table: recent_users
1581          ╭▸ 
1582        2 │   with recent_users as (
1583          │        │━━━━━━━━━━━
1584          │        │
1585          │ ┌──────focus range
1586          │ │
1587        3 │ │   select id, email as user_email
1588        4 │ │   from users
1589        5 │ │ )
1590          ╰╴└─┘ full range
1591        "
1592        );
1593    }
1594
1595    #[test]
1596    fn cte_table_with_column_list() {
1597        assert_snapshot!(
1598            symbols("
1599with t(a, b, c) as (
1600  select 1, 2, 3
1601)
1602select * from t;
1603"),
1604            @r"
1605        info: table: t
1606          ╭▸ 
1607        2 │   with t(a, b, c) as (
1608          │        ━ focus range
1609          │ ┌──────┘
1610          │ │
1611        3 │ │   select 1, 2, 3
1612        4 │ │ )
1613          │ └─┘ full range
161416151616        2 │   with t(a, b, c) as (
1617          │          ┯  ┯  ┯
1618          │          │  │  │
1619          │          │  │  full range for `column: c`
1620          │          │  │  focus range
1621          │          │  full range for `column: b`
1622          │          │  focus range
1623          │          full range for `column: a`
1624          ╰╴         focus range
1625        "
1626        );
1627    }
1628
1629    #[test]
1630    fn create_foreign_table() {
1631        assert_snapshot!(symbols("
1632create foreign table films (
1633  code char(5),
1634  title varchar(40)
1635) server film_server;
1636"), @r"
1637        info: table: public.films
1638          ╭▸ 
1639        2 │   create foreign table films (
1640          │   │                    ━━━━━ focus range
1641          │ ┌─┘
1642          │ │
1643        3 │ │   code char(5),
1644        4 │ │   title varchar(40)
1645        5 │ │ ) server film_server;
1646          │ └────────────────────┘ full range
164716481649        3 │     code char(5),
1650          │     ┯━━━────────
1651          │     │
1652          │     full range for `column: code char(5)`
1653          │     focus range
1654        4 │     title varchar(40)
1655          │     ┯━━━━────────────
1656          │     │
1657          │     full range for `column: title varchar(40)`
1658          ╰╴    focus range
1659        ");
1660    }
1661}