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.column_names())
406                    .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::FromAlias,
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
649        .relation_name()
650        .and_then(|relation| relation.relation_name_ref())
651        .and_then(|relation| relation.path_ref())
652    else {
653        return completions;
654    };
655
656    let Some(delete_table_name) = name::table_name(&path) else {
657        return completions;
658    };
659
660    let has_table_qualifier = qualifier_at_token(token).is_some_and(|q| q == delete_table_name);
661    let schema = schema_qualifier_at_token(token);
662    let position = token.text_range().start();
663
664    if has_table_qualifier {
665        let functions = binder.functions_with_single_param(&delete_table_name);
666        completions.extend(functions.into_iter().map(|name| CompletionItem {
667            label: name.to_string(),
668            kind: CompletionItemKind::Function,
669            detail: function_detail(db, file, name, schema.as_ref(), position),
670            insert_text: None,
671            insert_text_format: None,
672            trigger_completion_after_insert: false,
673            sort_text: None,
674        }));
675    } else {
676        let functions = binder.all_symbols_by_kind(SymbolKind::Function, None);
677        completions.extend(functions.into_iter().map(|name| CompletionItem {
678            label: format!("{name}()"),
679            kind: CompletionItemKind::Function,
680            detail: function_detail(db, file, name, schema.as_ref(), position),
681            insert_text: None,
682            insert_text_format: None,
683            trigger_completion_after_insert: false,
684            sort_text: None,
685        }));
686
687        completions.push(CompletionItem {
688            label: delete_table_name.to_string(),
689            kind: CompletionItemKind::Table,
690            detail: None,
691            insert_text: None,
692            insert_text_format: None,
693            trigger_completion_after_insert: false,
694            sort_text: None,
695        });
696    }
697
698    let schema = name::schema_name(&path);
699    let schemas = binder.resolved_schemas(position, schema.as_ref());
700    if let Some(table_ptr) = binder.lookup_with(&delete_table_name, SymbolKind::Table, &schemas)
701        && let Some(create_table) = table_ptr
702            .to_node(source_file.syntax())
703            .ancestors()
704            .find_map(ast::CreateTableLike::cast)
705    {
706        let columns = collect::table_columns(db, file, &create_table);
707        completions.extend(columns.into_iter().map(|(name, ty)| CompletionItem {
708            label: name.to_string(),
709            kind: CompletionItemKind::Column,
710            detail: ty.map(|t| t.to_string()),
711            insert_text: None,
712            insert_text_format: None,
713            trigger_completion_after_insert: false,
714            sort_text: None,
715        }));
716    }
717
718    completions
719}
720
721fn table_name_from_from_item(from_item: &ast::FromItem) -> Option<Name> {
722    if let Some(alias_name) = from_item.alias().and_then(|alias| alias.name()) {
723        return Some(Name::from_node(&alias_name));
724    }
725    if let ast::FromItem::RelationFromItem(relation) = from_item
726        && let Some(name_ref) = relation.name_ref()
727    {
728        return Some(Name::from_node(&name_ref));
729    }
730    None
731}
732
733fn qualifier_at_token(token: &SyntaxToken) -> Option<Name> {
734    let qualifier_token = if token.kind() == SyntaxKind::DOT {
735        token.prev_token()
736    } else if token.kind() == SyntaxKind::IDENT
737        && let Some(prev) = token.prev_token()
738        && prev.kind() == SyntaxKind::DOT
739    {
740        prev.prev_token()
741    } else {
742        None
743    };
744
745    qualifier_token
746        .filter(|tk| tk.kind() == SyntaxKind::IDENT)
747        .map(|tk| Name::from_string(tk.text().to_string()))
748}
749
750#[derive(Debug)]
751enum CompletionContext {
752    TableOnly,
753    Default,
754    SelectClause(ast::SelectClause),
755    SelectClauses(ast::Select),
756    SelectExpr(ast::Select),
757    LimitClause,
758    OffsetClause,
759    DeleteClauses(ast::Delete),
760    DeleteExpr(ast::Delete),
761}
762
763fn completion_context(token: &SyntaxToken) -> CompletionContext {
764    if let Some(node) = token.parent() {
765        let mut inside_delete_clause = false;
766        let mut inside_from_item = false;
767        let mut inside_paren_expr = false;
768        let mut inside_select_expr_clause = false;
769        let mut inside_limit_clause = false;
770        let mut inside_offset_clause = false;
771        for a in node.ancestors() {
772            if ast::Truncate::can_cast(a.kind()) || ast::Table::can_cast(a.kind()) {
773                return CompletionContext::TableOnly;
774            }
775            if ast::WhereClause::can_cast(a.kind())
776                || ast::UsingClause::can_cast(a.kind())
777                || ast::ReturningClause::can_cast(a.kind())
778            {
779                inside_delete_clause = true;
780            }
781            if ast::LimitClause::can_cast(a.kind()) {
782                inside_limit_clause = true;
783            }
784            if ast::OffsetClause::can_cast(a.kind()) {
785                inside_offset_clause = true;
786            }
787            if ast::WhereClause::can_cast(a.kind())
788                || ast::GroupByClause::can_cast(a.kind())
789                || ast::HavingClause::can_cast(a.kind())
790                || ast::OrderByClause::can_cast(a.kind())
791            {
792                inside_select_expr_clause = true;
793            }
794            if ast::FromItem::can_cast(a.kind()) {
795                inside_from_item = true;
796            }
797            if ast::ParenExpr::can_cast(a.kind()) {
798                inside_paren_expr = true;
799            }
800            if let Some(delete) = ast::Delete::cast(a.clone()) {
801                if inside_delete_clause {
802                    return CompletionContext::DeleteExpr(delete);
803                }
804                if delete.relation_name().is_some() {
805                    return CompletionContext::DeleteClauses(delete);
806                }
807                return CompletionContext::TableOnly;
808            }
809            if let Some(select) = ast::Select::cast(a.clone()) {
810                if inside_limit_clause {
811                    return CompletionContext::LimitClause;
812                }
813                if inside_offset_clause {
814                    return CompletionContext::OffsetClause;
815                }
816                if inside_select_expr_clause {
817                    return CompletionContext::SelectExpr(select);
818                }
819                if inside_from_item && !inside_paren_expr && select.from_clause().is_some() {
820                    return CompletionContext::SelectClauses(select);
821                }
822            }
823            if let Some(select_clause) = ast::SelectClause::cast(a.clone()) {
824                return CompletionContext::SelectClause(select_clause);
825            }
826        }
827    }
828    CompletionContext::Default
829}
830
831fn token_at_offset(file: &ast::SourceFile, offset: TextSize) -> Option<SyntaxToken> {
832    let Some(mut token) = file.syntax().token_at_offset(offset).left_biased() else {
833        // empty file - definitely at top level
834        return None;
835    };
836    while token.kind() == SyntaxKind::WHITESPACE {
837        if let Some(tk) = token.prev_token() {
838            token = tk;
839        }
840    }
841    Some(token)
842}
843
844// In order to make completions, we do something similar to rust analyzer by
845// inserting an ident to make the parse tree parse in more cases.
846// Rust analyzer does fancier things for this, which we can investigate later.
847//
848// This helps us support `select t. from t`, which parses as `select t.from t`.
849// If we insert the ident we get, `select t.c from t`.
850fn file_with_completion_marker(file: &ast::SourceFile, offset: TextSize) -> ast::SourceFile {
851    let mut sql = file.syntax().text().to_string();
852    let offset = u32::from(offset) as usize;
853    let offset = offset.min(sql.len());
854    sql.insert_str(offset, COMPLETION_MARKER);
855    ast::SourceFile::parse(&sql).tree()
856}
857
858fn schema_qualifier_at_token(token: &SyntaxToken) -> Option<Schema> {
859    qualifier_at_token(token).map(Schema)
860}
861
862fn function_detail(
863    db: &dyn Db,
864    file: File,
865    function_name: &Name,
866    schema: Option<&Schema>,
867    position: TextSize,
868) -> Option<String> {
869    let binder = bind(db, file);
870    let source_file = parse(db, file).tree();
871    let schemas = binder.resolved_schemas(position, schema);
872    let create_function = binder
873        .lookup_with(function_name, SymbolKind::Function, &schemas)?
874        .to_node(source_file.syntax())
875        .ancestors()
876        .find_map(ast::CreateFunction::cast)?;
877    let path = create_function.name()?.path()?;
878    let (schema, function_name) = resolve::resolve_function_info(db, InFile::new(file, &path))?;
879
880    let param_list = create_function.param_list()?;
881    let params = param_list.syntax().text().to_string();
882
883    let ret_type = create_function.ret_type()?;
884    let return_type = ret_type.syntax().text().to_string();
885
886    Some(format!("{schema}.{function_name}{params} {return_type}"))
887}
888
889fn default_completions() -> Vec<CompletionItem> {
890    ["delete from", "select", "table", "truncate"]
891        .map(|stmt| CompletionItem {
892            label: stmt.to_owned(),
893            kind: CompletionItemKind::Snippet,
894            detail: None,
895            insert_text: Some(format!("{stmt} $0;")),
896            insert_text_format: Some(CompletionInsertTextFormat::Snippet),
897            trigger_completion_after_insert: true,
898            sort_text: None,
899        })
900        .into_iter()
901        .collect()
902}
903
904#[derive(Debug, Clone, Copy, PartialEq, Eq)]
905pub enum CompletionItemKind {
906    Keyword,
907    Table,
908    Column,
909    Function,
910    Schema,
911    Type,
912    Snippet,
913    Operator,
914}
915
916impl CompletionItemKind {
917    fn sort_prefix(self) -> &'static str {
918        match self {
919            Self::Column => "0",
920            Self::Keyword => "1",
921            Self::Table => "1",
922            Self::Type => "1",
923            Self::Snippet => "1",
924            Self::Function => "2",
925            Self::Operator => "8",
926            Self::Schema => "9",
927        }
928    }
929}
930
931impl CompletionItem {
932    pub fn sort_text(&self) -> String {
933        let prefix = self.kind.sort_prefix();
934        let suffix = self.sort_text.as_ref().unwrap_or(&self.label);
935        format!("{prefix}_{suffix}")
936    }
937}
938
939#[derive(Debug, Clone, Copy, PartialEq, Eq)]
940pub enum CompletionInsertTextFormat {
941    PlainText,
942    Snippet,
943}
944
945#[derive(Debug, Clone, PartialEq, Eq)]
946pub struct CompletionItem {
947    pub label: String,
948    pub kind: CompletionItemKind,
949    pub detail: Option<String>,
950    pub insert_text: Option<String>,
951    pub insert_text_format: Option<CompletionInsertTextFormat>,
952    pub trigger_completion_after_insert: bool,
953    pub sort_text: Option<String>,
954}
955
956#[cfg(test)]
957mod tests {
958    use super::completion;
959
960    use crate::test_utils::Fixture;
961    use insta::assert_snapshot;
962    use tabled::builder::Builder;
963    use tabled::settings::Style;
964
965    #[must_use]
966    fn completions(sql: &str) -> String {
967        let fixture = Fixture::new_allow_errors(sql);
968        let offset = fixture.marker().offset();
969        let items = completion(fixture.db(), offset);
970        assert!(
971            !items.is_empty(),
972            "No completions found. If this was intended, use `completions_not_found` instead."
973        );
974        format_items(items)
975    }
976
977    fn completions_not_found(sql: &str) {
978        let fixture = Fixture::new_allow_errors(sql);
979        let offset = fixture.marker().offset();
980        let items = completion(fixture.db(), offset);
981        assert_eq!(
982            items,
983            vec![],
984            "Completions found. If this was unintended, use `completions` instead."
985        )
986    }
987
988    fn format_items(mut items: Vec<super::CompletionItem>) -> String {
989        items.sort_by_key(|a| a.sort_text());
990
991        let rows: Vec<Vec<String>> = items
992            .into_iter()
993            .map(|item| {
994                vec![
995                    item.label,
996                    format!("{:?}", item.kind),
997                    item.detail.unwrap_or_default(),
998                ]
999            })
1000            .collect();
1001
1002        let mut builder = Builder::default();
1003        builder.push_record(["label", "kind", "detail"]);
1004        for row in rows {
1005            builder.push_record(row);
1006        }
1007
1008        let mut table = builder.build();
1009        table.with(Style::psql());
1010        table.to_string()
1011    }
1012
1013    #[test]
1014    fn completion_at_start() {
1015        assert_snapshot!(completions("$0"), @r"
1016         label       | kind    | detail 
1017        -------------+---------+--------
1018         delete from | Snippet |        
1019         select      | Snippet |        
1020         table       | Snippet |        
1021         truncate    | Snippet |
1022        ");
1023    }
1024
1025    #[test]
1026    fn completion_at_top_level() {
1027        assert_snapshot!(completions("
1028create table t(a int);
1029$0
1030"), @r"
1031         label       | kind    | detail 
1032        -------------+---------+--------
1033         delete from | Snippet |        
1034         select      | Snippet |        
1035         table       | Snippet |        
1036         truncate    | Snippet |
1037        ");
1038    }
1039
1040    #[test]
1041    fn completion_in_string() {
1042        completions_not_found("select '$0';");
1043    }
1044
1045    #[test]
1046    fn completion_in_comment() {
1047        completions_not_found("-- $0 ");
1048    }
1049
1050    #[test]
1051    fn completion_after_truncate() {
1052        assert_snapshot!(completions("
1053create table users (id int);
1054truncate $0;
1055"), @r"
1056         label              | kind   | detail 
1057        --------------------+--------+--------
1058         users              | Table  |        
1059         public             | Schema |        
1060         pg_catalog         | Schema |        
1061         pg_temp            | Schema |        
1062         pg_toast           | Schema |        
1063         information_schema | Schema |
1064        ");
1065    }
1066
1067    #[test]
1068    fn completion_table_at_top_level() {
1069        assert_snapshot!(completions("$0"), @r"
1070         label       | kind    | detail 
1071        -------------+---------+--------
1072         delete from | Snippet |        
1073         select      | Snippet |        
1074         table       | Snippet |        
1075         truncate    | Snippet |
1076        ");
1077    }
1078
1079    #[test]
1080    fn completion_table_nested() {
1081        assert_snapshot!(completions("select * from ($0)"), @r"
1082         label       | kind    | detail 
1083        -------------+---------+--------
1084         delete from | Snippet |        
1085         select      | Snippet |        
1086         table       | Snippet |        
1087         truncate    | Snippet |
1088        ");
1089    }
1090
1091    #[test]
1092    fn completion_after_table() {
1093        assert_snapshot!(completions("
1094create table users (id int);
1095table $0;
1096"), @r"
1097         label              | kind   | detail 
1098        --------------------+--------+--------
1099         users              | Table  |        
1100         public             | Schema |        
1101         pg_catalog         | Schema |        
1102         pg_temp            | Schema |        
1103         pg_toast           | Schema |        
1104         information_schema | Schema |
1105        ");
1106    }
1107
1108    #[test]
1109    fn completion_select_without_from() {
1110        assert_snapshot!(completions("
1111create table t (a int);
1112select $0;
1113"), @r"
1114         label              | kind    | detail 
1115        --------------------+---------+--------
1116         except             | Snippet |        
1117         fetch              | Snippet |        
1118         for                | Snippet |        
1119         from               | Snippet |        
1120         group by           | Snippet |        
1121         having             | Snippet |        
1122         intersect          | Snippet |        
1123         limit              | Snippet |        
1124         offset             | Snippet |        
1125         order by           | Snippet |        
1126         t                  | Table   |        
1127         union              | Snippet |        
1128         where              | Snippet |        
1129         window             | Snippet |        
1130         public             | Schema  |        
1131         pg_catalog         | Schema  |        
1132         pg_temp            | Schema  |        
1133         pg_toast           | Schema  |        
1134         information_schema | Schema  |
1135        ");
1136    }
1137
1138    #[test]
1139    fn completion_after_select() {
1140        assert_snapshot!(completions("
1141create table t(a text, b int);
1142create function f() returns text as 'select 1::text' language sql;
1143select $0 from t;
1144"), @r"
1145         label              | kind     | detail                  
1146        --------------------+----------+-------------------------
1147         a                  | Column   | text                    
1148         b                  | Column   | int                     
1149         t                  | Table    |                         
1150         f()                | Function | public.f() returns text 
1151         *                  | Operator |                         
1152         public             | Schema   |                         
1153         pg_catalog         | Schema   |                         
1154         pg_temp            | Schema   |                         
1155         pg_toast           | Schema   |                         
1156         information_schema | Schema   |
1157        ");
1158    }
1159
1160    #[test]
1161    fn completion_after_select_create_table_inherits_builtin() {
1162        assert_snapshot!(completions("
1163-- include-builtins
1164create table t ()
1165inherits (information_schema.sql_features);
1166select $0 from t;
1167"), @"
1168         label              | kind     | detail         
1169        --------------------+----------+----------------
1170         comments           | Column   | character_data 
1171         feature_id         | Column   | character_data 
1172         feature_name       | Column   | character_data 
1173         is_supported       | Column   | yes_or_no      
1174         is_verified_by     | Column   | character_data 
1175         sub_feature_id     | Column   | character_data 
1176         sub_feature_name   | Column   | character_data 
1177         t                  | Table    |                
1178         *                  | Operator |                
1179         public             | Schema   |                
1180         pg_catalog         | Schema   |                
1181         pg_temp            | Schema   |                
1182         pg_toast           | Schema   |                
1183         information_schema | Schema   |
1184        ");
1185    }
1186
1187    #[test]
1188    fn completion_after_select_create_table_inherits_create_table_as() {
1189        assert_snapshot!(completions("
1190create table parent as select 1 a, 'x'::text b;
1191create table child (c int) inherits (parent);
1192select $0 from child;
1193"), @"
1194         label              | kind     | detail  
1195        --------------------+----------+---------
1196         a                  | Column   | integer 
1197         b                  | Column   | text    
1198         c                  | Column   | int     
1199         child              | Table    |         
1200         parent             | Table    |         
1201         *                  | Operator |         
1202         public             | Schema   |         
1203         pg_catalog         | Schema   |         
1204         pg_temp            | Schema   |         
1205         pg_toast           | Schema   |         
1206         information_schema | Schema   |
1207        ");
1208    }
1209
1210    #[test]
1211    fn completion_after_select_create_table_like_select_into() {
1212        assert_snapshot!(completions("
1213select 1 a, 'x'::text b into parent;
1214create table child (like parent);
1215select $0 from child;
1216"), @"
1217         label              | kind     | detail  
1218        --------------------+----------+---------
1219         a                  | Column   | integer 
1220         b                  | Column   | text    
1221         child              | Table    |         
1222         parent             | Table    |         
1223         *                  | Operator |         
1224         public             | Schema   |         
1225         pg_catalog         | Schema   |         
1226         pg_temp            | Schema   |         
1227         pg_toast           | Schema   |         
1228         information_schema | Schema   |
1229        ");
1230    }
1231
1232    #[test]
1233    fn completion_after_select_select_into() {
1234        assert_snapshot!(completions("
1235select 1 a, 'x'::text b into t;
1236select $0 from t;
1237"), @"
1238         label              | kind     | detail  
1239        --------------------+----------+---------
1240         a                  | Column   | integer 
1241         b                  | Column   | text    
1242         t                  | Table    |         
1243         *                  | Operator |         
1244         public             | Schema   |         
1245         pg_catalog         | Schema   |         
1246         pg_temp            | Schema   |         
1247         pg_toast           | Schema   |         
1248         information_schema | Schema   |
1249        ");
1250    }
1251
1252    #[test]
1253    fn completion_select_table_qualified() {
1254        assert_snapshot!(completions("
1255create table t (c int);
1256select t.$0 from t;
1257"), @r"
1258         label | kind     | detail 
1259        -------+----------+--------
1260         c     | Column   | int    
1261         *     | Operator |
1262        ");
1263    }
1264
1265    #[test]
1266    fn completion_after_select_with_cte() {
1267        assert_snapshot!(completions("
1268with t as (select 1 a)
1269select $0 from t;
1270"), @r"
1271         label              | kind     | detail  
1272        --------------------+----------+---------
1273         a                  | Column   | integer 
1274         *                  | Operator |         
1275         public             | Schema   |         
1276         pg_catalog         | Schema   |         
1277         pg_temp            | Schema   |         
1278         pg_toast           | Schema   |         
1279         information_schema | Schema   |
1280        ");
1281    }
1282
1283    #[test]
1284    fn completion_after_select_with_cte_alias_column_list() {
1285        assert_snapshot!(completions("
1286with t as (select 1 a, 2 b, 3 c)
1287select $0 from t as u(x, y);
1288"), @r"
1289         label              | kind     | detail  
1290        --------------------+----------+---------
1291         x                  | Column   | integer 
1292         y                  | Column   | integer 
1293         c                  | Column   | integer 
1294         *                  | Operator |         
1295         public             | Schema   |         
1296         pg_catalog         | Schema   |         
1297         pg_temp            | Schema   |         
1298         pg_toast           | Schema   |         
1299         information_schema | Schema   |
1300        ");
1301    }
1302
1303    #[test]
1304    fn completion_values_cte() {
1305        assert_snapshot!(completions("
1306with t as (values (1, 'foo', false))
1307select $0 from t;
1308"), @r"
1309         label              | kind     | detail  
1310        --------------------+----------+---------
1311         column1            | Column   | integer 
1312         column2            | Column   | text    
1313         column3            | Column   | boolean 
1314         *                  | Operator |         
1315         public             | Schema   |         
1316         pg_catalog         | Schema   |         
1317         pg_temp            | Schema   |         
1318         pg_toast           | Schema   |         
1319         information_schema | Schema   |
1320        ");
1321    }
1322
1323    #[test]
1324    fn completion_values_subquery() {
1325        assert_snapshot!(completions("
1326select $0 from (values (1, 'foo', 1.5, false));
1327"), @r"
1328         label              | kind     | detail  
1329        --------------------+----------+---------
1330         column1            | Column   | integer 
1331         column2            | Column   | text    
1332         column3            | Column   | numeric 
1333         column4            | Column   | boolean 
1334         *                  | Operator |         
1335         public             | Schema   |         
1336         pg_catalog         | Schema   |         
1337         pg_temp            | Schema   |         
1338         pg_toast           | Schema   |         
1339         information_schema | Schema   |
1340        ");
1341    }
1342
1343    #[test]
1344    fn completion_with_schema_qualifier() {
1345        assert_snapshot!(completions("
1346create function f() returns int8 as 'select 1' language sql;
1347create function foo.b() returns int8 as 'select 2' language sql;
1348select public.$0;
1349"), @r"
1350         label | kind     | detail                  
1351        -------+----------+-------------------------
1352         f()   | Function | public.f() returns int8
1353        ");
1354    }
1355
1356    #[test]
1357    fn completion_truncate_with_schema_qualifier() {
1358        assert_snapshot!(completions("
1359create table users (id int);
1360truncate public.$0;
1361"), @r"
1362         label | kind  | detail 
1363        -------+-------+--------
1364         users | Table |
1365        ");
1366    }
1367
1368    #[test]
1369    fn completion_after_delete_from() {
1370        assert_snapshot!(completions("
1371create table users (id int);
1372delete from $0;
1373"), @r"
1374         label              | kind   | detail 
1375        --------------------+--------+--------
1376         users              | Table  |        
1377         public             | Schema |        
1378         pg_catalog         | Schema |        
1379         pg_temp            | Schema |        
1380         pg_toast           | Schema |        
1381         information_schema | Schema |
1382        ");
1383    }
1384
1385    #[test]
1386    fn completion_delete_clauses() {
1387        assert_snapshot!(completions("
1388create table t (id int);
1389delete from t $0;
1390"), @r"
1391         label     | kind    | detail 
1392        -----------+---------+--------
1393         returning | Snippet |        
1394         using     | Snippet |        
1395         where     | Snippet |
1396        ");
1397    }
1398
1399    #[test]
1400    fn completion_delete_where_expr() {
1401        assert_snapshot!(completions("
1402create table t (id int, name text);
1403create function is_active() returns bool as 'select true' language sql;
1404delete from t where $0;
1405"), @r"
1406         label       | kind     | detail                          
1407        -------------+----------+---------------------------------
1408         id          | Column   | int                             
1409         name        | Column   | text                            
1410         t           | Table    |                                 
1411         is_active() | Function | public.is_active() returns bool
1412        ")
1413    }
1414
1415    #[test]
1416    fn completion_delete_returning_expr() {
1417        assert_snapshot!(completions("
1418create table t (id int, name text);
1419delete from t returning $0;
1420"), @r"
1421         label | kind   | detail 
1422        -------+--------+--------
1423         id    | Column | int    
1424         name  | Column | text   
1425         t     | Table  |
1426        ");
1427    }
1428
1429    #[test]
1430    fn completion_delete_where_qualified() {
1431        assert_snapshot!(completions("
1432-- different type than the table, so we shouldn't show this
1433create function b(diff_type) returns int8
1434  as 'select 1'
1435  language sql;
1436create function f(t) returns int8
1437  as 'select 1'
1438  language sql;
1439create table t (a int, b text);
1440delete from t where t.$0;
1441"), @r"
1442         label | kind     | detail 
1443        -------+----------+--------
1444         a     | Column   | int    
1445         b     | Column   | text   
1446         f     | Function |
1447        ");
1448    }
1449
1450    #[test]
1451    fn completion_select_clauses() {
1452        assert_snapshot!(completions("
1453with t as (select 1 a)
1454select a from t $0;
1455"), @r"
1456         label     | kind    | detail 
1457        -----------+---------+--------
1458         except    | Snippet |        
1459         fetch     | Snippet |        
1460         for       | Snippet |        
1461         group by  | Snippet |        
1462         having    | Snippet |        
1463         intersect | Snippet |        
1464         limit     | Snippet |        
1465         offset    | Snippet |        
1466         order by  | Snippet |        
1467         union     | Snippet |        
1468         where     | Snippet |        
1469         window    | Snippet |
1470        ");
1471    }
1472
1473    #[test]
1474    fn completion_select_clauses_simple() {
1475        assert_snapshot!(completions("
1476select 1 from t $0;
1477"), @r"
1478         label     | kind    | detail 
1479        -----------+---------+--------
1480         except    | Snippet |        
1481         fetch     | Snippet |        
1482         for       | Snippet |        
1483         group by  | Snippet |        
1484         having    | Snippet |        
1485         intersect | Snippet |        
1486         limit     | Snippet |        
1487         offset    | Snippet |        
1488         order by  | Snippet |        
1489         union     | Snippet |        
1490         where     | Snippet |        
1491         window    | Snippet |
1492        ");
1493    }
1494
1495    #[test]
1496    fn completion_select_group_by_expr() {
1497        assert_snapshot!(completions("
1498with t as (select 1 a)
1499select a from t group by $0;
1500"), @r"
1501         label | kind   | detail  
1502        -------+--------+---------
1503         a     | Column | integer 
1504         t     | Table  |
1505        ");
1506    }
1507
1508    #[test]
1509    fn completion_select_where_expr() {
1510        assert_snapshot!(completions("
1511create table t (id int, name text);
1512select * from t where $0;
1513"), @r"
1514         label | kind   | detail 
1515        -------+--------+--------
1516         id    | Column | int    
1517         name  | Column | text   
1518         t     | Table  |
1519        ");
1520    }
1521
1522    #[test]
1523    fn completion_select_limit() {
1524        assert_snapshot!(completions("
1525create function get_limit() returns int as 'select 10' language sql;
1526select 1 from t limit $0;
1527"), @r"
1528         label       | kind     | detail                         
1529        -------------+----------+--------------------------------
1530         all         | Keyword  |                                
1531         get_limit() | Function | public.get_limit() returns int
1532        ");
1533    }
1534
1535    #[test]
1536    fn completion_select_offset() {
1537        assert_snapshot!(completions("
1538create function get_offset() returns int as 'select 10' language sql;
1539select 1 from t offset $0;
1540"), @r"
1541         label        | kind     | detail                          
1542        --------------+----------+---------------------------------
1543         get_offset() | Function | public.get_offset() returns int
1544        ");
1545    }
1546}