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