Skip to main content

squawk_ide/
completion.rs

1use rowan::TextSize;
2use salsa::Database as Db;
3use squawk_syntax::ast::{self, AstNode};
4use squawk_syntax::{SyntaxKind, SyntaxNode, SyntaxToken};
5
6use crate::ast_nav;
7use crate::binder;
8use crate::collect;
9use crate::db::{File, bind, parse};
10use crate::file::InFile;
11use crate::name::{self, Name, Schema};
12use crate::resolve;
13use crate::symbols::SymbolKind;
14use crate::tokens::is_string_or_comment;
15
16const COMPLETION_MARKER: &str = "squawkCompletionMarker";
17
18pub fn completion(db: &dyn Db, position: InFile<TextSize>) -> Vec<CompletionItem> {
19    let file = position.file_id;
20    let offset = position.value;
21    let parse = parse(db, file);
22    let source_file = parse.tree();
23
24    let marker_file = file_with_completion_marker(&source_file, offset);
25    let Some(token) = token_at_offset(&marker_file, offset) else {
26        // empty file
27        return default_completions();
28    };
29    // We don't support completions inside comments since we don't have doc
30    // comments a la JSDoc.
31    // And we don't support enums aka string literal types yet so we bail out
32    // early for strings as well
33    if is_string_or_comment(token.kind()) {
34        return vec![];
35    }
36
37    match completion_context(&token) {
38        CompletionContext::TableOnly => table_completions(&marker_file, &token),
39        CompletionContext::Default => default_completions(),
40        CompletionContext::SelectClause(select_clause) => {
41            select_completions(db, file, select_clause, &token)
42        }
43        CompletionContext::SelectClauses(select) => select_clauses_completions(&select),
44        CompletionContext::SelectExpr(select) => select_expr_completions(db, file, &select, &token),
45        CompletionContext::LimitClause => limit_completions(db, file, &token),
46        CompletionContext::OffsetClause => offset_completions(db, file, &token),
47        CompletionContext::DeleteClauses(delete) => {
48            delete_clauses_completions(&marker_file, &delete, &token)
49        }
50        CompletionContext::DeleteExpr(delete) => {
51            delete_expr_completions(db, file, &marker_file, &delete, &token)
52        }
53    }
54}
55
56fn select_completions(
57    db: &dyn Db,
58    file: File,
59    select_clause: ast::SelectClause,
60    token: &SyntaxToken,
61) -> Vec<CompletionItem> {
62    let binder = bind(db, file);
63    let mut completions = vec![];
64    let schema = schema_qualifier_at_token(token);
65    let position = token.text_range().start();
66
67    completions.extend(function_completions(db, file, schema.as_ref(), position));
68
69    let tables = binder.all_symbols_by_kind(SymbolKind::Table, schema.as_ref());
70    completions.extend(tables.into_iter().map(|name| CompletionItem {
71        label: name.to_string(),
72        kind: CompletionItemKind::Table,
73        detail: None,
74        insert_text: None,
75        insert_text_format: None,
76        trigger_completion_after_insert: false,
77        sort_text: None,
78    }));
79
80    if schema.is_none() {
81        completions.extend(schema_completions(&binder));
82    }
83
84    if let Some(parent) = select_clause.syntax().parent()
85        && let Some(select) = ast::Select::cast(parent)
86    {
87        if let Some(from_clause) = select.from_clause() {
88            completions.push(CompletionItem {
89                label: "*".to_string(),
90                kind: CompletionItemKind::Operator,
91                detail: None,
92                insert_text: None,
93                insert_text_format: None,
94                trigger_completion_after_insert: false,
95                sort_text: None,
96            });
97            completions.extend(column_completions_from_clause(db, file, &from_clause));
98        } else if schema.is_none() {
99            completions.extend(select_clauses_completions(&select));
100        }
101    }
102
103    completions
104}
105
106fn select_clauses_completions(select: &ast::Select) -> Vec<CompletionItem> {
107    let mut completions = vec![];
108
109    if select.from_clause().is_none() {
110        completions.push(CompletionItem {
111            label: "from".to_owned(),
112            kind: CompletionItemKind::Snippet,
113            detail: None,
114            insert_text: Some("from $0".to_owned()),
115            insert_text_format: Some(CompletionInsertTextFormat::Snippet),
116            trigger_completion_after_insert: true,
117            sort_text: None,
118        });
119    }
120
121    if select.where_clause().is_none() {
122        completions.push(CompletionItem {
123            label: "where".to_owned(),
124            kind: CompletionItemKind::Snippet,
125            detail: None,
126            insert_text: Some("where $0".to_owned()),
127            insert_text_format: Some(CompletionInsertTextFormat::Snippet),
128            trigger_completion_after_insert: true,
129            sort_text: None,
130        });
131    }
132
133    if select.group_by_clause().is_none() {
134        completions.push(CompletionItem {
135            label: "group by".to_owned(),
136            kind: CompletionItemKind::Snippet,
137            detail: None,
138            insert_text: Some("group by $0".to_owned()),
139            insert_text_format: Some(CompletionInsertTextFormat::Snippet),
140            trigger_completion_after_insert: true,
141            sort_text: None,
142        });
143    }
144
145    if select.having_clause().is_none() {
146        completions.push(CompletionItem {
147            label: "having".to_owned(),
148            kind: CompletionItemKind::Snippet,
149            detail: None,
150            insert_text: Some("having $0".to_owned()),
151            insert_text_format: Some(CompletionInsertTextFormat::Snippet),
152            trigger_completion_after_insert: true,
153            sort_text: None,
154        });
155    }
156
157    if select.order_by_clause().is_none() {
158        completions.push(CompletionItem {
159            label: "order by".to_owned(),
160            kind: CompletionItemKind::Snippet,
161            detail: None,
162            insert_text: Some("order by $0".to_owned()),
163            insert_text_format: Some(CompletionInsertTextFormat::Snippet),
164            trigger_completion_after_insert: true,
165            sort_text: None,
166        });
167    }
168
169    if select.limit_clause().is_none() {
170        completions.push(CompletionItem {
171            label: "limit".to_owned(),
172            kind: CompletionItemKind::Snippet,
173            detail: None,
174            insert_text: Some("limit $0".to_owned()),
175            insert_text_format: Some(CompletionInsertTextFormat::Snippet),
176            trigger_completion_after_insert: true,
177            sort_text: None,
178        });
179    }
180
181    if select.offset_clause().is_none() {
182        completions.push(CompletionItem {
183            label: "offset".to_owned(),
184            kind: CompletionItemKind::Snippet,
185            detail: None,
186            insert_text: Some("offset $0".to_owned()),
187            insert_text_format: Some(CompletionInsertTextFormat::Snippet),
188            trigger_completion_after_insert: true,
189            sort_text: None,
190        });
191    }
192
193    if select.fetch_clause().is_none() {
194        completions.push(CompletionItem {
195            label: "fetch".to_owned(),
196            kind: CompletionItemKind::Snippet,
197            detail: None,
198            insert_text: Some(
199                "fetch ${1|first,next|} $2 ${3|row,rows|} ${4|only,with ties|}".to_owned(),
200            ),
201            insert_text_format: Some(CompletionInsertTextFormat::Snippet),
202            trigger_completion_after_insert: true,
203            sort_text: None,
204        });
205    }
206
207    if select.locking_clauses().next().is_none() {
208        completions.push(CompletionItem {
209            label: "for".to_owned(),
210            kind: CompletionItemKind::Snippet,
211            detail: None,
212            insert_text: Some("for ${1|update,no key update,share,key share|} $2".to_owned()),
213            insert_text_format: Some(CompletionInsertTextFormat::Snippet),
214            trigger_completion_after_insert: true,
215            sort_text: None,
216        });
217    }
218
219    if select.window_clause().is_none() {
220        completions.push(CompletionItem {
221            label: "window".to_owned(),
222            kind: CompletionItemKind::Snippet,
223            detail: None,
224            insert_text: Some("window $1 as ($0)".to_owned()),
225            insert_text_format: Some(CompletionInsertTextFormat::Snippet),
226            trigger_completion_after_insert: true,
227            sort_text: None,
228        });
229    }
230
231    completions.push(CompletionItem {
232        label: "union".to_owned(),
233        kind: CompletionItemKind::Snippet,
234        detail: None,
235        insert_text: Some("union $0".to_owned()),
236        insert_text_format: Some(CompletionInsertTextFormat::Snippet),
237        trigger_completion_after_insert: true,
238        sort_text: None,
239    });
240    completions.push(CompletionItem {
241        label: "intersect".to_owned(),
242        kind: CompletionItemKind::Snippet,
243        detail: None,
244        insert_text: Some("intersect $0".to_owned()),
245        insert_text_format: Some(CompletionInsertTextFormat::Snippet),
246        trigger_completion_after_insert: true,
247        sort_text: None,
248    });
249    completions.push(CompletionItem {
250        label: "except".to_owned(),
251        kind: CompletionItemKind::Snippet,
252        detail: None,
253        insert_text: Some("except $0".to_owned()),
254        insert_text_format: Some(CompletionInsertTextFormat::Snippet),
255        trigger_completion_after_insert: true,
256        sort_text: None,
257    });
258
259    completions
260}
261
262fn limit_completions(db: &dyn Db, file: File, token: &SyntaxToken) -> Vec<CompletionItem> {
263    let schema = schema_qualifier_at_token(token);
264    let position = token.text_range().start();
265
266    let mut completions = vec![CompletionItem {
267        label: "all".to_owned(),
268        kind: CompletionItemKind::Keyword,
269        detail: None,
270        insert_text: None,
271        insert_text_format: None,
272        trigger_completion_after_insert: false,
273        sort_text: None,
274    }];
275
276    completions.extend(function_completions(db, file, schema.as_ref(), position));
277    completions
278}
279
280fn offset_completions(db: &dyn Db, file: File, token: &SyntaxToken) -> Vec<CompletionItem> {
281    let schema = schema_qualifier_at_token(token);
282    let position = token.text_range().start();
283
284    function_completions(db, file, schema.as_ref(), position)
285}
286
287fn select_expr_completions(
288    db: &dyn Db,
289    file: File,
290    select: &ast::Select,
291    token: &SyntaxToken,
292) -> Vec<CompletionItem> {
293    let mut completions = vec![];
294    let schema = schema_qualifier_at_token(token);
295    let position = token.text_range().start();
296
297    completions.extend(function_completions(db, file, schema.as_ref(), position));
298
299    if let Some(from_clause) = select.from_clause() {
300        for from_item in from_clause.from_items() {
301            if let Some(table_name) = table_name_from_from_item(&from_item) {
302                completions.push(CompletionItem {
303                    label: table_name.to_string(),
304                    kind: CompletionItemKind::Table,
305                    detail: None,
306                    insert_text: None,
307                    insert_text_format: None,
308                    trigger_completion_after_insert: false,
309                    sort_text: None,
310                });
311            }
312        }
313
314        completions.extend(column_completions_from_clause(db, file, &from_clause));
315    }
316
317    completions
318}
319
320fn function_completions(
321    db: &dyn Db,
322    file: File,
323    schema: Option<&Schema>,
324    position: TextSize,
325) -> Vec<CompletionItem> {
326    let binder = bind(db, file);
327    binder
328        .all_symbols_by_kind(SymbolKind::Function, schema)
329        .into_iter()
330        .map(|name| CompletionItem {
331            label: format!("{name}()"),
332            kind: CompletionItemKind::Function,
333            detail: function_detail(db, file, name, schema, position),
334            insert_text: None,
335            insert_text_format: None,
336            trigger_completion_after_insert: false,
337            sort_text: None,
338        })
339        .collect()
340}
341
342fn column_completions_from_clause(
343    db: &dyn Db,
344    file: File,
345    from_clause: &ast::FromClause,
346) -> Vec<CompletionItem> {
347    let mut completions = vec![];
348    let syntax_root = from_clause.syntax().ancestors().last().unwrap();
349    for table_ptr in resolve::table_ptrs_from_clause(db, InFile::new(file, from_clause)) {
350        let table_node = table_ptr.to_node(&syntax_root);
351        match ast_nav::parent_source(&table_node) {
352            Some(ast_nav::ParentSouce::CreateTable(create_table)) => {
353                let columns = collect::table_columns(db, file, &create_table);
354                completions.extend(columns.into_iter().map(|(name, ty)| CompletionItem {
355                    label: name.to_string(),
356                    kind: CompletionItemKind::Column,
357                    detail: ty.map(|t| t.to_string()),
358                    insert_text: None,
359                    insert_text_format: None,
360                    trigger_completion_after_insert: false,
361                    sort_text: None,
362                }));
363            }
364            Some(ast_nav::ParentSouce::WithTable(with_table)) => {
365                let columns = collect::with_table_columns_with_types(db, file, with_table);
366                completions.extend(columns.into_iter().map(|(name, ty)| CompletionItem {
367                    label: name.to_string(),
368                    kind: CompletionItemKind::Column,
369                    detail: ty.map(|t| t.to_string()),
370                    insert_text: None,
371                    insert_text_format: None,
372                    trigger_completion_after_insert: false,
373                    sort_text: None,
374                }));
375            }
376            Some(ast_nav::ParentSouce::CreateTableAs(create_table_as)) => {
377                let columns =
378                    collect::create_table_as_columns_with_types(db, file, &create_table_as);
379                completions.extend(columns.into_iter().map(|(name, ty)| CompletionItem {
380                    label: name.to_string(),
381                    kind: CompletionItemKind::Column,
382                    detail: ty.map(|t| t.to_string()),
383                    insert_text: None,
384                    insert_text_format: None,
385                    trigger_completion_after_insert: false,
386                    sort_text: None,
387                }));
388            }
389            Some(ast_nav::ParentSouce::CreateView(create_view)) => {
390                let columns = collect::view_like_columns_with_types(db, file, &create_view);
391                completions.extend(columns.into_iter().map(|(name, ty)| CompletionItem {
392                    label: name.to_string(),
393                    kind: CompletionItemKind::Column,
394                    detail: ty.map(|t| t.to_string()),
395                    insert_text: None,
396                    insert_text_format: None,
397                    trigger_completion_after_insert: false,
398                    sort_text: None,
399                }));
400            }
401            Some(ast_nav::ParentSouce::Alias(alias)) => {
402                let alias_columns: Vec<Name> = alias
403                    .column_list()
404                    .into_iter()
405                    .flat_map(|column_list| column_list.columns())
406                    .filter_map(|column| column.name().map(|name| Name::from_node(&name)))
407                    .collect();
408
409                let base_columns = alias_base_columns_with_types(db, file, &syntax_root, &alias);
410
411                for (idx, alias_column) in alias_columns.iter().enumerate() {
412                    completions.push(CompletionItem {
413                        label: alias_column.to_string(),
414                        kind: CompletionItemKind::Column,
415                        detail: base_columns.get(idx).and_then(|(_, ty)| ty.clone()),
416                        insert_text: None,
417                        insert_text_format: None,
418                        trigger_completion_after_insert: false,
419                        sort_text: Some(format!("{idx:04}")),
420                    });
421                }
422
423                completions.extend(
424                    base_columns
425                        .into_iter()
426                        .skip(alias_columns.len())
427                        .enumerate()
428                        .map(|(idx, (name, ty))| CompletionItem {
429                            label: name.to_string(),
430                            kind: CompletionItemKind::Column,
431                            detail: ty,
432                            insert_text: None,
433                            insert_text_format: None,
434                            trigger_completion_after_insert: false,
435                            sort_text: Some(format!("{:04}", idx + alias_columns.len())),
436                        }),
437                );
438            }
439            Some(ast_nav::ParentSouce::ParenSelect(paren_select)) => {
440                let columns = collect::paren_select_columns_with_types(db, file, &paren_select);
441                completions.extend(columns.into_iter().map(|(name, ty)| CompletionItem {
442                    label: name.to_string(),
443                    kind: CompletionItemKind::Column,
444                    detail: ty.map(|t| t.to_string()),
445                    insert_text: None,
446                    insert_text_format: None,
447                    trigger_completion_after_insert: false,
448                    sort_text: None,
449                }));
450            }
451            Some(ast_nav::ParentSouce::SelectInto(select_into)) => {
452                let columns = collect::select_into_columns_with_types(db, file, &select_into);
453                completions.extend(columns.into_iter().map(|(name, ty)| CompletionItem {
454                    label: name.to_string(),
455                    kind: CompletionItemKind::Column,
456                    detail: ty.map(|t| t.to_string()),
457                    insert_text: None,
458                    insert_text_format: None,
459                    trigger_completion_after_insert: false,
460                    sort_text: None,
461                }));
462            }
463            None => {}
464        }
465    }
466    completions
467}
468
469fn alias_base_columns_with_types(
470    db: &dyn Db,
471    file: File,
472    syntax_root: &SyntaxNode,
473    alias: &ast::Alias,
474) -> Vec<(Name, Option<String>)> {
475    let Some(from_item) = alias.syntax().ancestors().find_map(ast::FromItem::cast) else {
476        return vec![];
477    };
478    let Some(table_ptr) = resolve::table_ptr_from_from_item(db, InFile::new(file, &from_item))
479    else {
480        return vec![];
481    };
482
483    let table_node = table_ptr.to_node(syntax_root);
484
485    match ast_nav::parent_source(&table_node) {
486        Some(ast_nav::ParentSouce::CreateTable(create_table)) => {
487            collect::table_columns(db, file, &create_table)
488                .into_iter()
489                .map(|(name, ty)| (name, ty.map(|t| t.to_string())))
490                .collect()
491        }
492        Some(ast_nav::ParentSouce::WithTable(with_table)) => {
493            collect::with_table_columns_with_types(db, file, with_table)
494                .into_iter()
495                .map(|(name, ty)| (name, ty.map(|t| t.to_string())))
496                .collect()
497        }
498        Some(ast_nav::ParentSouce::CreateView(create_view)) => {
499            collect::view_like_columns_with_types(db, file, &create_view)
500                .into_iter()
501                .map(|(name, ty)| (name, ty.map(|t| t.to_string())))
502                .collect()
503        }
504        Some(ast_nav::ParentSouce::ParenSelect(paren_select)) => {
505            collect::paren_select_columns_with_types(db, file, &paren_select)
506                .into_iter()
507                .map(|(name, ty)| (name, ty.map(|t| t.to_string())))
508                .collect()
509        }
510        Some(ast_nav::ParentSouce::CreateTableAs(create_table_as)) => {
511            collect::create_table_as_columns_with_types(db, file, &create_table_as)
512                .into_iter()
513                .map(|(name, ty)| (name, ty.map(|t| t.to_string())))
514                .collect()
515        }
516        Some(ast_nav::ParentSouce::SelectInto(select_into)) => {
517            collect::select_into_columns_with_types(db, file, &select_into)
518                .into_iter()
519                .map(|(name, ty)| (name, ty.map(|t| t.to_string())))
520                .collect()
521        }
522        Some(ast_nav::ParentSouce::Alias(_)) | None => vec![],
523    }
524}
525
526fn schema_completions(binder: &binder::Binder) -> Vec<CompletionItem> {
527    let builtin_schemas = [
528        "public",
529        "pg_catalog",
530        "pg_temp",
531        "pg_toast",
532        "information_schema",
533    ];
534    let mut completions: Vec<CompletionItem> = builtin_schemas
535        .into_iter()
536        .enumerate()
537        .map(|(i, name)| CompletionItem {
538            label: name.to_string(),
539            kind: CompletionItemKind::Schema,
540            detail: None,
541            insert_text: None,
542            insert_text_format: None,
543            trigger_completion_after_insert: false,
544            sort_text: Some(format!("{i}")),
545        })
546        .collect();
547
548    for name in binder.all_symbols_by_kind(SymbolKind::Schema, None) {
549        completions.push(CompletionItem {
550            label: name.to_string(),
551            kind: CompletionItemKind::Schema,
552            detail: None,
553            insert_text: None,
554            insert_text_format: None,
555            trigger_completion_after_insert: false,
556            sort_text: None,
557        });
558    }
559
560    completions
561}
562
563fn table_completions(file: &ast::SourceFile, token: &SyntaxToken) -> Vec<CompletionItem> {
564    let binder = binder::bind(file);
565    let schema = schema_qualifier_at_token(token);
566    let tables = binder.all_symbols_by_kind(SymbolKind::Table, schema.as_ref());
567    let mut completions: Vec<CompletionItem> = tables
568        .into_iter()
569        .map(|name| CompletionItem {
570            label: name.to_string(),
571            kind: CompletionItemKind::Table,
572            detail: None,
573            insert_text: None,
574            insert_text_format: None,
575            trigger_completion_after_insert: false,
576            sort_text: None,
577        })
578        .collect();
579
580    if schema.is_none() {
581        completions.extend(schema_completions(&binder));
582    }
583
584    completions
585}
586
587fn delete_clauses_completions(
588    file: &ast::SourceFile,
589    delete: &ast::Delete,
590    token: &SyntaxToken,
591) -> Vec<CompletionItem> {
592    let mut completions = vec![];
593
594    // `delete from $0`
595    if token.kind() == SyntaxKind::FROM_KW {
596        return table_completions(file, token);
597    }
598
599    if delete.using_clause().is_none() {
600        completions.push(CompletionItem {
601            label: "using".to_owned(),
602            kind: CompletionItemKind::Snippet,
603            detail: None,
604            insert_text: Some("using $0".to_owned()),
605            insert_text_format: Some(CompletionInsertTextFormat::Snippet),
606            trigger_completion_after_insert: true,
607            sort_text: None,
608        });
609    }
610
611    if delete.where_clause().is_none() {
612        completions.push(CompletionItem {
613            label: "where".to_owned(),
614            kind: CompletionItemKind::Snippet,
615            detail: None,
616            insert_text: Some("where $0".to_owned()),
617            insert_text_format: Some(CompletionInsertTextFormat::Snippet),
618            trigger_completion_after_insert: true,
619            sort_text: None,
620        });
621    }
622
623    if delete.returning_clause().is_none() {
624        completions.push(CompletionItem {
625            label: "returning".to_owned(),
626            kind: CompletionItemKind::Snippet,
627            detail: None,
628            insert_text: Some("returning $0".to_owned()),
629            insert_text_format: Some(CompletionInsertTextFormat::Snippet),
630            trigger_completion_after_insert: true,
631            sort_text: None,
632        });
633    }
634
635    completions
636}
637
638fn delete_expr_completions(
639    db: &dyn Db,
640    file: File,
641    source_file: &ast::SourceFile,
642    delete: &ast::Delete,
643    token: &SyntaxToken,
644) -> Vec<CompletionItem> {
645    let binder = binder::bind(source_file);
646    let mut completions = vec![];
647
648    let Some(path) = delete.relation_name().and_then(|r| r.path()) else {
649        return completions;
650    };
651
652    let Some(delete_table_name) = name::table_name(&path) else {
653        return completions;
654    };
655
656    let has_table_qualifier = qualifier_at_token(token).is_some_and(|q| q == delete_table_name);
657    let schema = schema_qualifier_at_token(token);
658    let position = token.text_range().start();
659
660    if has_table_qualifier {
661        let functions = binder.functions_with_single_param(&delete_table_name);
662        completions.extend(functions.into_iter().map(|name| CompletionItem {
663            label: name.to_string(),
664            kind: CompletionItemKind::Function,
665            detail: function_detail(db, file, name, schema.as_ref(), position),
666            insert_text: None,
667            insert_text_format: None,
668            trigger_completion_after_insert: false,
669            sort_text: None,
670        }));
671    } else {
672        let functions = binder.all_symbols_by_kind(SymbolKind::Function, None);
673        completions.extend(functions.into_iter().map(|name| CompletionItem {
674            label: format!("{name}()"),
675            kind: CompletionItemKind::Function,
676            detail: function_detail(db, file, name, schema.as_ref(), position),
677            insert_text: None,
678            insert_text_format: None,
679            trigger_completion_after_insert: false,
680            sort_text: None,
681        }));
682
683        completions.push(CompletionItem {
684            label: delete_table_name.to_string(),
685            kind: CompletionItemKind::Table,
686            detail: None,
687            insert_text: None,
688            insert_text_format: None,
689            trigger_completion_after_insert: false,
690            sort_text: None,
691        });
692    }
693
694    let schema = name::schema_name(&path);
695    let schemas = binder.resolved_schemas(position, schema.as_ref());
696    if let Some(table_ptr) = binder.lookup_with(&delete_table_name, SymbolKind::Table, &schemas)
697        && let Some(create_table) = table_ptr
698            .to_node(source_file.syntax())
699            .ancestors()
700            .find_map(ast::CreateTableLike::cast)
701    {
702        let columns = collect::table_columns(db, file, &create_table);
703        completions.extend(columns.into_iter().map(|(name, ty)| CompletionItem {
704            label: name.to_string(),
705            kind: CompletionItemKind::Column,
706            detail: ty.map(|t| t.to_string()),
707            insert_text: None,
708            insert_text_format: None,
709            trigger_completion_after_insert: false,
710            sort_text: None,
711        }));
712    }
713
714    completions
715}
716
717fn table_name_from_from_item(from_item: &ast::FromItem) -> Option<Name> {
718    if let Some(alias) = from_item.alias()
719        && let Some(alias_name) = alias.name()
720    {
721        return Some(Name::from_node(&alias_name));
722    }
723    if let Some(name_ref) = from_item.name_ref() {
724        return Some(Name::from_node(&name_ref));
725    }
726    None
727}
728
729fn qualifier_at_token(token: &SyntaxToken) -> Option<Name> {
730    let qualifier_token = if token.kind() == SyntaxKind::DOT {
731        token.prev_token()
732    } else if token.kind() == SyntaxKind::IDENT
733        && let Some(prev) = token.prev_token()
734        && prev.kind() == SyntaxKind::DOT
735    {
736        prev.prev_token()
737    } else {
738        None
739    };
740
741    qualifier_token
742        .filter(|tk| tk.kind() == SyntaxKind::IDENT)
743        .map(|tk| Name::from_string(tk.text().to_string()))
744}
745
746#[derive(Debug)]
747enum CompletionContext {
748    TableOnly,
749    Default,
750    SelectClause(ast::SelectClause),
751    SelectClauses(ast::Select),
752    SelectExpr(ast::Select),
753    LimitClause,
754    OffsetClause,
755    DeleteClauses(ast::Delete),
756    DeleteExpr(ast::Delete),
757}
758
759fn completion_context(token: &SyntaxToken) -> CompletionContext {
760    if let Some(node) = token.parent() {
761        let mut inside_delete_clause = false;
762        let mut inside_from_item = false;
763        let mut inside_paren_expr = false;
764        let mut inside_select_expr_clause = false;
765        let mut inside_limit_clause = false;
766        let mut inside_offset_clause = false;
767        for a in node.ancestors() {
768            if ast::Truncate::can_cast(a.kind()) || ast::Table::can_cast(a.kind()) {
769                return CompletionContext::TableOnly;
770            }
771            if ast::WhereClause::can_cast(a.kind())
772                || ast::UsingClause::can_cast(a.kind())
773                || ast::ReturningClause::can_cast(a.kind())
774            {
775                inside_delete_clause = true;
776            }
777            if ast::LimitClause::can_cast(a.kind()) {
778                inside_limit_clause = true;
779            }
780            if ast::OffsetClause::can_cast(a.kind()) {
781                inside_offset_clause = true;
782            }
783            if ast::WhereClause::can_cast(a.kind())
784                || ast::GroupByClause::can_cast(a.kind())
785                || ast::HavingClause::can_cast(a.kind())
786                || ast::OrderByClause::can_cast(a.kind())
787            {
788                inside_select_expr_clause = true;
789            }
790            if ast::FromItem::can_cast(a.kind()) {
791                inside_from_item = true;
792            }
793            if ast::ParenExpr::can_cast(a.kind()) {
794                inside_paren_expr = true;
795            }
796            if let Some(delete) = ast::Delete::cast(a.clone()) {
797                if inside_delete_clause {
798                    return CompletionContext::DeleteExpr(delete);
799                }
800                if delete.relation_name().is_some() {
801                    return CompletionContext::DeleteClauses(delete);
802                }
803                return CompletionContext::TableOnly;
804            }
805            if let Some(select) = ast::Select::cast(a.clone()) {
806                if inside_limit_clause {
807                    return CompletionContext::LimitClause;
808                }
809                if inside_offset_clause {
810                    return CompletionContext::OffsetClause;
811                }
812                if inside_select_expr_clause {
813                    return CompletionContext::SelectExpr(select);
814                }
815                if inside_from_item && !inside_paren_expr && select.from_clause().is_some() {
816                    return CompletionContext::SelectClauses(select);
817                }
818            }
819            if let Some(select_clause) = ast::SelectClause::cast(a.clone()) {
820                return CompletionContext::SelectClause(select_clause);
821            }
822        }
823    }
824    CompletionContext::Default
825}
826
827fn token_at_offset(file: &ast::SourceFile, offset: TextSize) -> Option<SyntaxToken> {
828    let Some(mut token) = file.syntax().token_at_offset(offset).left_biased() else {
829        // empty file - definitely at top level
830        return None;
831    };
832    while token.kind() == SyntaxKind::WHITESPACE {
833        if let Some(tk) = token.prev_token() {
834            token = tk;
835        }
836    }
837    Some(token)
838}
839
840// In order to make completions, we do something similar to rust analyzer by
841// inserting an ident to make the parse tree parse in more cases.
842// Rust analyzer does fancier things for this, which we can investigate later.
843//
844// This helps us support `select t. from t`, which parses as `select t.from t`.
845// If we insert the ident we get, `select t.c from t`.
846fn file_with_completion_marker(file: &ast::SourceFile, offset: TextSize) -> ast::SourceFile {
847    let mut sql = file.syntax().text().to_string();
848    let offset = u32::from(offset) as usize;
849    let offset = offset.min(sql.len());
850    sql.insert_str(offset, COMPLETION_MARKER);
851    ast::SourceFile::parse(&sql).tree()
852}
853
854fn schema_qualifier_at_token(token: &SyntaxToken) -> Option<Schema> {
855    qualifier_at_token(token).map(Schema)
856}
857
858fn function_detail(
859    db: &dyn Db,
860    file: File,
861    function_name: &Name,
862    schema: Option<&Schema>,
863    position: TextSize,
864) -> Option<String> {
865    let binder = bind(db, file);
866    let source_file = parse(db, file).tree();
867    let schemas = binder.resolved_schemas(position, schema);
868    let create_function = binder
869        .lookup_with(function_name, SymbolKind::Function, &schemas)?
870        .to_node(source_file.syntax())
871        .ancestors()
872        .find_map(ast::CreateFunction::cast)?;
873    let path = create_function.path()?;
874    let (schema, function_name) = resolve::resolve_function_info(db, InFile::new(file, &path))?;
875
876    let param_list = create_function.param_list()?;
877    let params = param_list.syntax().text().to_string();
878
879    let ret_type = create_function.ret_type()?;
880    let return_type = ret_type.syntax().text().to_string();
881
882    Some(format!("{schema}.{function_name}{params} {return_type}"))
883}
884
885fn default_completions() -> Vec<CompletionItem> {
886    ["delete from", "select", "table", "truncate"]
887        .map(|stmt| CompletionItem {
888            label: stmt.to_owned(),
889            kind: CompletionItemKind::Snippet,
890            detail: None,
891            insert_text: Some(format!("{stmt} $0;")),
892            insert_text_format: Some(CompletionInsertTextFormat::Snippet),
893            trigger_completion_after_insert: true,
894            sort_text: None,
895        })
896        .into_iter()
897        .collect()
898}
899
900#[derive(Debug, Clone, Copy, PartialEq, Eq)]
901pub enum CompletionItemKind {
902    Keyword,
903    Table,
904    Column,
905    Function,
906    Schema,
907    Type,
908    Snippet,
909    Operator,
910}
911
912impl CompletionItemKind {
913    fn sort_prefix(self) -> &'static str {
914        match self {
915            Self::Column => "0",
916            Self::Keyword => "1",
917            Self::Table => "1",
918            Self::Type => "1",
919            Self::Snippet => "1",
920            Self::Function => "2",
921            Self::Operator => "8",
922            Self::Schema => "9",
923        }
924    }
925}
926
927impl CompletionItem {
928    pub fn sort_text(&self) -> String {
929        let prefix = self.kind.sort_prefix();
930        let suffix = self.sort_text.as_ref().unwrap_or(&self.label);
931        format!("{prefix}_{suffix}")
932    }
933}
934
935#[derive(Debug, Clone, Copy, PartialEq, Eq)]
936pub enum CompletionInsertTextFormat {
937    PlainText,
938    Snippet,
939}
940
941#[derive(Debug, Clone, PartialEq, Eq)]
942pub struct CompletionItem {
943    pub label: String,
944    pub kind: CompletionItemKind,
945    pub detail: Option<String>,
946    pub insert_text: Option<String>,
947    pub insert_text_format: Option<CompletionInsertTextFormat>,
948    pub trigger_completion_after_insert: bool,
949    pub sort_text: Option<String>,
950}
951
952#[cfg(test)]
953mod tests {
954    use super::completion;
955
956    use crate::test_utils::Fixture;
957    use insta::assert_snapshot;
958    use tabled::builder::Builder;
959    use tabled::settings::Style;
960
961    #[must_use]
962    fn completions(sql: &str) -> String {
963        let fixture = Fixture::new_allow_errors(sql);
964        let offset = fixture.marker().offset();
965        let items = completion(fixture.db(), offset);
966        assert!(
967            !items.is_empty(),
968            "No completions found. If this was intended, use `completions_not_found` instead."
969        );
970        format_items(items)
971    }
972
973    fn completions_not_found(sql: &str) {
974        let fixture = Fixture::new_allow_errors(sql);
975        let offset = fixture.marker().offset();
976        let items = completion(fixture.db(), offset);
977        assert_eq!(
978            items,
979            vec![],
980            "Completions found. If this was unintended, use `completions` instead."
981        )
982    }
983
984    fn format_items(mut items: Vec<super::CompletionItem>) -> String {
985        items.sort_by_key(|a| a.sort_text());
986
987        let rows: Vec<Vec<String>> = items
988            .into_iter()
989            .map(|item| {
990                vec![
991                    item.label,
992                    format!("{:?}", item.kind),
993                    item.detail.unwrap_or_default(),
994                ]
995            })
996            .collect();
997
998        let mut builder = Builder::default();
999        builder.push_record(["label", "kind", "detail"]);
1000        for row in rows {
1001            builder.push_record(row);
1002        }
1003
1004        let mut table = builder.build();
1005        table.with(Style::psql());
1006        table.to_string()
1007    }
1008
1009    #[test]
1010    fn completion_at_start() {
1011        assert_snapshot!(completions("$0"), @r"
1012         label       | kind    | detail 
1013        -------------+---------+--------
1014         delete from | Snippet |        
1015         select      | Snippet |        
1016         table       | Snippet |        
1017         truncate    | Snippet |
1018        ");
1019    }
1020
1021    #[test]
1022    fn completion_at_top_level() {
1023        assert_snapshot!(completions("
1024create table t(a int);
1025$0
1026"), @r"
1027         label       | kind    | detail 
1028        -------------+---------+--------
1029         delete from | Snippet |        
1030         select      | Snippet |        
1031         table       | Snippet |        
1032         truncate    | Snippet |
1033        ");
1034    }
1035
1036    #[test]
1037    fn completion_in_string() {
1038        completions_not_found("select '$0';");
1039    }
1040
1041    #[test]
1042    fn completion_in_comment() {
1043        completions_not_found("-- $0 ");
1044    }
1045
1046    #[test]
1047    fn completion_after_truncate() {
1048        assert_snapshot!(completions("
1049create table users (id int);
1050truncate $0;
1051"), @r"
1052         label              | kind   | detail 
1053        --------------------+--------+--------
1054         users              | Table  |        
1055         public             | Schema |        
1056         pg_catalog         | Schema |        
1057         pg_temp            | Schema |        
1058         pg_toast           | Schema |        
1059         information_schema | Schema |
1060        ");
1061    }
1062
1063    #[test]
1064    fn completion_table_at_top_level() {
1065        assert_snapshot!(completions("$0"), @r"
1066         label       | kind    | detail 
1067        -------------+---------+--------
1068         delete from | Snippet |        
1069         select      | Snippet |        
1070         table       | Snippet |        
1071         truncate    | Snippet |
1072        ");
1073    }
1074
1075    #[test]
1076    fn completion_table_nested() {
1077        assert_snapshot!(completions("select * from ($0)"), @r"
1078         label       | kind    | detail 
1079        -------------+---------+--------
1080         delete from | Snippet |        
1081         select      | Snippet |        
1082         table       | Snippet |        
1083         truncate    | Snippet |
1084        ");
1085    }
1086
1087    #[test]
1088    fn completion_after_table() {
1089        assert_snapshot!(completions("
1090create table users (id int);
1091table $0;
1092"), @r"
1093         label              | kind   | detail 
1094        --------------------+--------+--------
1095         users              | Table  |        
1096         public             | Schema |        
1097         pg_catalog         | Schema |        
1098         pg_temp            | Schema |        
1099         pg_toast           | Schema |        
1100         information_schema | Schema |
1101        ");
1102    }
1103
1104    #[test]
1105    fn completion_select_without_from() {
1106        assert_snapshot!(completions("
1107create table t (a int);
1108select $0;
1109"), @r"
1110         label              | kind    | detail 
1111        --------------------+---------+--------
1112         except             | Snippet |        
1113         fetch              | Snippet |        
1114         for                | Snippet |        
1115         from               | Snippet |        
1116         group by           | Snippet |        
1117         having             | Snippet |        
1118         intersect          | Snippet |        
1119         limit              | Snippet |        
1120         offset             | Snippet |        
1121         order by           | Snippet |        
1122         t                  | Table   |        
1123         union              | Snippet |        
1124         where              | Snippet |        
1125         window             | Snippet |        
1126         public             | Schema  |        
1127         pg_catalog         | Schema  |        
1128         pg_temp            | Schema  |        
1129         pg_toast           | Schema  |        
1130         information_schema | Schema  |
1131        ");
1132    }
1133
1134    #[test]
1135    fn completion_after_select() {
1136        assert_snapshot!(completions("
1137create table t(a text, b int);
1138create function f() returns text as 'select 1::text' language sql;
1139select $0 from t;
1140"), @r"
1141         label              | kind     | detail                  
1142        --------------------+----------+-------------------------
1143         a                  | Column   | text                    
1144         b                  | Column   | int                     
1145         t                  | Table    |                         
1146         f()                | Function | public.f() returns text 
1147         *                  | Operator |                         
1148         public             | Schema   |                         
1149         pg_catalog         | Schema   |                         
1150         pg_temp            | Schema   |                         
1151         pg_toast           | Schema   |                         
1152         information_schema | Schema   |
1153        ");
1154    }
1155
1156    #[test]
1157    fn completion_after_select_create_table_inherits_builtin() {
1158        assert_snapshot!(completions("
1159-- include-builtins
1160create table t ()
1161inherits (information_schema.sql_features);
1162select $0 from t;
1163"), @"
1164         label              | kind     | detail         
1165        --------------------+----------+----------------
1166         comments           | Column   | character_data 
1167         feature_id         | Column   | character_data 
1168         feature_name       | Column   | character_data 
1169         is_supported       | Column   | yes_or_no      
1170         is_verified_by     | Column   | character_data 
1171         sub_feature_id     | Column   | character_data 
1172         sub_feature_name   | Column   | character_data 
1173         t                  | Table    |                
1174         *                  | Operator |                
1175         public             | Schema   |                
1176         pg_catalog         | Schema   |                
1177         pg_temp            | Schema   |                
1178         pg_toast           | Schema   |                
1179         information_schema | Schema   |
1180        ");
1181    }
1182
1183    #[test]
1184    fn completion_after_select_create_table_inherits_create_table_as() {
1185        assert_snapshot!(completions("
1186create table parent as select 1 a, 'x'::text b;
1187create table child (c int) inherits (parent);
1188select $0 from child;
1189"), @"
1190         label              | kind     | detail  
1191        --------------------+----------+---------
1192         a                  | Column   | integer 
1193         b                  | Column   | text    
1194         c                  | Column   | int     
1195         child              | Table    |         
1196         parent             | Table    |         
1197         *                  | Operator |         
1198         public             | Schema   |         
1199         pg_catalog         | Schema   |         
1200         pg_temp            | Schema   |         
1201         pg_toast           | Schema   |         
1202         information_schema | Schema   |
1203        ");
1204    }
1205
1206    #[test]
1207    fn completion_after_select_create_table_like_select_into() {
1208        assert_snapshot!(completions("
1209select 1 a, 'x'::text b into parent;
1210create table child (like parent);
1211select $0 from child;
1212"), @"
1213         label              | kind     | detail  
1214        --------------------+----------+---------
1215         a                  | Column   | integer 
1216         b                  | Column   | text    
1217         child              | Table    |         
1218         parent             | Table    |         
1219         *                  | Operator |         
1220         public             | Schema   |         
1221         pg_catalog         | Schema   |         
1222         pg_temp            | Schema   |         
1223         pg_toast           | Schema   |         
1224         information_schema | Schema   |
1225        ");
1226    }
1227
1228    #[test]
1229    fn completion_after_select_select_into() {
1230        assert_snapshot!(completions("
1231select 1 a, 'x'::text b into t;
1232select $0 from t;
1233"), @"
1234         label              | kind     | detail  
1235        --------------------+----------+---------
1236         a                  | Column   | integer 
1237         b                  | Column   | text    
1238         t                  | Table    |         
1239         *                  | Operator |         
1240         public             | Schema   |         
1241         pg_catalog         | Schema   |         
1242         pg_temp            | Schema   |         
1243         pg_toast           | Schema   |         
1244         information_schema | Schema   |
1245        ");
1246    }
1247
1248    #[test]
1249    fn completion_select_table_qualified() {
1250        assert_snapshot!(completions("
1251create table t (c int);
1252select t.$0 from t;
1253"), @r"
1254         label | kind     | detail 
1255        -------+----------+--------
1256         c     | Column   | int    
1257         *     | Operator |
1258        ");
1259    }
1260
1261    #[test]
1262    fn completion_after_select_with_cte() {
1263        assert_snapshot!(completions("
1264with t as (select 1 a)
1265select $0 from t;
1266"), @r"
1267         label              | kind     | detail  
1268        --------------------+----------+---------
1269         a                  | Column   | integer 
1270         *                  | Operator |         
1271         public             | Schema   |         
1272         pg_catalog         | Schema   |         
1273         pg_temp            | Schema   |         
1274         pg_toast           | Schema   |         
1275         information_schema | Schema   |
1276        ");
1277    }
1278
1279    #[test]
1280    fn completion_after_select_with_cte_alias_column_list() {
1281        assert_snapshot!(completions("
1282with t as (select 1 a, 2 b, 3 c)
1283select $0 from t as u(x, y);
1284"), @r"
1285         label              | kind     | detail  
1286        --------------------+----------+---------
1287         x                  | Column   | integer 
1288         y                  | Column   | integer 
1289         c                  | Column   | integer 
1290         *                  | Operator |         
1291         public             | Schema   |         
1292         pg_catalog         | Schema   |         
1293         pg_temp            | Schema   |         
1294         pg_toast           | Schema   |         
1295         information_schema | Schema   |
1296        ");
1297    }
1298
1299    #[test]
1300    fn completion_values_cte() {
1301        assert_snapshot!(completions("
1302with t as (values (1, 'foo', false))
1303select $0 from t;
1304"), @r"
1305         label              | kind     | detail  
1306        --------------------+----------+---------
1307         column1            | Column   | integer 
1308         column2            | Column   | text    
1309         column3            | Column   | boolean 
1310         *                  | Operator |         
1311         public             | Schema   |         
1312         pg_catalog         | Schema   |         
1313         pg_temp            | Schema   |         
1314         pg_toast           | Schema   |         
1315         information_schema | Schema   |
1316        ");
1317    }
1318
1319    #[test]
1320    fn completion_values_subquery() {
1321        assert_snapshot!(completions("
1322select $0 from (values (1, 'foo', 1.5, false));
1323"), @r"
1324         label              | kind     | detail  
1325        --------------------+----------+---------
1326         column1            | Column   | integer 
1327         column2            | Column   | text    
1328         column3            | Column   | numeric 
1329         column4            | Column   | boolean 
1330         *                  | Operator |         
1331         public             | Schema   |         
1332         pg_catalog         | Schema   |         
1333         pg_temp            | Schema   |         
1334         pg_toast           | Schema   |         
1335         information_schema | Schema   |
1336        ");
1337    }
1338
1339    #[test]
1340    fn completion_with_schema_qualifier() {
1341        assert_snapshot!(completions("
1342create function f() returns int8 as 'select 1' language sql;
1343create function foo.b() returns int8 as 'select 2' language sql;
1344select public.$0;
1345"), @r"
1346         label | kind     | detail                  
1347        -------+----------+-------------------------
1348         f()   | Function | public.f() returns int8
1349        ");
1350    }
1351
1352    #[test]
1353    fn completion_truncate_with_schema_qualifier() {
1354        assert_snapshot!(completions("
1355create table users (id int);
1356truncate public.$0;
1357"), @r"
1358         label | kind  | detail 
1359        -------+-------+--------
1360         users | Table |
1361        ");
1362    }
1363
1364    #[test]
1365    fn completion_after_delete_from() {
1366        assert_snapshot!(completions("
1367create table users (id int);
1368delete from $0;
1369"), @r"
1370         label              | kind   | detail 
1371        --------------------+--------+--------
1372         users              | Table  |        
1373         public             | Schema |        
1374         pg_catalog         | Schema |        
1375         pg_temp            | Schema |        
1376         pg_toast           | Schema |        
1377         information_schema | Schema |
1378        ");
1379    }
1380
1381    #[test]
1382    fn completion_delete_clauses() {
1383        assert_snapshot!(completions("
1384create table t (id int);
1385delete from t $0;
1386"), @r"
1387         label     | kind    | detail 
1388        -----------+---------+--------
1389         returning | Snippet |        
1390         using     | Snippet |        
1391         where     | Snippet |
1392        ");
1393    }
1394
1395    #[test]
1396    fn completion_delete_where_expr() {
1397        assert_snapshot!(completions("
1398create table t (id int, name text);
1399create function is_active() returns bool as 'select true' language sql;
1400delete from t where $0;
1401"), @r"
1402         label       | kind     | detail                          
1403        -------------+----------+---------------------------------
1404         id          | Column   | int                             
1405         name        | Column   | text                            
1406         t           | Table    |                                 
1407         is_active() | Function | public.is_active() returns bool
1408        ")
1409    }
1410
1411    #[test]
1412    fn completion_delete_returning_expr() {
1413        assert_snapshot!(completions("
1414create table t (id int, name text);
1415delete from t returning $0;
1416"), @r"
1417         label | kind   | detail 
1418        -------+--------+--------
1419         id    | Column | int    
1420         name  | Column | text   
1421         t     | Table  |
1422        ");
1423    }
1424
1425    #[test]
1426    fn completion_delete_where_qualified() {
1427        assert_snapshot!(completions("
1428-- different type than the table, so we shouldn't show this
1429create function b(diff_type) returns int8
1430  as 'select 1'
1431  language sql;
1432create function f(t) returns int8
1433  as 'select 1'
1434  language sql;
1435create table t (a int, b text);
1436delete from t where t.$0;
1437"), @r"
1438         label | kind     | detail 
1439        -------+----------+--------
1440         a     | Column   | int    
1441         b     | Column   | text   
1442         f     | Function |
1443        ");
1444    }
1445
1446    #[test]
1447    fn completion_select_clauses() {
1448        assert_snapshot!(completions("
1449with t as (select 1 a)
1450select a from t $0;
1451"), @r"
1452         label     | kind    | detail 
1453        -----------+---------+--------
1454         except    | Snippet |        
1455         fetch     | Snippet |        
1456         for       | Snippet |        
1457         group by  | Snippet |        
1458         having    | Snippet |        
1459         intersect | Snippet |        
1460         limit     | Snippet |        
1461         offset    | Snippet |        
1462         order by  | Snippet |        
1463         union     | Snippet |        
1464         where     | Snippet |        
1465         window    | Snippet |
1466        ");
1467    }
1468
1469    #[test]
1470    fn completion_select_clauses_simple() {
1471        assert_snapshot!(completions("
1472select 1 from t $0;
1473"), @r"
1474         label     | kind    | detail 
1475        -----------+---------+--------
1476         except    | Snippet |        
1477         fetch     | Snippet |        
1478         for       | Snippet |        
1479         group by  | Snippet |        
1480         having    | Snippet |        
1481         intersect | Snippet |        
1482         limit     | Snippet |        
1483         offset    | Snippet |        
1484         order by  | Snippet |        
1485         union     | Snippet |        
1486         where     | Snippet |        
1487         window    | Snippet |
1488        ");
1489    }
1490
1491    #[test]
1492    fn completion_select_group_by_expr() {
1493        assert_snapshot!(completions("
1494with t as (select 1 a)
1495select a from t group by $0;
1496"), @r"
1497         label | kind   | detail  
1498        -------+--------+---------
1499         a     | Column | integer 
1500         t     | Table  |
1501        ");
1502    }
1503
1504    #[test]
1505    fn completion_select_where_expr() {
1506        assert_snapshot!(completions("
1507create table t (id int, name text);
1508select * from t where $0;
1509"), @r"
1510         label | kind   | detail 
1511        -------+--------+--------
1512         id    | Column | int    
1513         name  | Column | text   
1514         t     | Table  |
1515        ");
1516    }
1517
1518    #[test]
1519    fn completion_select_limit() {
1520        assert_snapshot!(completions("
1521create function get_limit() returns int as 'select 10' language sql;
1522select 1 from t limit $0;
1523"), @r"
1524         label       | kind     | detail                         
1525        -------------+----------+--------------------------------
1526         all         | Keyword  |                                
1527         get_limit() | Function | public.get_limit() returns int
1528        ");
1529    }
1530
1531    #[test]
1532    fn completion_select_offset() {
1533        assert_snapshot!(completions("
1534create function get_offset() returns int as 'select 10' language sql;
1535select 1 from t offset $0;
1536"), @r"
1537         label        | kind     | detail                          
1538        --------------+----------+---------------------------------
1539         get_offset() | Function | public.get_offset() returns int
1540        ");
1541    }
1542}