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