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