Skip to main content

squawk_ide/
semantic_tokens.rs

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/// A semantic token with its position and classification.
145#[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/// Semantic token types supported by the language server.
161#[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    // Determine the root based on the given range.
265    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    // Taken from: https://github.com/rust-lang/rust-analyzer/blob/2efc80078029894eec0699f62ec8d5c1a56af763/crates/ide/src/syntax_highlighting.rs#L267C21-L267C21
282    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        // Element outside of the viewport, no need to highlight
291        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                // Cleanup various operators that the textmate grammar
318                // highlights spuriously. These are for the select cases that
319                // aren't easily handled in the textmate grammar.
320                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    #[must_use]
358    fn semantic_tokens(sql: &str) -> String {
359        let db = Database::default();
360        let file = File::new(&db, sql.to_string().into());
361        let tokens = super::semantic_tokens(&db, file, None);
362
363        let mut result = String::new();
364        for token in tokens {
365            let start: usize = token.range.start().into();
366            let end: usize = token.range.end().into();
367            let token_text = &sql[start..end];
368            // TODO: once we get modfifiers, we'll need to update this
369            let modifiers_text = "";
370            writeln!(
371                result,
372                "{:?} @ {}..{}: {:?}{}",
373                token_text, start, end, token.token_type, modifiers_text
374            )
375            .unwrap();
376        }
377        result
378    }
379
380    #[test]
381    fn create_function_misc_params() {
382        assert_snapshot!(semantic_tokens(
383            "
384create function add(
385  in a int = 1,
386  inout b text default 'x',
387  in out c varchar(10)[],
388  variadic d int[]
389) returns int
390as 'select $1 + $2'
391language sql;
392",
393        ), @r#"
394        "add" @ 17..20: Function
395        "in" @ 24..26: Keyword
396        "a" @ 27..28: Parameter
397        "int" @ 29..32: Type
398        "inout" @ 40..45: Keyword
399        "b" @ 46..47: Parameter
400        "text" @ 48..52: Type
401        "in" @ 68..70: Keyword
402        "out" @ 71..74: Keyword
403        "c" @ 75..76: Parameter
404        "varchar" @ 77..84: Type
405        "variadic" @ 94..102: Keyword
406        "d" @ 103..104: Parameter
407        "int" @ 105..108: Type
408        "int" @ 121..124: Type
409        "#);
410    }
411
412    #[test]
413    fn create_function_param_mode_type() {
414        assert_snapshot!(semantic_tokens(
415            "
416create function f(int8 in int8)
417returns void
418as '' language sql;
419",
420        ), @r#"
421        "f" @ 17..18: Function
422        "int8" @ 19..23: Parameter
423        "in" @ 24..26: Keyword
424        "int8" @ 27..31: Type
425        "void" @ 41..45: Type
426        "#);
427    }
428
429    #[test]
430    fn create_function_percent_type() {
431        assert_snapshot!(semantic_tokens(
432            "
433create function f(a t.c%type) 
434returns t.b%type 
435as '' language plpgsql;
436",
437        ), @r#"
438        "f" @ 17..18: Function
439        "a" @ 19..20: Parameter
440        "#);
441    }
442
443    #[test]
444    fn select_keywords() {
445        assert_snapshot!(semantic_tokens("
446select 1 and, 2 select;
447"), @r#"
448        "and" @ 10..13: Column
449        "select" @ 17..23: Column
450        "#)
451    }
452
453    #[test]
454    fn positional_param() {
455        assert_snapshot!(semantic_tokens("
456select $1, $2;
457"), @r#"
458        "$1" @ 8..10: PositionalParam
459        "$2" @ 12..14: PositionalParam
460        "#)
461    }
462
463    #[test]
464    fn insert_column_list() {
465        assert_snapshot!(semantic_tokens(
466            "
467create table products (product_no bigint, name text, price text);
468insert into products (product_no, name, price) values
469    (1, 'Cheese', 9.99),
470    (2, 'Bread', 1.99),
471    (3, 'Milk', 2.99);
472",
473        ), @r#"
474        "products" @ 14..22: Table
475        "product_no" @ 24..34: Column
476        "bigint" @ 35..41: Type
477        "name" @ 43..47: Column
478        "text" @ 48..52: Type
479        "price" @ 54..59: Column
480        "text" @ 60..64: Type
481        "products" @ 79..87: Table
482        "product_no" @ 89..99: Column
483        "name" @ 101..105: Column
484        "price" @ 107..112: Column
485        "#)
486    }
487
488    #[test]
489    fn from_alias_column_types() {
490        assert_snapshot!(semantic_tokens(
491            "
492select *
493from f as t(a int, b jsonb, c text, x int, ca char(5)[], ia int[][], r text);
494",
495        ), @r#"
496        "t" @ 20..21: Table
497        "a" @ 22..23: Column
498        "int" @ 24..27: Type
499        "b" @ 29..30: Column
500        "jsonb" @ 31..36: Type
501        "c" @ 38..39: Column
502        "text" @ 40..44: Type
503        "x" @ 46..47: Column
504        "int" @ 48..51: Type
505        "ca" @ 53..55: Column
506        "char" @ 56..60: Type
507        "ia" @ 67..69: Column
508        "int" @ 70..73: Type
509        "r" @ 79..80: Column
510        "text" @ 81..85: Type
511        "#);
512    }
513
514    #[test]
515    fn json_table_columns() {
516        assert_snapshot!(semantic_tokens(
517            "
518select *
519from my_films,
520json_table(
521  js,
522  '$.favorites[*]' columns (
523    id for ordinality,
524    kind text path '$.kind'
525  )
526) as jt;
527",
528        ), @r#"
529        "id" @ 76..78: Column
530        "kind" @ 99..103: Column
531        "text" @ 104..108: Type
532        "jt" @ 132..134: Table
533        "#);
534    }
535
536    #[test]
537    fn xml_table_columns() {
538        assert_snapshot!(semantic_tokens(
539            "
540select *
541from xmltable(
542  '/root/item'
543  passing xmlparse(document '<root><item id=\"1\"/></root>')
544  columns
545    row_num for ordinality,
546    item_id integer path '@id'
547);
548",
549        ), @r#"
550        "row_num" @ 113..120: Column
551        "item_id" @ 141..148: Column
552        "integer" @ 149..156: Type
553        "#);
554    }
555
556    #[test]
557    fn cast_types() {
558        assert_snapshot!(semantic_tokens(
559            "
560select '1'::jsonb, '2'::json, cast(1 as integer), cast(1 as int4[][]), cast(1 as varchar(10));
561",
562        ), @r#"
563        "jsonb" @ 13..18: Type
564        "json" @ 25..29: Type
565        "integer" @ 41..48: Type
566        "int4" @ 61..65: Type
567        "varchar" @ 82..89: Type
568        "#);
569    }
570
571    #[test]
572    fn cast_double() {
573        assert_snapshot!(semantic_tokens(
574            "
575select '1'::double precision;
576",
577        ), @r#"
578        "double" @ 13..19: Type
579        "precision" @ 20..29: Type
580        "#);
581    }
582
583    #[test]
584    fn cast_time_and_timestamp_time_zone() {
585        assert_snapshot!(semantic_tokens(
586            "
587select 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);
588",
589        ), @r#"
590        "timestamp" @ 18..27: Type
591        "with" @ 28..32: Type
592        "time" @ 33..37: Type
593        "zone" @ 38..42: Type
594        "timestamp" @ 55..64: Type
595        "without" @ 65..72: Type
596        "time" @ 73..77: Type
597        "zone" @ 78..82: Type
598        "time" @ 95..99: Type
599        "with" @ 100..104: Type
600        "time" @ 105..109: Type
601        "zone" @ 110..114: Type
602        "time" @ 127..131: Type
603        "without" @ 132..139: Type
604        "time" @ 140..144: Type
605        "zone" @ 145..149: Type
606        "#);
607    }
608
609    #[test]
610    fn cast_national_character_varying_type() {
611        assert_snapshot!(semantic_tokens(
612            "
613select 'foo'::national character varying;
614",
615        ), @r#"
616        "national" @ 15..23: Type
617        "character" @ 24..33: Type
618        "varying" @ 34..41: Type
619        "#);
620    }
621
622    #[test]
623    fn create_function_returns_setof_type() {
624        assert_snapshot!(semantic_tokens(
625            "
626create function f() returns setof int
627as 'select 1'
628language sql;
629",
630        ), @r#"
631        "f" @ 17..18: Function
632        "setof" @ 29..34: Type
633        "int" @ 35..38: Type
634        "#);
635    }
636
637    #[test]
638    fn create_table_temporal_primary_key_column_types() {
639        assert_snapshot!(semantic_tokens(
640            "
641-- temporal_primary_key
642CREATE TABLE addresses (
643    id int8 generated BY DEFAULT AS IDENTITY,
644    valid_range tstzrange NOT NULL DEFAULT tstzrange(now(), 'infinity', '[)'),
645    recipient text NOT NULL,
646    PRIMARY KEY (id, valid_range WITHOUT OVERLAPS)
647);
648",
649        ), @r#"
650        "addresses" @ 38..47: Table
651        "id" @ 54..56: Column
652        "int8" @ 57..61: Type
653        "valid_range" @ 100..111: Column
654        "tstzrange" @ 112..121: Type
655        "NOT" @ 122..125: Keyword
656        "tstzrange" @ 139..148: Function
657        "now" @ 149..152: Function
658        "recipient" @ 179..188: Column
659        "text" @ 189..193: Type
660        "NOT" @ 194..197: Keyword
661        "id" @ 221..223: Column
662        "valid_range" @ 225..236: Column
663        "#);
664    }
665
666    #[test]
667    fn like_clause_keyword() {
668        assert_snapshot!(semantic_tokens(
669            "
670create table products(a text);
671create table test (
672  like products
673);
674",
675        ), @r#"
676        "products" @ 14..22: Table
677        "a" @ 23..24: Column
678        "text" @ 25..29: Type
679        "test" @ 45..49: Table
680        "like" @ 54..58: Keyword
681        "products" @ 59..67: Table
682        "#)
683    }
684
685    #[test]
686    fn partition_for_values_in_keywords() {
687        assert_snapshot!(semantic_tokens(
688            "
689create table t(a int);
690create table t_1 partition of t for values in (1);
691",
692        ), @r#"
693        "t" @ 14..15: Table
694        "a" @ 16..17: Column
695        "int" @ 18..21: Type
696        "t_1" @ 37..40: Table
697        "t" @ 54..55: Table
698        "in" @ 67..69: Keyword
699        "#)
700    }
701
702    #[test]
703    fn positional_param_and_cast_type() {
704        assert_snapshot!(semantic_tokens(
705            "
706select $2::jsonb;
707",
708        ), @r#"
709        "$2" @ 8..10: PositionalParam
710        "jsonb" @ 12..17: Type
711        "#);
712    }
713
714    #[test]
715    fn select_target_column() {
716        assert_snapshot!(semantic_tokens(
717            "
718create table t(a int, b text);
719select a, b from t;
720",
721        ), @r#"
722        "t" @ 14..15: Table
723        "a" @ 16..17: Column
724        "int" @ 18..21: Type
725        "b" @ 23..24: Column
726        "text" @ 25..29: Type
727        "a" @ 39..40: Column
728        "b" @ 42..43: Column
729        "t" @ 49..50: Table
730        "#);
731    }
732
733    #[test]
734    fn select_target_qualified_column() {
735        assert_snapshot!(semantic_tokens(
736            "
737create table t(a int);
738select t.a from t;
739",
740        ), @r#"
741        "t" @ 14..15: Table
742        "a" @ 16..17: Column
743        "int" @ 18..21: Type
744        "t" @ 31..32: Table
745        "a" @ 33..34: Column
746        "t" @ 40..41: Table
747        "#);
748    }
749
750    #[test]
751    fn select_target_function_call() {
752        assert_snapshot!(semantic_tokens(
753            "
754create function f() returns int as 'select 1' language sql;
755select f();
756",
757        ), @r#"
758        "f" @ 17..18: Function
759        "int" @ 29..32: Type
760        "f" @ 68..69: Function
761        "#);
762    }
763
764    #[test]
765    fn select_function_arg_and_qualified_column() {
766        assert_snapshot!(semantic_tokens(
767            "
768create table t(a int);
769create function b(t) returns int as 'select 1' language sql;
770select b(t), t.b from t;
771",
772        ), @r#"
773        "t" @ 14..15: Table
774        "a" @ 16..17: Column
775        "int" @ 18..21: Type
776        "b" @ 40..41: Function
777        "t" @ 42..43: Type
778        "int" @ 53..56: Type
779        "b" @ 92..93: Function
780        "t" @ 94..95: Table
781        "t" @ 98..99: Table
782        "b" @ 100..101: Function
783        "t" @ 107..108: Table
784        "#);
785    }
786
787    #[test]
788    fn policy_field_style_function_call() {
789        assert_snapshot!(semantic_tokens(
790            "
791create table t(c int);
792create function x(t) returns int as 'select 1' language sql;
793create policy p on t
794  with check (t.x > 0 and t.c > 0);
795",
796        ), @r#"
797        "t" @ 14..15: Table
798        "c" @ 16..17: Column
799        "int" @ 18..21: Type
800        "x" @ 40..41: Function
801        "t" @ 42..43: Type
802        "int" @ 53..56: Type
803        "t" @ 104..105: Table
804        "t" @ 120..121: Table
805        "x" @ 122..123: Function
806        "t" @ 132..133: Table
807        "c" @ 134..135: Column
808        "#);
809    }
810
811    #[test]
812    fn with_cte_name() {
813        assert_snapshot!(semantic_tokens(
814            "
815with t as (
816  select 1
817)
818select * from t;
819",
820        ), @r#"
821        "t" @ 6..7: Table
822        "t" @ 40..41: Table
823        "#);
824    }
825
826    #[test]
827    fn create_property_graph() {
828        assert_snapshot!(semantic_tokens(
829            "
830create property graph foo
831  vertex tables (bar key (a) no properties);
832",
833        ), @r#"
834        "foo" @ 23..26: PropertyGraph
835        "#);
836    }
837
838    #[test]
839    fn select_target_schema_qualified() {
840        assert_snapshot!(semantic_tokens(
841            "
842create schema s;
843create table s.t(a int);
844select s.t.a from s.t;
845",
846        ), @r#"
847        "s" @ 15..16: Schema
848        "s" @ 31..32: Schema
849        "t" @ 33..34: Table
850        "a" @ 35..36: Column
851        "int" @ 37..40: Type
852        "s" @ 50..51: Schema
853        "t" @ 52..53: Table
854        "a" @ 54..55: Column
855        "s" @ 61..62: Schema
856        "t" @ 63..64: Table
857        "#);
858    }
859}