1use rowan::{NodeOrToken, TextRange};
2use salsa::Database as Db;
3use squawk_syntax::{
4 SyntaxElement, SyntaxKind,
5 ast::{self, AstNode},
6};
7
8use crate::db::{File, parse};
9use crate::file::InFile;
10use crate::goto_definition::goto_definition;
11use crate::location::LocationKind;
12
13fn highlight_param_mode(out: &mut SemanticTokenBuilder, mode: ast::ParamMode) {
14 match mode {
15 ast::ParamMode::ParamIn(param_in) => {
16 if let Some(token) = param_in.in_token() {
17 out.push_keyword(token.into());
18 }
19 }
20 ast::ParamMode::ParamInOut(param_in_out) => {
21 if let Some(token) = param_in_out.in_token() {
22 out.push_keyword(token.into());
23 }
24 if let Some(token) = param_in_out.inout_token() {
25 out.push_keyword(token.into());
26 }
27 if let Some(token) = param_in_out.out_token() {
28 out.push_keyword(token.into());
29 }
30 }
31 ast::ParamMode::ParamOut(param_out) => {
32 if let Some(token) = param_out.out_token() {
33 out.push_keyword(token.into());
34 }
35 }
36 ast::ParamMode::ParamVariadic(param_variadic) => {
37 if let Some(token) = param_variadic.variadic_token() {
38 out.push_keyword(token.into());
39 }
40 }
41 }
42}
43
44fn highlight_type(out: &mut SemanticTokenBuilder, ty: ast::Type) {
45 match ty {
46 ast::Type::ArrayType(_) => (),
47 ast::Type::BitType(bit_type) => {
48 if let Some(token) = bit_type.setof_token() {
49 out.push_type(token.into());
50 }
51 if let Some(token) = bit_type.bit_token() {
52 out.push_type(token.into());
53 }
54 if let Some(token) = bit_type.varying_token() {
55 out.push_type(token.into());
56 }
57 }
58 ast::Type::CharType(char_type) => {
59 if let Some(token) = char_type.setof_token() {
60 out.push_type(token.into());
61 }
62 if let Some(token) = char_type.national_token() {
63 out.push_type(token.into());
64 }
65
66 if let Some(token) = char_type
67 .varchar_token()
68 .or_else(|| char_type.nchar_token())
69 .or_else(|| char_type.character_token())
70 .or_else(|| char_type.char_token())
71 {
72 out.push_type(token.into());
73 }
74 if let Some(token) = char_type.varying_token() {
75 out.push_type(token.into());
76 }
77 }
78 ast::Type::DoubleType(double_type) => {
79 if let Some(token) = double_type.setof_token() {
80 out.push_type(token.into());
81 }
82 if let Some(token) = double_type.double_token() {
83 out.push_type(token.into());
84 }
85 if let Some(token) = double_type.precision_token() {
86 out.push_type(token.into());
87 }
88 }
89 ast::Type::ExprType(_) => (),
90 ast::Type::IntervalType(interval_type) => {
91 if let Some(token) = interval_type.setof_token() {
92 out.push_type(token.into());
93 }
94 if let Some(token) = interval_type.interval_token() {
95 out.push_type(token.into());
96 }
97 }
98 ast::Type::PathType(path_type) => {
99 if let Some(token) = path_type.setof_token() {
100 out.push_type(token.into());
101 }
102 }
103 ast::Type::PercentType(_) => (),
104 ast::Type::TimeType(time_type) => {
105 if let Some(token) = time_type.setof_token() {
106 out.push_type(token.into());
107 }
108 if let Some(token) = time_type
109 .timestamp_token()
110 .or_else(|| time_type.time_token())
111 {
112 out.push_type(token.into());
113 }
114
115 if let Some(timezone) = time_type.timezone() {
116 match timezone {
117 ast::Timezone::WithTimezone(with_timezone) => {
118 if let Some(token) = with_timezone.with_token() {
119 out.push_type(token.into());
120 }
121 if let Some(token) = with_timezone.time_token() {
122 out.push_type(token.into());
123 }
124 if let Some(token) = with_timezone.zone_token() {
125 out.push_type(token.into());
126 }
127 }
128 ast::Timezone::WithoutTimezone(without_timezone) => {
129 if let Some(token) = without_timezone.without_token() {
130 out.push_type(token.into());
131 }
132 if let Some(token) = without_timezone.time_token() {
133 out.push_type(token.into());
134 }
135 if let Some(token) = without_timezone.zone_token() {
136 out.push_type(token.into());
137 }
138 }
139 }
140 }
141 }
142 }
143}
144
145#[derive(Debug, Clone, PartialEq, Eq)]
147pub struct SemanticToken {
148 pub range: TextRange,
149 pub token_type: SemanticTokenType,
150 pub modifiers: Option<SemanticTokenModifier>,
151}
152
153#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
154#[repr(u8)]
155pub enum SemanticTokenModifier {
156 Definition = 0,
157 Readonly,
158 Documentation,
159}
160
161#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
163pub enum SemanticTokenType {
164 Keyword,
165 String,
166 Bool,
167 Number,
168 Function,
169 Operator,
170 Punctuation,
171 Name,
172 NameRef,
173 Comment,
174 Column,
175 Type,
176 Parameter,
177 PositionalParam,
178 PropertyGraph,
179 Table,
180 Schema,
181}
182
183impl TryFrom<LocationKind> for SemanticTokenType {
184 type Error = LocationKind;
185
186 fn try_from(kind: LocationKind) -> Result<Self, Self::Error> {
187 match kind {
188 LocationKind::Aggregate | LocationKind::Function | LocationKind::Procedure => {
189 Ok(SemanticTokenType::Function)
190 }
191 LocationKind::Column => Ok(SemanticTokenType::Column),
192 LocationKind::NamedArgParameter => Ok(SemanticTokenType::Parameter),
193 LocationKind::Schema => Ok(SemanticTokenType::Schema),
194 LocationKind::PropertyGraph => Ok(SemanticTokenType::PropertyGraph),
195 LocationKind::Sequence | LocationKind::Table | LocationKind::View => {
196 Ok(SemanticTokenType::Table)
197 }
198 LocationKind::Type => Ok(SemanticTokenType::Type),
199 LocationKind::CaseExpr
200 | LocationKind::Channel
201 | LocationKind::CommitBegin
202 | LocationKind::CommitEnd
203 | LocationKind::Cursor
204 | LocationKind::Database
205 | LocationKind::EventTrigger
206 | LocationKind::Extension
207 | LocationKind::Index
208 | LocationKind::Policy
209 | LocationKind::PreparedStatement
210 | LocationKind::Role
211 | LocationKind::Server
212 | LocationKind::Tablespace
213 | LocationKind::Trigger
214 | LocationKind::Window => Err(kind),
215 }
216 }
217}
218
219fn token_type_for_node<T: AstNode>(db: &dyn Db, node: InFile<&T>) -> Option<SemanticTokenType> {
220 let offset = node.value.syntax().text_range().start();
221 let location = goto_definition(db, InFile::new(node.file_id, offset))
222 .into_iter()
223 .next()?;
224
225 SemanticTokenType::try_from(location.kind).ok()
226}
227
228#[derive(Default)]
229struct SemanticTokenBuilder {
230 tokens: Vec<SemanticToken>,
231}
232
233impl SemanticTokenBuilder {
234 fn build(mut self) -> Vec<SemanticToken> {
235 self.tokens
236 .sort_by_key(|token| (token.range.start(), token.range.end()));
237 self.tokens
238 }
239
240 fn push_keyword(&mut self, syntax_element: SyntaxElement) {
241 self.push_token(syntax_element, SemanticTokenType::Keyword);
242 }
243
244 fn push_type(&mut self, syntax_element: SyntaxElement) {
245 self.push_token(syntax_element, SemanticTokenType::Type);
246 }
247
248 fn push_token(&mut self, syntax_element: SyntaxElement, token_type: SemanticTokenType) {
249 self.tokens.push(SemanticToken {
250 range: syntax_element.text_range(),
251 token_type,
252 modifiers: None,
253 });
254 }
255}
256
257#[salsa::tracked]
258pub fn semantic_tokens(
259 db: &dyn Db,
260 file: File,
261 range_to_highlight: Option<TextRange>,
262) -> Vec<SemanticToken> {
263 let parse = parse(db, file);
264 let tree = parse.tree();
265 let root = tree.syntax();
266
267 let (root, range_to_highlight) = {
269 let source_file = root;
270 match range_to_highlight {
271 Some(range) => {
272 let node = match source_file.covering_element(range) {
273 NodeOrToken::Node(it) => it,
274 NodeOrToken::Token(it) => it.parent().unwrap_or_else(|| source_file.clone()),
275 };
276 (node, range)
277 }
278 None => (source_file.clone(), source_file.text_range()),
279 }
280 };
281
282 let mut out = SemanticTokenBuilder::default();
283
284 let preorder = root.preorder_with_tokens();
286 for event in preorder {
287 use rowan::WalkEvent::{Enter, Leave};
288
289 let range = match &event {
290 Enter(it) | Leave(it) => it.text_range(),
291 };
292
293 if range_to_highlight.intersect(range).is_none() {
295 continue;
296 }
297
298 match event {
299 Enter(NodeOrToken::Node(node)) => {
300 if let Some(name) = ast::Name::cast(node.clone())
301 && let Some(token_type) = token_type_for_node(db, InFile::new(file, &name))
302 {
303 out.push_token(name.syntax().clone().into(), token_type);
304 }
305
306 if let Some(name_ref) = ast::NameRef::cast(node.clone())
307 && let Some(token_type) = token_type_for_node(db, InFile::new(file, &name_ref))
308 {
309 out.push_token(name_ref.syntax().clone().into(), token_type);
310 }
311
312 if let Some(ty) = ast::Type::cast(node.clone()) {
313 highlight_type(&mut out, ty);
314 }
315
316 if let Some(mode) = ast::ParamMode::cast(node.clone()) {
317 highlight_param_mode(&mut out, mode);
318 }
319
320 if let Some(like_clause) = ast::LikeClause::cast(node.clone())
324 && let Some(token) = like_clause.like_token()
325 {
326 out.push_keyword(token.into());
327 }
328 if let Some(not_null_constraint) = ast::NotNullConstraint::cast(node.clone())
329 && let Some(token) = not_null_constraint.not_token()
330 {
331 out.push_keyword(token.into());
332 }
333 if let Some(partition_for_values_in) = ast::PartitionForValuesIn::cast(node.clone())
334 && let Some(token) = partition_for_values_in.in_token()
335 {
336 out.push_keyword(token.into());
337 }
338 }
339 Enter(NodeOrToken::Token(token)) => {
340 if token.kind() == SyntaxKind::WHITESPACE {
341 continue;
342 }
343 if token.kind() == SyntaxKind::POSITIONAL_PARAM {
344 out.push_token(token.into(), SemanticTokenType::PositionalParam);
345 }
346 }
347 Leave(_) => {}
348 }
349 }
350
351 out.build()
352}
353
354#[cfg(test)]
355mod test {
356 use crate::db::{Database, File};
357 use insta::assert_snapshot;
358 use std::fmt::Write;
359
360 #[must_use]
361 fn semantic_tokens(sql: &str) -> String {
362 let db = Database::default();
363 let file = File::new(&db, sql.to_string().into());
364 let tokens = super::semantic_tokens(&db, file, None);
365
366 let mut result = String::new();
367 for token in tokens {
368 let start: usize = token.range.start().into();
369 let end: usize = token.range.end().into();
370 let token_text = &sql[start..end];
371 let modifiers_text = "";
373 writeln!(
374 result,
375 "{:?} @ {}..{}: {:?}{}",
376 token_text, start, end, token.token_type, modifiers_text
377 )
378 .unwrap();
379 }
380 result
381 }
382
383 #[test]
384 fn create_function_misc_params() {
385 assert_snapshot!(semantic_tokens(
386 "
387create function add(
388 in a int = 1,
389 inout b text default 'x',
390 in out c varchar(10)[],
391 variadic d int[]
392) returns int
393as 'select $1 + $2'
394language sql;
395",
396 ), @r#"
397 "add" @ 17..20: Function
398 "in" @ 24..26: Keyword
399 "a" @ 27..28: Parameter
400 "int" @ 29..32: Type
401 "inout" @ 40..45: Keyword
402 "b" @ 46..47: Parameter
403 "text" @ 48..52: Type
404 "in" @ 68..70: Keyword
405 "out" @ 71..74: Keyword
406 "c" @ 75..76: Parameter
407 "varchar" @ 77..84: Type
408 "variadic" @ 94..102: Keyword
409 "d" @ 103..104: Parameter
410 "int" @ 105..108: Type
411 "int" @ 121..124: Type
412 "#);
413 }
414
415 #[test]
416 fn create_function_param_mode_type() {
417 assert_snapshot!(semantic_tokens(
418 "
419create function f(int8 in int8)
420returns void
421as '' language sql;
422",
423 ), @r#"
424 "f" @ 17..18: Function
425 "int8" @ 19..23: Parameter
426 "in" @ 24..26: Keyword
427 "int8" @ 27..31: Type
428 "void" @ 41..45: Type
429 "#);
430 }
431
432 #[test]
433 fn create_function_percent_type() {
434 assert_snapshot!(semantic_tokens(
435 "
436create function f(a t.c%type)
437returns t.b%type
438as '' language plpgsql;
439",
440 ), @r#"
441 "f" @ 17..18: Function
442 "a" @ 19..20: Parameter
443 "#);
444 }
445
446 #[test]
447 fn select_keywords() {
448 assert_snapshot!(semantic_tokens("
449select 1 and, 2 select;
450"), @r#"
451 "and" @ 10..13: Column
452 "select" @ 17..23: Column
453 "#)
454 }
455
456 #[test]
457 fn positional_param() {
458 assert_snapshot!(semantic_tokens("
459select $1, $2;
460"), @r#"
461 "$1" @ 8..10: PositionalParam
462 "$2" @ 12..14: PositionalParam
463 "#)
464 }
465
466 #[test]
467 fn insert_column_list() {
468 assert_snapshot!(semantic_tokens(
469 "
470create table products (product_no bigint, name text, price text);
471insert into products (product_no, name, price) values
472 (1, 'Cheese', 9.99),
473 (2, 'Bread', 1.99),
474 (3, 'Milk', 2.99);
475",
476 ), @r#"
477 "products" @ 14..22: Table
478 "product_no" @ 24..34: Column
479 "bigint" @ 35..41: Type
480 "name" @ 43..47: Column
481 "text" @ 48..52: Type
482 "price" @ 54..59: Column
483 "text" @ 60..64: Type
484 "products" @ 79..87: Table
485 "product_no" @ 89..99: Column
486 "name" @ 101..105: Column
487 "price" @ 107..112: Column
488 "#)
489 }
490
491 #[test]
492 fn from_alias_column_types() {
493 assert_snapshot!(semantic_tokens(
494 "
495select *
496from f as t(a int, b jsonb, c text, x int, ca char(5)[], ia int[][], r text);
497",
498 ), @r#"
499 "t" @ 20..21: Table
500 "a" @ 22..23: Column
501 "int" @ 24..27: Type
502 "b" @ 29..30: Column
503 "jsonb" @ 31..36: Type
504 "c" @ 38..39: Column
505 "text" @ 40..44: Type
506 "x" @ 46..47: Column
507 "int" @ 48..51: Type
508 "ca" @ 53..55: Column
509 "char" @ 56..60: Type
510 "ia" @ 67..69: Column
511 "int" @ 70..73: Type
512 "r" @ 79..80: Column
513 "text" @ 81..85: Type
514 "#);
515 }
516
517 #[test]
518 fn json_table_columns() {
519 assert_snapshot!(semantic_tokens(
520 "
521select *
522from my_films,
523json_table(
524 js,
525 '$.favorites[*]' columns (
526 id for ordinality,
527 kind text path '$.kind'
528 )
529) as jt;
530",
531 ), @r#"
532 "id" @ 76..78: Column
533 "kind" @ 99..103: Column
534 "text" @ 104..108: Type
535 "jt" @ 132..134: Table
536 "#);
537 }
538
539 #[test]
540 fn xml_table_columns() {
541 assert_snapshot!(semantic_tokens(
542 "
543select *
544from xmltable(
545 '/root/item'
546 passing xmlparse(document '<root><item id=\"1\"/></root>')
547 columns
548 row_num for ordinality,
549 item_id integer path '@id'
550);
551",
552 ), @r#"
553 "row_num" @ 113..120: Column
554 "item_id" @ 141..148: Column
555 "integer" @ 149..156: Type
556 "#);
557 }
558
559 #[test]
560 fn cast_types() {
561 assert_snapshot!(semantic_tokens(
562 "
563select '1'::jsonb, '2'::json, cast(1 as integer), cast(1 as int4[][]), cast(1 as varchar(10));
564",
565 ), @r#"
566 "jsonb" @ 13..18: Type
567 "json" @ 25..29: Type
568 "integer" @ 41..48: Type
569 "int4" @ 61..65: Type
570 "varchar" @ 82..89: Type
571 "#);
572 }
573
574 #[test]
575 fn cast_double() {
576 assert_snapshot!(semantic_tokens(
577 "
578select '1'::double precision;
579",
580 ), @r#"
581 "double" @ 13..19: Type
582 "precision" @ 20..29: Type
583 "#);
584 }
585
586 #[test]
587 fn cast_time_and_timestamp_time_zone() {
588 assert_snapshot!(semantic_tokens(
589 "
590select cast(1 as timestamp with time zone), cast(1 as timestamp without time zone), cast(1 as time with time zone), cast(1 as time without time zone);
591",
592 ), @r#"
593 "timestamp" @ 18..27: Type
594 "with" @ 28..32: Type
595 "time" @ 33..37: Type
596 "zone" @ 38..42: Type
597 "timestamp" @ 55..64: Type
598 "without" @ 65..72: Type
599 "time" @ 73..77: Type
600 "zone" @ 78..82: Type
601 "time" @ 95..99: Type
602 "with" @ 100..104: Type
603 "time" @ 105..109: Type
604 "zone" @ 110..114: Type
605 "time" @ 127..131: Type
606 "without" @ 132..139: Type
607 "time" @ 140..144: Type
608 "zone" @ 145..149: Type
609 "#);
610 }
611
612 #[test]
613 fn cast_national_character_varying_type() {
614 assert_snapshot!(semantic_tokens(
615 "
616select 'foo'::national character varying;
617",
618 ), @r#"
619 "national" @ 15..23: Type
620 "character" @ 24..33: Type
621 "varying" @ 34..41: Type
622 "#);
623 }
624
625 #[test]
626 fn create_function_returns_setof_type() {
627 assert_snapshot!(semantic_tokens(
628 "
629create function f() returns setof int
630as 'select 1'
631language sql;
632",
633 ), @r#"
634 "f" @ 17..18: Function
635 "setof" @ 29..34: Type
636 "int" @ 35..38: Type
637 "#);
638 }
639
640 #[test]
641 fn create_table_temporal_primary_key_column_types() {
642 assert_snapshot!(semantic_tokens(
643 "
644-- temporal_primary_key
645CREATE TABLE addresses (
646 id int8 generated BY DEFAULT AS IDENTITY,
647 valid_range tstzrange NOT NULL DEFAULT tstzrange(now(), 'infinity', '[)'),
648 recipient text NOT NULL,
649 PRIMARY KEY (id, valid_range WITHOUT OVERLAPS)
650);
651",
652 ), @r#"
653 "addresses" @ 38..47: Table
654 "id" @ 54..56: Column
655 "int8" @ 57..61: Type
656 "valid_range" @ 100..111: Column
657 "tstzrange" @ 112..121: Type
658 "NOT" @ 122..125: Keyword
659 "tstzrange" @ 139..148: Function
660 "now" @ 149..152: Function
661 "recipient" @ 179..188: Column
662 "text" @ 189..193: Type
663 "NOT" @ 194..197: Keyword
664 "id" @ 221..223: Column
665 "valid_range" @ 225..236: Column
666 "#);
667 }
668
669 #[test]
670 fn like_clause_keyword() {
671 assert_snapshot!(semantic_tokens(
672 "
673create table products(a text);
674create table test (
675 like products
676);
677",
678 ), @r#"
679 "products" @ 14..22: Table
680 "a" @ 23..24: Column
681 "text" @ 25..29: Type
682 "test" @ 45..49: Table
683 "like" @ 54..58: Keyword
684 "products" @ 59..67: Table
685 "#)
686 }
687
688 #[test]
689 fn partition_for_values_in_keywords() {
690 assert_snapshot!(semantic_tokens(
691 "
692create table t(a int);
693create table t_1 partition of t for values in (1);
694",
695 ), @r#"
696 "t" @ 14..15: Table
697 "a" @ 16..17: Column
698 "int" @ 18..21: Type
699 "t_1" @ 37..40: Table
700 "t" @ 54..55: Table
701 "in" @ 67..69: Keyword
702 "#)
703 }
704
705 #[test]
706 fn positional_param_and_cast_type() {
707 assert_snapshot!(semantic_tokens(
708 "
709select $2::jsonb;
710",
711 ), @r#"
712 "$2" @ 8..10: PositionalParam
713 "jsonb" @ 12..17: Type
714 "#);
715 }
716
717 #[test]
718 fn select_target_column() {
719 assert_snapshot!(semantic_tokens(
720 "
721create table t(a int, b text);
722select a, b from t;
723",
724 ), @r#"
725 "t" @ 14..15: Table
726 "a" @ 16..17: Column
727 "int" @ 18..21: Type
728 "b" @ 23..24: Column
729 "text" @ 25..29: Type
730 "a" @ 39..40: Column
731 "b" @ 42..43: Column
732 "t" @ 49..50: Table
733 "#);
734 }
735
736 #[test]
737 fn select_target_qualified_column() {
738 assert_snapshot!(semantic_tokens(
739 "
740create table t(a int);
741select t.a from t;
742",
743 ), @r#"
744 "t" @ 14..15: Table
745 "a" @ 16..17: Column
746 "int" @ 18..21: Type
747 "t" @ 31..32: Table
748 "a" @ 33..34: Column
749 "t" @ 40..41: Table
750 "#);
751 }
752
753 #[test]
754 fn select_target_function_call() {
755 assert_snapshot!(semantic_tokens(
756 "
757create function f() returns int as 'select 1' language sql;
758select f();
759",
760 ), @r#"
761 "f" @ 17..18: Function
762 "int" @ 29..32: Type
763 "f" @ 68..69: Function
764 "#);
765 }
766
767 #[test]
768 fn select_function_arg_and_qualified_column() {
769 assert_snapshot!(semantic_tokens(
770 "
771create table t(a int);
772create function b(t) returns int as 'select 1' language sql;
773select b(t), t.b from t;
774",
775 ), @r#"
776 "t" @ 14..15: Table
777 "a" @ 16..17: Column
778 "int" @ 18..21: Type
779 "b" @ 40..41: Function
780 "t" @ 42..43: Type
781 "int" @ 53..56: Type
782 "b" @ 92..93: Function
783 "t" @ 94..95: Table
784 "t" @ 98..99: Table
785 "b" @ 100..101: Function
786 "t" @ 107..108: Table
787 "#);
788 }
789
790 #[test]
791 fn policy_field_style_function_call() {
792 assert_snapshot!(semantic_tokens(
793 "
794create table t(c int);
795create function x(t) returns int as 'select 1' language sql;
796create policy p on t
797 with check (t.x > 0 and t.c > 0);
798",
799 ), @r#"
800 "t" @ 14..15: Table
801 "c" @ 16..17: Column
802 "int" @ 18..21: Type
803 "x" @ 40..41: Function
804 "t" @ 42..43: Type
805 "int" @ 53..56: Type
806 "t" @ 104..105: Table
807 "t" @ 120..121: Table
808 "x" @ 122..123: Function
809 "t" @ 132..133: Table
810 "c" @ 134..135: Column
811 "#);
812 }
813
814 #[test]
815 fn with_cte_name() {
816 assert_snapshot!(semantic_tokens(
817 "
818with t as (
819 select 1
820)
821select * from t;
822",
823 ), @r#"
824 "t" @ 6..7: Table
825 "t" @ 40..41: Table
826 "#);
827 }
828
829 #[test]
830 fn create_property_graph() {
831 assert_snapshot!(semantic_tokens(
832 "
833create property graph foo
834 vertex tables (bar key (a) no properties);
835",
836 ), @r#"
837 "foo" @ 23..26: PropertyGraph
838 "#);
839 }
840
841 #[test]
842 fn select_target_schema_qualified() {
843 assert_snapshot!(semantic_tokens(
844 "
845create schema s;
846create table s.t(a int);
847select s.t.a from s.t;
848",
849 ), @r#"
850 "s" @ 15..16: Schema
851 "s" @ 31..32: Schema
852 "t" @ 33..34: Table
853 "a" @ 35..36: Column
854 "int" @ 37..40: Type
855 "s" @ 50..51: Schema
856 "t" @ 52..53: Table
857 "a" @ 54..55: Column
858 "s" @ 61..62: Schema
859 "t" @ 63..64: Table
860 "#);
861 }
862}