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#[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#[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 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 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 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 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 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}