1use sql_dialect_fmt_lexer::tokenize;
8use sql_dialect_fmt_syntax::SyntaxKind;
9use std::ops::Range;
10
11pub const CREATE_PROCEDURE_DOCS: &str =
12 "https://docs.snowflake.com/en/sql-reference/sql/create-procedure";
13pub const CREATE_TASK_DOCS: &str = "https://docs.snowflake.com/en/sql-reference/sql/create-task";
14pub const DATA_TYPES_DOCS: &str = "https://docs.snowflake.com/en/sql-reference/data-types";
15
16const ROUTINE_OPTION_STOPS: &[&str] = &[
17 "AS",
18 "ARTIFACT_REPOSITORY",
19 "CALLED",
20 "COMMENT",
21 "COPY",
22 "EXECUTE",
23 "EXTERNAL_ACCESS_INTEGRATIONS",
24 "HANDLER",
25 "IMMUTABLE",
26 "IMPORTS",
27 "LANGUAGE",
28 "MEMOIZABLE",
29 "NULL",
30 "PACKAGES",
31 "RETURNS",
32 "RUNTIME_VERSION",
33 "SECRETS",
34 "SECURE",
35 "STRICT",
36 "TARGET_PATH",
37 "VOLATILE",
38];
39
40#[derive(Clone, Debug, PartialEq, Eq)]
41pub struct Hover {
42 pub kind: HoverKind,
43 pub title: String,
44 pub body: String,
45 pub range: Range<usize>,
46 pub docs_url: Option<&'static str>,
47}
48
49#[derive(Clone, Copy, Debug, PartialEq, Eq)]
50pub enum HoverKind {
51 Keyword,
52 Type,
53 Procedure,
54 Task,
55 Language,
56 Property,
57}
58
59#[derive(Clone, Debug)]
60struct SpannedToken<'a> {
61 kind: SyntaxKind,
62 text: &'a str,
63 range: Range<usize>,
64}
65
66#[derive(Clone, Copy)]
67struct StaticHover {
68 kind: HoverKind,
69 title: &'static str,
70 body: &'static str,
71 docs_url: Option<&'static str>,
72}
73
74pub fn hover_at(source: &str, offset: usize) -> Option<Hover> {
79 let tokens = spanned_tokens(source);
80 let index = token_at(&tokens, offset)?;
81 let token = tokens[index].clone();
82
83 if let Some(hover) = procedure_symbol_hover(source, &tokens, index) {
84 return Some(hover);
85 }
86 if let Some(hover) = task_symbol_hover(source, &tokens, index) {
87 return Some(hover);
88 }
89 if let Some(hover) = type_hover(&token) {
90 return Some(hover);
91 }
92 if let Some(hover) = language_hover(&token) {
93 return Some(hover);
94 }
95 if let Some(hover) = property_hover(&token) {
96 return Some(hover);
97 }
98 keyword_hover(&token)
99}
100
101fn spanned_tokens(source: &str) -> Vec<SpannedToken<'_>> {
102 let mut offset = 0usize;
103 tokenize(source)
104 .tokens
105 .into_iter()
106 .filter_map(|token| {
107 let start = offset;
108 offset += token.text.len();
109 (!token.kind.is_trivia()).then_some(SpannedToken {
110 kind: token.kind,
111 text: token.text,
112 range: start..offset,
113 })
114 })
115 .collect()
116}
117
118fn token_at(tokens: &[SpannedToken<'_>], offset: usize) -> Option<usize> {
119 tokens
120 .iter()
121 .position(|token| token.range.start <= offset && offset < token.range.end)
122 .or_else(|| {
123 offset.checked_sub(1).and_then(|previous| {
124 tokens
125 .iter()
126 .position(|token| token.range.start <= previous && previous < token.range.end)
127 })
128 })
129}
130
131fn procedure_symbol_hover(
132 source: &str,
133 tokens: &[SpannedToken<'_>],
134 index: usize,
135) -> Option<Hover> {
136 let object = object_declaration(tokens, index, "PROCEDURE")?;
137 if word(tokens.get(object.keyword + 1)?, "SCOPED") {
138 return None;
139 }
140 let name_range = procedure_name_range(tokens, object.keyword, object.end)?;
141 if !name_range.contains(&index) {
142 return None;
143 }
144
145 let name = compact_token_text(tokens, name_range.clone());
146 let args = procedure_args(source, tokens, name_range.end, object.end)
147 .unwrap_or_else(|| String::from(""));
148 let returns = clause_after_keyword(
149 source,
150 tokens,
151 object.keyword,
152 object.end,
153 "RETURNS",
154 &[
155 "LANGUAGE",
156 "RUNTIME_VERSION",
157 "PACKAGES",
158 "IMPORTS",
159 "HANDLER",
160 "AS",
161 "COMMENT",
162 "EXECUTE",
163 ],
164 );
165 let language = value_after_keyword(source, tokens, object.keyword, object.end, "LANGUAGE");
166 let handler =
167 routine_option_after_keyword(source, tokens, object.keyword, object.end, "HANDLER");
168 let runtime = routine_option_after_keyword(
169 source,
170 tokens,
171 object.keyword,
172 object.end,
173 "RUNTIME_VERSION",
174 );
175 let packages =
176 routine_option_after_keyword(source, tokens, object.keyword, object.end, "PACKAGES");
177 let imports =
178 routine_option_after_keyword(source, tokens, object.keyword, object.end, "IMPORTS");
179 let target_path =
180 routine_option_after_keyword(source, tokens, object.keyword, object.end, "TARGET_PATH");
181
182 let mut lines = vec![format!("Stored procedure `{name}`.")];
183 if !args.is_empty() {
184 lines.push(format!("Arguments: `{args}`."));
185 }
186 if let Some(returns) = returns {
187 lines.push(format!("Returns: `{returns}`."));
188 }
189 if let Some(language) = language {
190 lines.push(format!("Language: `{language}`."));
191 }
192 if let Some(handler) = handler {
193 lines.push(format!("Handler: `{handler}`."));
194 }
195 if let Some(runtime) = runtime {
196 lines.push(format!("Runtime: `{runtime}`."));
197 }
198 if let Some(packages) = packages {
199 lines.push(format!("Packages: `{packages}`."));
200 }
201 if let Some(imports) = imports {
202 lines.push(format!("Imports: `{imports}`."));
203 }
204 if let Some(target_path) = target_path {
205 lines.push(format!("Target path: `{target_path}`."));
206 }
207 lines.push(String::from(
208 "Snowflake resolves stored procedures by name plus argument types.",
209 ));
210 lines.push(String::from(
211 "External-language procedures usually pair LANGUAGE with HANDLER, PACKAGES, IMPORTS, and RUNTIME_VERSION.",
212 ));
213
214 Some(Hover {
215 kind: HoverKind::Procedure,
216 title: format!("Stored procedure `{name}`"),
217 body: lines.join("\n"),
218 range: combined_range(tokens, name_range),
219 docs_url: Some(CREATE_PROCEDURE_DOCS),
220 })
221}
222
223fn task_symbol_hover(source: &str, tokens: &[SpannedToken<'_>], index: usize) -> Option<Hover> {
224 let object = object_declaration(tokens, index, "TASK")?;
225 let name_range = task_name_range(tokens, object.keyword, object.end)?;
226 if !name_range.contains(&index) {
227 return None;
228 }
229
230 let name = compact_token_text(tokens, name_range.clone());
231 let warehouse = value_after_keyword(source, tokens, object.keyword, object.end, "WAREHOUSE")
232 .or_else(|| {
233 value_after_keyword(
234 source,
235 tokens,
236 object.keyword,
237 object.end,
238 "USER_TASK_MANAGED_INITIAL_WAREHOUSE_SIZE",
239 )
240 });
241 let schedule = value_after_keyword(source, tokens, object.keyword, object.end, "SCHEDULE");
242 let after = clause_after_keyword(
243 source,
244 tokens,
245 object.keyword,
246 object.end,
247 "AFTER",
248 &["WHEN", "AS", "EXECUTE", "COMMENT", "FINALIZE"],
249 );
250 let when = clause_after_keyword(source, tokens, object.keyword, object.end, "WHEN", &["AS"]);
251
252 let mut lines = vec![format!("Task `{name}`.")];
253 if let Some(warehouse) = warehouse {
254 lines.push(format!("Compute: `{warehouse}`."));
255 }
256 if let Some(schedule) = schedule {
257 lines.push(format!("Schedule: `{schedule}`."));
258 }
259 if let Some(after) = after {
260 lines.push(format!("Predecessors: `{after}`."));
261 }
262 if let Some(when) = when {
263 lines.push(format!("Condition: `{when}`."));
264 }
265 lines.push(String::from(
266 "Tasks run SQL on a schedule or after predecessor tasks; newly created tasks start suspended.",
267 ));
268
269 Some(Hover {
270 kind: HoverKind::Task,
271 title: format!("Task `{name}`"),
272 body: lines.join("\n"),
273 range: combined_range(tokens, name_range),
274 docs_url: Some(CREATE_TASK_DOCS),
275 })
276}
277
278#[derive(Clone, Copy)]
279struct ObjectDeclaration {
280 keyword: usize,
281 end: usize,
282}
283
284fn object_declaration(
285 tokens: &[SpannedToken<'_>],
286 index: usize,
287 object_keyword: &str,
288) -> Option<ObjectDeclaration> {
289 let (start, end) = statement_bounds(tokens, index);
290 for keyword in (start..=index.min(end.saturating_sub(1))).rev() {
291 if !word(&tokens[keyword], object_keyword) {
292 continue;
293 }
294 if tokens[start..keyword]
295 .iter()
296 .any(|token| word(token, "CREATE"))
297 {
298 return Some(ObjectDeclaration { keyword, end });
299 }
300 }
301 None
302}
303
304fn statement_bounds(tokens: &[SpannedToken<'_>], index: usize) -> (usize, usize) {
305 let start = tokens[..index]
306 .iter()
307 .rposition(|token| token.kind == SyntaxKind::SEMICOLON)
308 .map_or(0, |idx| idx + 1);
309 let end = tokens[index..]
310 .iter()
311 .position(|token| token.kind == SyntaxKind::SEMICOLON)
312 .map_or(tokens.len(), |relative| index + relative);
313 (start, end)
314}
315
316fn procedure_name_range(
317 tokens: &[SpannedToken<'_>],
318 procedure_keyword: usize,
319 end: usize,
320) -> Option<Range<usize>> {
321 let start = procedure_keyword + 1;
322 let mut cursor = start;
323 while cursor < end && tokens[cursor].kind != SyntaxKind::L_PAREN {
324 if !is_name_part(&tokens[cursor]) {
325 return None;
326 }
327 cursor += 1;
328 }
329 (start < cursor).then_some(start..cursor)
330}
331
332fn task_name_range(
333 tokens: &[SpannedToken<'_>],
334 task_keyword: usize,
335 end: usize,
336) -> Option<Range<usize>> {
337 let mut start = task_keyword + 1;
338 if word_seq(tokens, start, &["IF", "NOT", "EXISTS"]) {
339 start += 3;
340 }
341
342 let mut cursor = start;
343 while cursor < end && is_name_part(&tokens[cursor]) && !is_clause_boundary(&tokens[cursor]) {
344 cursor += 1;
345 }
346 (start < cursor).then_some(start..cursor)
347}
348
349fn procedure_args(
350 source: &str,
351 tokens: &[SpannedToken<'_>],
352 start: usize,
353 end: usize,
354) -> Option<String> {
355 let open = (start..end).find(|&idx| tokens[idx].kind == SyntaxKind::L_PAREN)?;
356 let close = matching_paren(tokens, open, end)?;
357 let inside = token_slice(source, tokens, open + 1, close);
358 Some(inside)
359}
360
361fn matching_paren(tokens: &[SpannedToken<'_>], open: usize, end: usize) -> Option<usize> {
362 let mut depth = 0usize;
363 for (idx, token) in tokens.iter().enumerate().take(end).skip(open) {
364 match token.kind {
365 SyntaxKind::L_PAREN => depth += 1,
366 SyntaxKind::R_PAREN => {
367 depth = depth.saturating_sub(1);
368 if depth == 0 {
369 return Some(idx);
370 }
371 }
372 _ => {}
373 }
374 }
375 None
376}
377
378fn value_after_keyword(
379 source: &str,
380 tokens: &[SpannedToken<'_>],
381 start: usize,
382 end: usize,
383 keyword: &str,
384) -> Option<String> {
385 let idx = (start..end).find(|&idx| word(&tokens[idx], keyword))?;
386 let mut value_start = idx + 1;
387 if value_start < end && tokens[value_start].kind == SyntaxKind::EQ {
388 value_start += 1;
389 }
390 let mut value_end = value_start;
391 while value_end < end
392 && !is_clause_boundary(&tokens[value_end])
393 && tokens[value_end].kind != SyntaxKind::COMMA
394 {
395 value_end += 1;
396 }
397 let value = token_slice(source, tokens, value_start, value_end);
398 (!value.is_empty()).then_some(value)
399}
400
401fn routine_option_after_keyword(
402 source: &str,
403 tokens: &[SpannedToken<'_>],
404 start: usize,
405 end: usize,
406 keyword: &str,
407) -> Option<String> {
408 clause_after_keyword(source, tokens, start, end, keyword, ROUTINE_OPTION_STOPS)
409 .map(|value| value.strip_prefix("= ").unwrap_or(&value).to_string())
410}
411
412fn clause_after_keyword(
413 source: &str,
414 tokens: &[SpannedToken<'_>],
415 start: usize,
416 end: usize,
417 keyword: &str,
418 stops: &[&str],
419) -> Option<String> {
420 let idx = (start..end).find(|&idx| word(&tokens[idx], keyword))?;
421 let value_start = idx + 1;
422 let mut depth = 0usize;
423 let mut value_end = value_start;
424 while value_end < end {
425 let token = &tokens[value_end];
426 match token.kind {
427 SyntaxKind::L_PAREN | SyntaxKind::L_BRACKET => depth += 1,
428 SyntaxKind::R_PAREN | SyntaxKind::R_BRACKET => depth = depth.saturating_sub(1),
429 _ => {}
430 }
431 if depth == 0 && stops.iter().any(|stop| word(token, stop)) {
432 break;
433 }
434 value_end += 1;
435 }
436 let value = token_slice(source, tokens, value_start, value_end);
437 (!value.is_empty()).then_some(value)
438}
439
440fn token_slice(source: &str, tokens: &[SpannedToken<'_>], start: usize, end: usize) -> String {
441 if start >= end || start >= tokens.len() {
442 return String::new();
443 }
444 let end = end.min(tokens.len());
445 compact_text(&source[tokens[start].range.start..tokens[end - 1].range.end])
446}
447
448fn compact_text(text: &str) -> String {
449 text.split_whitespace().collect::<Vec<_>>().join(" ")
450}
451
452fn compact_token_text(tokens: &[SpannedToken<'_>], range: Range<usize>) -> String {
453 tokens[range]
454 .iter()
455 .map(|token| token.text)
456 .collect::<Vec<_>>()
457 .join("")
458}
459
460fn combined_range(tokens: &[SpannedToken<'_>], range: Range<usize>) -> Range<usize> {
461 tokens[range.start].range.start..tokens[range.end - 1].range.end
462}
463
464fn is_name_part(token: &SpannedToken<'_>) -> bool {
465 matches!(
466 token.kind,
467 SyntaxKind::IDENT | SyntaxKind::QUOTED_IDENT | SyntaxKind::DOT
468 )
469}
470
471fn is_clause_boundary(token: &SpannedToken<'_>) -> bool {
472 matches!(
473 token.kind,
474 SyntaxKind::SEMICOLON | SyntaxKind::R_PAREN | SyntaxKind::R_BRACKET
475 ) || [
476 "AS",
477 "AFTER",
478 "ARTIFACT_REPOSITORY",
479 "CALLED",
480 "COMMENT",
481 "CONFIG",
482 "COPY",
483 "ERROR_INTEGRATION",
484 "EXECUTE",
485 "EXTERNAL_ACCESS_INTEGRATIONS",
486 "FINALIZE",
487 "HANDLER",
488 "IMMUTABLE",
489 "IMPORTS",
490 "LANGUAGE",
491 "MEMOIZABLE",
492 "NULL",
493 "OVERLAP_POLICY",
494 "PACKAGES",
495 "RETURNS",
496 "RUNTIME_VERSION",
497 "SCHEDULE",
498 "SECRETS",
499 "SECURE",
500 "STRICT",
501 "SUCCESS_INTEGRATION",
502 "TASK_AUTO_RETRY_ATTEMPTS",
503 "TARGET_PATH",
504 "USER_TASK_MANAGED_INITIAL_WAREHOUSE_SIZE",
505 "USER_TASK_TIMEOUT_MS",
506 "VOLATILE",
507 "WAREHOUSE",
508 "WHEN",
509 ]
510 .iter()
511 .any(|boundary| word(token, boundary))
512}
513
514fn word(token: &SpannedToken<'_>, expected: &str) -> bool {
515 (token.kind == SyntaxKind::IDENT || token.kind.is_keyword())
516 && token.text.eq_ignore_ascii_case(expected)
517}
518
519fn word_seq(tokens: &[SpannedToken<'_>], start: usize, words: &[&str]) -> bool {
520 words.iter().enumerate().all(|(offset, expected)| {
521 tokens
522 .get(start + offset)
523 .is_some_and(|t| word(t, expected))
524 })
525}
526
527fn type_hover(token: &SpannedToken<'_>) -> Option<Hover> {
528 let (canonical, body) = type_info(token.text)?;
529 Some(Hover {
530 kind: HoverKind::Type,
531 title: format!("Snowflake type `{canonical}`"),
532 body: body.to_string(),
533 range: token.range.clone(),
534 docs_url: Some(DATA_TYPES_DOCS),
535 })
536}
537
538fn type_info(text: &str) -> Option<(&'static str, &'static str)> {
539 let item = [
540 (
541 &["NUMBER", "DECIMAL", "NUMERIC", "INT", "INTEGER", "BIGINT"][..],
542 "NUMBER",
543 "Exact fixed-point numeric type. Use precision and scale for stable financial or identifier-like values.",
544 ),
545 (
546 &["FLOAT", "DOUBLE", "REAL"][..],
547 "FLOAT",
548 "Approximate floating-point numeric type. Prefer NUMBER when exact decimal behavior matters.",
549 ),
550 (
551 &["VARCHAR", "STRING", "TEXT", "CHAR"][..],
552 "VARCHAR",
553 "Variable-length character data. STRING and TEXT are Snowflake aliases for VARCHAR.",
554 ),
555 (&["BOOLEAN"][..], "BOOLEAN", "TRUE/FALSE logical value."),
556 (
557 &["VARIANT"][..],
558 "VARIANT",
559 "Semi-structured value that can hold JSON-like OBJECT, ARRAY, scalar, or SQL NULL distinctions.",
560 ),
561 (
562 &["OBJECT"][..],
563 "OBJECT",
564 "Semi-structured key/value object. Common with JSON ingestion and colon path access.",
565 ),
566 (
567 &["ARRAY"][..],
568 "ARRAY",
569 "Semi-structured ordered collection. Use bracket indexing and FLATTEN/TABLE for traversal.",
570 ),
571 (
572 &["MAP"][..],
573 "MAP",
574 "Structured key/value collection type. Useful when key and value types are part of the schema.",
575 ),
576 (
577 &["VECTOR"][..],
578 "VECTOR",
579 "Vector type for embeddings and similarity workloads; keep element type and dimension explicit.",
580 ),
581 (
582 &["DATE"][..],
583 "DATE",
584 "Calendar date without time of day.",
585 ),
586 (
587 &["TIME"][..],
588 "TIME",
589 "Time of day without date.",
590 ),
591 (
592 &["TIMESTAMP", "TIMESTAMP_NTZ", "TIMESTAMP_LTZ", "TIMESTAMP_TZ"][..],
593 "TIMESTAMP",
594 "Timestamp family. NTZ has no time zone, LTZ uses the session time zone, and TZ stores an explicit offset.",
595 ),
596 (&["BINARY"][..], "BINARY", "Variable-length binary data."),
597 (
598 &["GEOGRAPHY", "GEOMETRY"][..],
599 "GEOSPATIAL",
600 "Geospatial data for spherical geography or planar geometry operations.",
601 ),
602 ]
603 .into_iter()
604 .find(|(aliases, _, _)| aliases.iter().any(|alias| text.eq_ignore_ascii_case(alias)))?;
605
606 Some((item.1, item.2))
607}
608
609fn language_hover(token: &SpannedToken<'_>) -> Option<Hover> {
610 let template = if token.text.eq_ignore_ascii_case("JAVASCRIPT") {
611 StaticHover {
612 kind: HoverKind::Language,
613 title: "LANGUAGE JAVASCRIPT",
614 body: "JavaScript stored procedure body. The handler is the body itself; SQL argument names can need careful case handling inside JavaScript.",
615 docs_url: Some(CREATE_PROCEDURE_DOCS),
616 }
617 } else if token.text.eq_ignore_ascii_case("PYTHON") {
618 StaticHover {
619 kind: HoverKind::Language,
620 title: "LANGUAGE PYTHON",
621 body: "Snowpark Python stored procedure. HANDLER names the Python function; RUNTIME_VERSION pins Python; PACKAGES, IMPORTS, EXTERNAL_ACCESS_INTEGRATIONS, and SECRETS describe the runtime environment.",
622 docs_url: Some(CREATE_PROCEDURE_DOCS),
623 }
624 } else if token.text.eq_ignore_ascii_case("JAVA") {
625 StaticHover {
626 kind: HoverKind::Language,
627 title: "LANGUAGE JAVA",
628 body: "Java stored procedure. HANDLER names a class and method; RUNTIME_VERSION, PACKAGES, IMPORTS, and TARGET_PATH describe the JVM runtime and staged artifact.",
629 docs_url: Some(CREATE_PROCEDURE_DOCS),
630 }
631 } else if token.text.eq_ignore_ascii_case("SCALA") {
632 StaticHover {
633 kind: HoverKind::Language,
634 title: "LANGUAGE SCALA",
635 body: "Snowpark Scala stored procedure. HANDLER names the Scala entry point; RUNTIME_VERSION, PACKAGES, IMPORTS, and TARGET_PATH describe the JVM runtime and staged artifact.",
636 docs_url: Some(CREATE_PROCEDURE_DOCS),
637 }
638 } else if token.text.eq_ignore_ascii_case("SQL") {
639 StaticHover {
640 kind: HoverKind::Language,
641 title: "LANGUAGE SQL",
642 body: "Snowflake Scripting stored procedure body. Use SQL procedural constructs such as DECLARE, BEGIN, RETURN, loops, and EXCEPTION handlers.",
643 docs_url: Some(CREATE_PROCEDURE_DOCS),
644 }
645 } else {
646 return None;
647 };
648 Some(from_static(token.range.clone(), template))
649}
650
651fn property_hover(token: &SpannedToken<'_>) -> Option<Hover> {
652 let properties = [
653 StaticHover {
654 kind: HoverKind::Property,
655 title: "RETURNS",
656 body: "Declares a procedure result type. It can be a scalar type or RETURNS TABLE(...), depending on the procedure language and support level.",
657 docs_url: Some(CREATE_PROCEDURE_DOCS),
658 },
659 StaticHover {
660 kind: HoverKind::Property,
661 title: "LANGUAGE",
662 body: "Selects the stored procedure handler language: SQL, JAVASCRIPT, PYTHON, JAVA, or SCALA.",
663 docs_url: Some(CREATE_PROCEDURE_DOCS),
664 },
665 StaticHover {
666 kind: HoverKind::Property,
667 title: "HANDLER",
668 body: "Names the external-language entry point, such as a Python function or JVM method.",
669 docs_url: Some(CREATE_PROCEDURE_DOCS),
670 },
671 StaticHover {
672 kind: HoverKind::Property,
673 title: "PACKAGES",
674 body: "Declares runtime packages available to Snowpark Java, Scala, or Python procedure handlers.",
675 docs_url: Some(CREATE_PROCEDURE_DOCS),
676 },
677 StaticHover {
678 kind: HoverKind::Property,
679 title: "IMPORTS",
680 body: "Adds staged files that the stored procedure handler can read at runtime.",
681 docs_url: Some(CREATE_PROCEDURE_DOCS),
682 },
683 StaticHover {
684 kind: HoverKind::Property,
685 title: "RUNTIME_VERSION",
686 body: "Pins the language runtime version for Java, Python, or Scala procedure handlers.",
687 docs_url: Some(CREATE_PROCEDURE_DOCS),
688 },
689 StaticHover {
690 kind: HoverKind::Property,
691 title: "TARGET_PATH",
692 body: "Stage path for the compiled Java or Scala procedure artifact. Use it when Snowflake should write the generated handler artifact to a stage.",
693 docs_url: Some(CREATE_PROCEDURE_DOCS),
694 },
695 StaticHover {
696 kind: HoverKind::Property,
697 title: "ARTIFACT_REPOSITORY",
698 body: "Selects an artifact repository for resolving supported external-language dependencies.",
699 docs_url: Some(CREATE_PROCEDURE_DOCS),
700 },
701 StaticHover {
702 kind: HoverKind::Property,
703 title: "EXTERNAL_ACCESS_INTEGRATIONS",
704 body: "Allows an external-language procedure to use one or more external access integrations for outbound network access.",
705 docs_url: Some(CREATE_PROCEDURE_DOCS),
706 },
707 StaticHover {
708 kind: HoverKind::Property,
709 title: "SECRETS",
710 body: "Maps Snowflake secrets to names the external-language handler can use at runtime.",
711 docs_url: Some(CREATE_PROCEDURE_DOCS),
712 },
713 StaticHover {
714 kind: HoverKind::Property,
715 title: "STRICT",
716 body: "Alias for RETURNS NULL ON NULL INPUT: Snowflake does not call the handler when any input argument is NULL.",
717 docs_url: Some(CREATE_PROCEDURE_DOCS),
718 },
719 StaticHover {
720 kind: HoverKind::Property,
721 title: "CALLED",
722 body: "Part of CALLED ON NULL INPUT: Snowflake calls the procedure even when an input argument is NULL.",
723 docs_url: Some(CREATE_PROCEDURE_DOCS),
724 },
725 StaticHover {
726 kind: HoverKind::Property,
727 title: "IMMUTABLE",
728 body: "Declares that the result depends only on inputs and not on database state or side effects.",
729 docs_url: Some(CREATE_PROCEDURE_DOCS),
730 },
731 StaticHover {
732 kind: HoverKind::Property,
733 title: "VOLATILE",
734 body: "Declares that the procedure can depend on state or side effects; this is the conservative behavior for procedural code.",
735 docs_url: Some(CREATE_PROCEDURE_DOCS),
736 },
737 StaticHover {
738 kind: HoverKind::Property,
739 title: "EXECUTE",
740 body: "Controls whether a procedure or task runs with owner, caller, restricted caller, or user execution context.",
741 docs_url: Some(CREATE_PROCEDURE_DOCS),
742 },
743 StaticHover {
744 kind: HoverKind::Property,
745 title: "WAREHOUSE",
746 body: "Virtual warehouse that supplies compute for a task run. Omit it only when using serverless task sizing.",
747 docs_url: Some(CREATE_TASK_DOCS),
748 },
749 StaticHover {
750 kind: HoverKind::Property,
751 title: "USER_TASK_MANAGED_INITIAL_WAREHOUSE_SIZE",
752 body: "Initial serverless task size. Snowflake can manage task compute after enough run history exists.",
753 docs_url: Some(CREATE_TASK_DOCS),
754 },
755 StaticHover {
756 kind: HoverKind::Property,
757 title: "SCHEDULE",
758 body: "Task schedule. Snowflake accepts interval strings or USING CRON expressions with a time zone.",
759 docs_url: Some(CREATE_TASK_DOCS),
760 },
761 StaticHover {
762 kind: HoverKind::Property,
763 title: "AFTER",
764 body: "Creates a task graph dependency; this task runs after one or more predecessor tasks.",
765 docs_url: Some(CREATE_TASK_DOCS),
766 },
767 StaticHover {
768 kind: HoverKind::Property,
769 title: "WHEN",
770 body: "Boolean task condition evaluated before the task body runs. Common with stream checks such as SYSTEM$STREAM_HAS_DATA.",
771 docs_url: Some(CREATE_TASK_DOCS),
772 },
773 StaticHover {
774 kind: HoverKind::Property,
775 title: "FINALIZE",
776 body: "Marks a task as a finalizer for a task graph root.",
777 docs_url: Some(CREATE_TASK_DOCS),
778 },
779 StaticHover {
780 kind: HoverKind::Property,
781 title: "TASK_AUTO_RETRY_ATTEMPTS",
782 body: "Configures automatic retries for failed task graph runs.",
783 docs_url: Some(CREATE_TASK_DOCS),
784 },
785 ];
786
787 properties
788 .into_iter()
789 .find(|property| token.text.eq_ignore_ascii_case(property.title))
790 .map(|template| from_static(token.range.clone(), template))
791}
792
793fn keyword_hover(token: &SpannedToken<'_>) -> Option<Hover> {
794 let template = if token.text.eq_ignore_ascii_case("PROCEDURE") {
795 StaticHover {
796 kind: HoverKind::Procedure,
797 title: "Stored procedure",
798 body: "Schema object that can be called with CALL. Procedures support SQL, JavaScript, Python, Java, and Scala handlers.",
799 docs_url: Some(CREATE_PROCEDURE_DOCS),
800 }
801 } else if token.text.eq_ignore_ascii_case("TASK") {
802 StaticHover {
803 kind: HoverKind::Task,
804 title: "Task",
805 body: "Schema object that executes SQL on a schedule or as part of a task graph. Newly created tasks start suspended.",
806 docs_url: Some(CREATE_TASK_DOCS),
807 }
808 } else {
809 return None;
810 };
811 Some(from_static(token.range.clone(), template))
812}
813
814fn from_static(range: Range<usize>, template: StaticHover) -> Hover {
815 Hover {
816 kind: template.kind,
817 title: template.title.to_string(),
818 body: template.body.to_string(),
819 range,
820 docs_url: template.docs_url,
821 }
822}