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    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            // TODO: once we get modfifiers, we'll need to update this
368            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}