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::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/// A semantic token with its position and classification.
146#[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/// Semantic token types supported by the language server.
162#[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    // Determine the root based on the given range.
268    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    // Taken from: https://github.com/rust-lang/rust-analyzer/blob/2efc80078029894eec0699f62ec8d5c1a56af763/crates/ide/src/syntax_highlighting.rs#L267C21-L267C21
285    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        // Element outside of the viewport, no need to highlight
294        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                // Cleanup various operators that the textmate grammar
321                // highlights spuriously. These are for the select cases that
322                // aren't easily handled in the textmate grammar.
323                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            // TODO: once we get modfifiers, we'll need to update this
372            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}