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