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::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 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 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 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 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 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}