diff --git a/Cargo.lock b/Cargo.lock index f3f5914ea..803c04887 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -749,7 +749,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a68f1f47cdf0ec8ee4b941b2eee2a80cb796db73118c0dd09ac63fbe405be22" dependencies = [ "memchr", - "regex-automata 0.4.9", + "regex-automata 0.4.13", "serde", ] @@ -1667,7 +1667,7 @@ dependencies = [ "aho-corasick", "bstr", "log", - "regex-automata 0.4.9", + "regex-automata 0.4.13", "regex-syntax 0.8.5", ] @@ -1971,7 +1971,7 @@ dependencies = [ "globset", "log", "memchr", - "regex-automata 0.4.9", + "regex-automata 0.4.13", "same-file", "walkdir", "winapi-util", @@ -2717,11 +2717,13 @@ dependencies = [ "async-std", "criterion", "fuzzy-matcher", + "insta", "pgls_schema_cache", "pgls_test_utils", "pgls_text_size", "pgls_treesitter", "pgls_treesitter_grammar", + "regex", "schemars", "serde", "serde_json", @@ -2729,6 +2731,7 @@ dependencies = [ "tokio", "tracing", "tree-sitter", + "unindent", ] [[package]] @@ -3634,13 +3637,13 @@ dependencies = [ [[package]] name = "regex" -version = "1.11.1" +version = "1.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.9", + "regex-automata 0.4.13", "regex-syntax 0.8.5", ] @@ -3655,9 +3658,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.9" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" dependencies = [ "aho-corasick", "memchr", @@ -5025,6 +5028,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" +[[package]] +name = "unindent" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" + [[package]] name = "untrusted" version = "0.9.0" diff --git a/crates/pgls_completions/Cargo.toml b/crates/pgls_completions/Cargo.toml index cfb4fa864..43c0c3f4d 100644 --- a/crates/pgls_completions/Cargo.toml +++ b/crates/pgls_completions/Cargo.toml @@ -32,7 +32,11 @@ tokio = { version = "1.41.1", features = ["full"] } [dev-dependencies] criterion.workspace = true +insta.workspace = true pgls_test_utils.workspace = true +regex = "1.12.2" +unindent = "0.2.4" + [lib] doctest = false diff --git a/crates/pgls_completions/src/providers/columns.rs b/crates/pgls_completions/src/providers/columns.rs index a902579e4..eda6fdcd1 100644 --- a/crates/pgls_completions/src/providers/columns.rs +++ b/crates/pgls_completions/src/providers/columns.rs @@ -50,36 +50,12 @@ fn get_completion_text(ctx: &TreesitterContext, col: &Column) -> CompletionText #[cfg(test)] mod tests { - use std::vec; + use sqlx::PgPool; - use pgls_text_size::TextRange; - use sqlx::{Executor, PgPool}; - - use crate::{ - CompletionItem, CompletionItemKind, complete, - test_helper::{ - CompletionAssertion, assert_complete_results, get_test_deps, get_test_params, - }, - }; - - use pgls_test_utils::QueryWithCursorPosition; - - struct TestCase { - query: String, - message: &'static str, - label: &'static str, - description: &'static str, - } - - impl TestCase { - fn get_input_query(&self) -> QueryWithCursorPosition { - let strs: Vec<&str> = self.query.split_whitespace().collect(); - strs.join(" ").as_str().into() - } - } + use crate::test_helper::{TestCompletionsCase, TestCompletionsSuite}; #[sqlx::test(migrator = "pgls_test_utils::MIGRATIONS")] - async fn completes_columns(pool: PgPool) { + async fn handles_nested_queries(pool: PgPool) { let setup = r#" create schema private; @@ -99,62 +75,20 @@ mod tests { ); "#; - pool.execute(setup).await.unwrap(); - - let queries: Vec = vec![ - TestCase { - message: "correctly prefers the columns of present tables", - query: format!( - r#"select na{} from public.audio_books;"#, - QueryWithCursorPosition::cursor_marker() - ), - label: "narrator", - description: "public.audio_books", - }, - TestCase { - message: "correctly handles nested queries", - query: format!( - r#" - select - * - from ( - select id, na{} - from private.audio_books - ) as subquery - join public.users u - on u.id = subquery.id; - "#, - QueryWithCursorPosition::cursor_marker() - ), - label: "narrator_id", - description: "private.audio_books", - }, - TestCase { - message: "works without a schema", - query: format!( - r#"select na{} from users;"#, - QueryWithCursorPosition::cursor_marker() - ), - label: "name", - description: "public.users", - }, - ]; - - for q in queries { - let (tree, cache) = get_test_deps(None, q.get_input_query(), &pool).await; - let params = get_test_params(&tree, &cache, q.get_input_query()); - let results = complete(params); - - let CompletionItem { - label, description, .. - } = results - .into_iter() - .next() - .expect("Should return at least one completion item"); - - assert_eq!(label, q.label, "{}", q.message); - assert_eq!(description, q.description, "{}", q.message); - } + TestCompletionsSuite::new(&pool, Some(setup)).with_case( + TestCompletionsCase::new() + .inside_static_statement(r#" + select * from ( + + ) as subquery + join public.users u + on u.id = subquery.id; + "#) + .type_sql("select id, narrator_id<1> from private.audio_books") + .comment("Should prefer the one from private.audio_audiobooks, since the other tables are out of scope.") + ) + .snapshot("handles_nested_queries") + .await; } #[sqlx::test(migrator = "pgls_test_utils::MIGRATIONS")] @@ -178,49 +112,14 @@ mod tests { ); "#; - pool.execute(setup).await.unwrap(); - - let case = TestCase { - query: format!(r#"select n{};"#, QueryWithCursorPosition::cursor_marker()), - description: "", - label: "", - message: "", - }; - - let (tree, cache) = get_test_deps(None, case.get_input_query(), &pool).await; - let params = get_test_params(&tree, &cache, case.get_input_query()); - let mut items = complete(params); - - let _ = items.split_off(4); - - #[derive(Eq, PartialEq, Debug)] - struct LabelAndDesc { - label: String, - desc: String, - } - - let labels: Vec = items - .into_iter() - .map(|c| LabelAndDesc { - label: c.label, - desc: c.description, - }) - .collect(); - - let expected = vec![ - ("name", "public.users"), - ("narrator", "public.audio_books"), - ("narrator_id", "private.audio_books"), - ("id", "public.audio_books"), - ] - .into_iter() - .map(|(label, schema)| LabelAndDesc { - label: label.into(), - desc: schema.into(), - }) - .collect::>(); - - assert_eq!(labels, expected); + TestCompletionsSuite::new(&pool, Some(setup)) + .with_case( + TestCompletionsCase::new() + .type_sql("select narrator_id<1>") + .comment("Should suggest all columns with n first"), + ) + .snapshot("shows_multiple_columns_if_no_relation_specified") + .await; } #[sqlx::test(migrator = "pgls_test_utils::MIGRATIONS")] @@ -234,44 +133,10 @@ mod tests { ); "#; - let test_case = TestCase { - message: "suggests user created tables first", - query: format!( - r#"select {} from users"#, - QueryWithCursorPosition::cursor_marker() - ), - label: "", - description: "", - }; - - let (tree, cache) = get_test_deps(Some(setup), test_case.get_input_query(), &pool).await; - let params = get_test_params(&tree, &cache, test_case.get_input_query()); - let results = complete(params); - - let (first_four, _rest) = results.split_at(4); - - let has_column_in_first_four = |col: &'static str| { - first_four - .iter() - .any(|compl_item| compl_item.label.as_str() == col) - }; - - assert!( - has_column_in_first_four("id"), - "`id` not present in first four completion items." - ); - assert!( - has_column_in_first_four("name"), - "`name` not present in first four completion items." - ); - assert!( - has_column_in_first_four("address"), - "`address` not present in first four completion items." - ); - assert!( - has_column_in_first_four("email"), - "`email` not present in first four completion items." - ); + TestCompletionsSuite::new(&pool, Some(setup)) + .with_case(TestCompletionsCase::new().type_sql("select name from users")) + .snapshot("suggests_relevant_columns_without_letters") + .await; } #[sqlx::test(migrator = "pgls_test_utils::MIGRATIONS")] @@ -283,29 +148,20 @@ mod tests { id serial primary key, name text, address text, - email text + email text, + public boolean ); "#; - let test_case = TestCase { - message: "suggests user created tables first", - query: format!( - r#"select * from private.{}"#, - QueryWithCursorPosition::cursor_marker() - ), - label: "", - description: "", - }; - - let (tree, cache) = get_test_deps(Some(setup), test_case.get_input_query(), &pool).await; - let params = get_test_params(&tree, &cache, test_case.get_input_query()); - let results = complete(params); - - assert!( - !results - .into_iter() - .any(|item| item.kind == CompletionItemKind::Column) - ); + TestCompletionsSuite::new(&pool, Some(setup)) + .with_case( + TestCompletionsCase::new() + .inside_static_statement("select * from ") + .type_sql("private<1>.users") + .comment("No column suggestions."), + ) + .snapshot("ignores_cols_in_from_clause") + .await; } #[sqlx::test(migrator = "pgls_test_utils::MIGRATIONS")] @@ -330,58 +186,65 @@ mod tests { ); "#; - pool.execute(setup).await.unwrap(); + TestCompletionsSuite::new(&pool, Some(setup)) + .with_case( + TestCompletionsCase::new() + .inside_static_statement(" from public.users") + .type_sql("select address2<1>") + .comment("Should suggest address 2 from public table"), + ) + .with_case( + TestCompletionsCase::new() + .inside_static_statement(" from private.users") + .type_sql("select address1<1>") + .comment("Should suggest address 1 from private table"), + ) + .with_case( + TestCompletionsCase::new() + .inside_static_statement(" from private.users") + .type_sql("select settings<1>") + .comment("Should prioritize columns starting with s"), + ) + .snapshot("prefers_columns_of_mentioned_tables") + .await; + } - assert_complete_results( - format!( - r#"select {} from users"#, - QueryWithCursorPosition::cursor_marker() - ) - .as_str(), - vec![ - CompletionAssertion::Label("address2".into()), - CompletionAssertion::Label("email2".into()), - CompletionAssertion::Label("id2".into()), - CompletionAssertion::Label("name2".into()), - ], - None, - &pool, - ) - .await; + #[sqlx::test(migrator = "pgls_test_utils::MIGRATIONS")] + async fn filters_out_by_aliases_in_join_on(pool: PgPool) { + let setup = r#" + create schema auth; - assert_complete_results( - format!( - r#"select {} from private.users"#, - QueryWithCursorPosition::cursor_marker() - ) - .as_str(), - vec![ - CompletionAssertion::Label("address1".into()), - CompletionAssertion::Label("email1".into()), - CompletionAssertion::Label("id1".into()), - CompletionAssertion::Label("name1".into()), - ], - None, - &pool, - ) - .await; + create table auth.users ( + uid serial primary key, + name text not null, + email text unique not null + ); - // asserts fuzzy finding for "settings" - assert_complete_results( - format!( - r#"select sett{} from private.users"#, - QueryWithCursorPosition::cursor_marker() - ) - .as_str(), - vec![CompletionAssertion::Label("user_settings".into())], - None, - &pool, - ) - .await; + create table auth.posts ( + pid serial primary key, + user_id int not null references auth.users(uid), + title text not null, + content text, + created_at timestamp default now() + ); + "#; + + TestCompletionsSuite::new(&pool, Some(setup)) + .with_case( + TestCompletionsCase::new() + .inside_static_statement( + "select u.id, p.content from auth.users u join auth.posts p ", + ) + .type_sql("on u<1>.id = p.<2>user_id") + .comment("Should prefer primary indices here.") + .comment("We should only get columns from the auth.posts table."), + ) + .snapshot("filters_out_by_aliases_in_join_on") + .await; } #[sqlx::test(migrator = "pgls_test_utils::MIGRATIONS")] - async fn filters_out_by_aliases(pool: PgPool) { + async fn filters_out_by_aliases_in_select(pool: PgPool) { let setup = r#" create schema auth; @@ -400,52 +263,17 @@ mod tests { ); "#; - pool.execute(setup).await.unwrap(); - - // test in SELECT clause - assert_complete_results( - format!( - "select u.id, p.{} from auth.users u join auth.posts p on u.id = p.user_id;", - QueryWithCursorPosition::cursor_marker() - ) - .as_str(), - vec![ - CompletionAssertion::LabelNotExists("uid".to_string()), - CompletionAssertion::LabelNotExists("name".to_string()), - CompletionAssertion::LabelNotExists("email".to_string()), - CompletionAssertion::Label("content".to_string()), - CompletionAssertion::Label("created_at".to_string()), - CompletionAssertion::Label("pid".to_string()), - CompletionAssertion::Label("title".to_string()), - CompletionAssertion::Label("user_id".to_string()), - ], - None, - &pool, - ) - .await; - - // test in JOIN clause - assert_complete_results( - format!( - "select u.id, p.content from auth.users u join auth.posts p on u.id = p.{};", - QueryWithCursorPosition::cursor_marker() + TestCompletionsSuite::new(&pool, Some(setup)) + .with_case( + TestCompletionsCase::new() + .inside_static_statement( + " from auth.users u join auth.posts p on u.id = p.user_id;", + ) + .type_sql("select u.id, p.pid<1>") + .comment("We should only get columns from the auth.posts table."), ) - .as_str(), - vec![ - CompletionAssertion::LabelNotExists("uid".to_string()), - CompletionAssertion::LabelNotExists("name".to_string()), - CompletionAssertion::LabelNotExists("email".to_string()), - // primary keys are preferred - CompletionAssertion::Label("pid".to_string()), - CompletionAssertion::Label("content".to_string()), - CompletionAssertion::Label("created_at".to_string()), - CompletionAssertion::Label("title".to_string()), - CompletionAssertion::Label("user_id".to_string()), - ], - None, - &pool, - ) - .await; + .snapshot("filters_out_by_aliases_in_select") + .await; } #[sqlx::test(migrator = "pgls_test_utils::MIGRATIONS")] @@ -468,24 +296,12 @@ mod tests { ); "#; - /* - * We are not in the "ON" part of the JOIN clause, so we should not complete columns. - */ - assert_complete_results( - format!( - "select u.id, p.content from auth.users u join auth.{}", - QueryWithCursorPosition::cursor_marker() - ) - .as_str(), - vec![ - CompletionAssertion::KindNotExists(CompletionItemKind::Column), - CompletionAssertion::LabelAndKind("posts".to_string(), CompletionItemKind::Table), - CompletionAssertion::LabelAndKind("users".to_string(), CompletionItemKind::Table), - ], - Some(setup), - &pool, - ) - .await; + TestCompletionsSuite::new(&pool, Some(setup)).with_case( + TestCompletionsCase::new() + .type_sql("select u.uid, p.content from auth<1>.users<2> u join auth.posts p on u.uid = p.user_id") + .comment("Schema suggestions should be prioritized, since we want to push users to specify them.") + .comment("Here, we shouldn't have schema completions.") + ).snapshot("does_not_complete_cols_in_join_clauses").await; } #[sqlx::test(migrator = "pgls_test_utils::MIGRATIONS")] @@ -508,41 +324,16 @@ mod tests { ); "#; - pool.execute(setup).await.unwrap(); - - assert_complete_results( - format!( - "select u.id, auth.posts.content from auth.users u join auth.posts on u.{}", - QueryWithCursorPosition::cursor_marker() - ) - .as_str(), - vec![ - CompletionAssertion::KindNotExists(CompletionItemKind::Table), - CompletionAssertion::LabelAndKind("uid".to_string(), CompletionItemKind::Column), - CompletionAssertion::LabelAndKind("email".to_string(), CompletionItemKind::Column), - CompletionAssertion::LabelAndKind("name".to_string(), CompletionItemKind::Column), - ], - None, - &pool, - ) - .await; - - assert_complete_results( - format!( - "select u.id, p.content from auth.users u join auth.posts p on p.user_id = u.{}", - QueryWithCursorPosition::cursor_marker() - ) - .as_str(), - vec![ - CompletionAssertion::KindNotExists(CompletionItemKind::Table), - CompletionAssertion::LabelAndKind("uid".to_string(), CompletionItemKind::Column), - CompletionAssertion::LabelAndKind("email".to_string(), CompletionItemKind::Column), - CompletionAssertion::LabelAndKind("name".to_string(), CompletionItemKind::Column), - ], - None, - &pool, - ) - .await; + TestCompletionsSuite::new(&pool, Some(setup)).with_case( + TestCompletionsCase::new() + .inside_static_statement( + "select u.id, auth.posts.content from auth.users u join auth.posts p on ", + ) + .type_sql("<1>p.user_id<2> = u.uid<3>;") + .comment("Should prioritize primary keys here.") + .comment("Should only consider columns from auth.posts here.") + .comment("Should only consider columns from auth.users here.") + ).snapshot("completes_in_join_on_clause").await; } #[sqlx::test(migrator = "pgls_test_utils::MIGRATIONS")] @@ -565,76 +356,26 @@ mod tests { ); "#; - pool.execute(setup).await.unwrap(); - - assert_complete_results( - format!( - "select {} from public.one o join public.two on o.id = t.id;", - QueryWithCursorPosition::cursor_marker() - ) - .as_str(), - vec![ - CompletionAssertion::Label("a".to_string()), - CompletionAssertion::Label("b".to_string()), - CompletionAssertion::Label("c".to_string()), - CompletionAssertion::Label("d".to_string()), - CompletionAssertion::Label("e".to_string()), - ], - None, - &pool, - ) - .await; - - // "a" is already mentioned, so it jumps down - assert_complete_results( - format!( - "select a, {} from public.one o join public.two on o.id = t.id;", - QueryWithCursorPosition::cursor_marker() - ) - .as_str(), - vec![ - CompletionAssertion::Label("b".to_string()), - CompletionAssertion::Label("c".to_string()), - CompletionAssertion::Label("d".to_string()), - CompletionAssertion::Label("e".to_string()), - CompletionAssertion::Label("id".to_string()), - CompletionAssertion::Label("z".to_string()), - CompletionAssertion::Label("a".to_string()), - ], - None, - &pool, - ) - .await; - - // "id" of table one is mentioned, but table two isn't – - // its priority stays up - assert_complete_results( - format!( - "select o.id, a, b, c, d, e, {} from public.one o join public.two on o.id = t.id;", - QueryWithCursorPosition::cursor_marker() - ) - .as_str(), - vec![ - CompletionAssertion::LabelAndDesc("id".to_string(), "public.two".to_string()), - CompletionAssertion::Label("z".to_string()), - ], - None, - &pool, - ) - .await; - - // "id" is ambiguous, so both "id" columns are lowered in priority - assert_complete_results( - format!( - "select id, a, b, c, d, e, {} from public.one o join public.two on o.id = t.id;", - QueryWithCursorPosition::cursor_marker() - ) - .as_str(), - vec![CompletionAssertion::Label("z".to_string())], - None, - &pool, - ) - .await; + TestCompletionsSuite::new(&pool, Some(setup)) + .with_case( + TestCompletionsCase::new() + .inside_static_statement( + " from public.one o join public.two on o.id = t.id;", + ) + .type_sql("select o.id, a, <1>b, c, d, e, <2>z") + .comment("Should have low priority for `a`, since it's already mentioned.") + .comment("Should have high priority of id of table two, but not one, since it's already mentioned.") + ) + .with_case( + TestCompletionsCase::new() + .inside_static_statement( + " from public.one o join public.two on o.id = t.id;", + ) + .type_sql("select id, a, b, c, d, e, <1>z") + .comment("`id` could be from both tables, so both priorities are lowered."), + ) + .snapshot("prefers_not_mentioned_columns") + .await; } #[sqlx::test(migrator = "pgls_test_utils::MIGRATIONS")] @@ -653,69 +394,25 @@ mod tests { ); "#; - pool.execute(setup).await.unwrap(); - - // We should prefer the instrument columns, even though they - // are lower in the alphabet - - assert_complete_results( - format!( - "insert into instruments ({})", - QueryWithCursorPosition::cursor_marker() + TestCompletionsSuite::new(&pool, Some(setup)) + .with_case( + TestCompletionsCase::new() + .type_sql("insert into instruments (id, name) values (1, 'my_bass');"), ) - .as_str(), - vec![ - CompletionAssertion::Label("id".to_string()), - CompletionAssertion::Label("name".to_string()), - CompletionAssertion::Label("z".to_string()), - ], - None, - &pool, - ) - .await; - - assert_complete_results( - format!( - "insert into instruments (id, {})", - QueryWithCursorPosition::cursor_marker() + .with_case( + TestCompletionsCase::new() + .type_sql(r#"insert into instruments ("id", "name") values (1, 'my_bass');"#), ) - .as_str(), - vec![ - CompletionAssertion::Label("name".to_string()), - CompletionAssertion::Label("z".to_string()), - ], - None, - &pool, - ) - .await; - - assert_complete_results( - format!( - "insert into instruments (id, {}, name)", - QueryWithCursorPosition::cursor_marker() + .with_case( + TestCompletionsCase::new() + .inside_static_statement( + r#"insert into instruments (, name) values ('my_bass');"#, + ) + .type_sql("id, <1>z") + .comment("`name` is already written, so z should be suggested."), ) - .as_str(), - vec![CompletionAssertion::Label("z".to_string())], - None, - &pool, - ) - .await; - - // works with completed statement - assert_complete_results( - format!( - "insert into instruments (name, {}) values ('my_bass');", - QueryWithCursorPosition::cursor_marker() - ) - .as_str(), - vec![ - CompletionAssertion::Label("id".to_string()), - CompletionAssertion::Label("z".to_string()), - ], - None, - &pool, - ) - .await; + .snapshot("suggests_columns_in_insert_clause") + .await; } #[sqlx::test(migrator = "pgls_test_utils::MIGRATIONS")] @@ -735,74 +432,19 @@ mod tests { ); "#; - pool.execute(setup).await.unwrap(); - - assert_complete_results( - format!( - "select name from instruments where {} ", - QueryWithCursorPosition::cursor_marker() - ) - .as_str(), - vec![ - CompletionAssertion::Label("created_at".into()), - CompletionAssertion::Label("id".into()), - CompletionAssertion::Label("name".into()), - CompletionAssertion::Label("z".into()), - ], - None, - &pool, - ) - .await; - - assert_complete_results( - format!( - "select name from instruments where z = 'something' and created_at > {}", - QueryWithCursorPosition::cursor_marker() - ) - .as_str(), - // simply do not complete columns + schemas; functions etc. are ok - vec![ - CompletionAssertion::KindNotExists(CompletionItemKind::Column), - CompletionAssertion::KindNotExists(CompletionItemKind::Schema), - ], - None, - &pool, - ) - .await; - - // prefers not mentioned columns - assert_complete_results( - format!( - "select name from instruments where id = 'something' and {}", - QueryWithCursorPosition::cursor_marker() - ) - .as_str(), - vec![ - CompletionAssertion::Label("created_at".into()), - CompletionAssertion::Label("name".into()), - CompletionAssertion::Label("z".into()), - ], - None, - &pool, - ) - .await; - - // // uses aliases - assert_complete_results( - format!( - "select name from instruments i join others o on i.z = o.a where i.{}", - QueryWithCursorPosition::cursor_marker() - ) - .as_str(), - vec![ - CompletionAssertion::Label("created_at".into()), - CompletionAssertion::Label("id".into()), - CompletionAssertion::Label("name".into()), - ], - None, - &pool, - ) - .await; + TestCompletionsSuite::new(&pool, Some(setup)) + .with_case( + TestCompletionsCase::new() + .inside_static_statement( + "select name from instruments i join others o on i.z = o.a ", + ) + .type_sql("where o.<1>a = <2>i.z and <3>i.id > 5;") + .comment("should respect alias speciifcation") + .comment("should not prioritize suggest columns or schemas (right side of binary expression)") + .comment("should prioritize columns that aren't already mentioned") + ) + .snapshot("suggests_columns_in_where_clause") + .await; } #[sqlx::test(migrator = "pgls_test_utils::MIGRATIONS")] @@ -814,65 +456,42 @@ mod tests { z text, created_at timestamp with time zone default now() ); - - create table others ( - a text, - b text, - c text - ); "#; - pool.execute(setup).await.unwrap(); - - let queries = vec![ - format!( - "alter table instruments drop column {}", - QueryWithCursorPosition::cursor_marker() - ), - format!( - "alter table instruments drop column if exists {}", - QueryWithCursorPosition::cursor_marker() - ), - format!( - "alter table instruments alter column {} set default", - QueryWithCursorPosition::cursor_marker() - ), - format!( - "alter table instruments alter {} set default", - QueryWithCursorPosition::cursor_marker() - ), - format!( - "alter table public.instruments alter column {}", - QueryWithCursorPosition::cursor_marker() - ), - format!( - "alter table instruments alter {}", - QueryWithCursorPosition::cursor_marker() - ), - format!( - "alter table instruments rename {} to new_col", - QueryWithCursorPosition::cursor_marker() - ), - format!( - "alter table public.instruments rename column {} to new_col", - QueryWithCursorPosition::cursor_marker() - ), - ]; - - for query in queries { - assert_complete_results( - query.as_str(), - vec![ - CompletionAssertion::Label("created_at".into()), - CompletionAssertion::Label("id".into()), - CompletionAssertion::Label("name".into()), - CompletionAssertion::Label("z".into()), - ], - None, - &pool, + TestCompletionsSuite::new(&pool, Some(setup)) + .with_case( + TestCompletionsCase::new().type_sql("alter table instruments drop column name"), + ) + .with_case( + TestCompletionsCase::new().type_sql("alter table instruments drop column name"), + ) + .with_case( + TestCompletionsCase::new() + .type_sql("alter table instruments drop column if exists name"), + ) + .with_case( + TestCompletionsCase::new() + .type_sql("alter table instruments alter column name set default"), + ) + .with_case( + TestCompletionsCase::new() + .type_sql("alter table instruments alter name set default"), + ) + .with_case( + TestCompletionsCase::new() + .type_sql("alter table public.instruments alter column name"), ) + .with_case(TestCompletionsCase::new().type_sql("alter table instruments alter name")) + .with_case( + TestCompletionsCase::new() + .type_sql("alter table instruments rename name to new_col"), + ) + .with_case( + TestCompletionsCase::new() + .type_sql("alter table public.instruments rename column name to new_col"), + ) + .snapshot("suggests_columns_in_alter_table_and_drop_table") .await; - } } #[sqlx::test(migrator = "pgls_test_utils::MIGRATIONS")] @@ -886,41 +505,23 @@ mod tests { ); "#; - pool.execute(setup).await.unwrap(); - - let col_queries = vec![ - format!( - r#"create policy "my_pol" on public.instruments for select using ({})"#, - QueryWithCursorPosition::cursor_marker() - ), - format!( - r#"create policy "my_pol" on public.instruments for insert with check ({})"#, - QueryWithCursorPosition::cursor_marker() - ), - format!( - r#"create policy "my_pol" on public.instruments for update using (id = 1 and {})"#, - QueryWithCursorPosition::cursor_marker() - ), - format!( - r#"create policy "my_pol" on public.instruments for insert with check (id = 1 and {})"#, - QueryWithCursorPosition::cursor_marker() - ), - ]; - - for query in col_queries { - assert_complete_results( - query.as_str(), - vec![ - CompletionAssertion::Label("created_at".into()), - CompletionAssertion::Label("id".into()), - CompletionAssertion::Label("name".into()), - CompletionAssertion::Label("z".into()), - ], - None, - &pool, - ) + TestCompletionsSuite::new(&pool, Some(setup)) + .with_case( + TestCompletionsCase::new() + .inside_static_statement( + r#"create policy "my_pol" on public.instruments for select using ()"#, + ) + .type_sql("id = 1 and created_at > '2025-01-01'"), + ) + .with_case( + TestCompletionsCase::new() + .inside_static_statement( + r#"create policy "my_pol" on public.instruments for insert with check ()"#, + ) + .type_sql("id = 1 and created_at > '2025-01-01'"), + ) + .snapshot("suggests_columns_policy_using_clause") .await; - } } #[sqlx::test(migrator = "pgls_test_utils::MIGRATIONS")] @@ -936,75 +537,12 @@ mod tests { ); "#; - pool.execute(setup).await.unwrap(); - - // test completion inside quoted column name - assert_complete_results( - format!( - r#"select "em{}" from "private"."users""#, - QueryWithCursorPosition::cursor_marker() - ) - .as_str(), - vec![CompletionAssertion::LabelAndDesc( - "email".to_string(), - "private.users".to_string(), - )], - None, - &pool, - ) - .await; - - // test completion for already quoted column - assert_complete_results( - format!( - r#"select "quoted_col{}" from "private"."users""#, - QueryWithCursorPosition::cursor_marker() - ) - .as_str(), - vec![CompletionAssertion::LabelAndDesc( - "quoted_column".to_string(), - "private.users".to_string(), - )], - None, - &pool, - ) - .await; - - // test completion with empty quotes - assert_complete_results( - format!( - r#"select "{}" from "private"."users""#, - QueryWithCursorPosition::cursor_marker() + TestCompletionsSuite::new(&pool, Some(setup)) + .with_case( + TestCompletionsCase::new().type_sql(r#"select "email" from "private"."users";"#), ) - .as_str(), - vec![ - CompletionAssertion::Label("email".to_string()), - CompletionAssertion::Label("id".to_string()), - CompletionAssertion::Label("name".to_string()), - CompletionAssertion::Label("quoted_column".to_string()), - ], - None, - &pool, - ) - .await; - - // test completion with partially opened quote - assert_complete_results( - format!( - r#"select "{} from "private"."users""#, - QueryWithCursorPosition::cursor_marker() - ) - .as_str(), - vec![ - CompletionAssertion::Label("email".to_string()), - CompletionAssertion::Label("id".to_string()), - CompletionAssertion::Label("name".to_string()), - CompletionAssertion::Label("quoted_column".to_string()), - ], - None, - &pool, - ) - .await; + .snapshot("completes_quoted_columns") + .await; } #[sqlx::test(migrator = "pgls_test_utils::MIGRATIONS")] @@ -1025,240 +563,40 @@ mod tests { ); "#; - pool.execute(setup).await.unwrap(); - - { - // should suggest pr"."email and insert into existing quotes - let query = format!( - r#"select "e{}" from private.users "pr""#, - QueryWithCursorPosition::cursor_marker() - ); - - assert_complete_results( - query.as_str(), - vec![CompletionAssertion::CompletionTextAndRange( - r#"pr"."email"#.into(), - // replaces the full `"e"` - TextRange::new(8.into(), 9.into()), - )], - None, - &pool, - ) - .await; - } - - { - // should suggest pr"."email and insert into existing quotes - let query = format!( - r#"select "{}" from private.users "pr""#, - QueryWithCursorPosition::cursor_marker() - ); - - assert_complete_results( - query.as_str(), - vec![CompletionAssertion::CompletionTextAndRange( - r#"pr"."email"#.into(), - TextRange::new(8.into(), 8.into()), - )], - None, - &pool, - ) - .await; - } - - { - // should suggest email and insert into quotes - let query = format!( - r#"select pr."{}" from private.users "pr""#, - QueryWithCursorPosition::cursor_marker() - ); - assert_complete_results( - query.as_str(), - vec![CompletionAssertion::CompletionTextAndRange( - r#"email"#.into(), - TextRange::new(11.into(), 11.into()), - )], - None, - &pool, - ) - .await; - } - - { - // should suggest email - let query = format!( - r#"select "pr".{} from private.users "pr""#, - QueryWithCursorPosition::cursor_marker() - ); - assert_complete_results( - query.as_str(), - vec![CompletionAssertion::CompletionTextAndRange( - "email".into(), - TextRange::new(12.into(), 12.into()), - )], - None, - &pool, - ) - .await; - } - - { - // should suggest `email` - let query = format!( - r#"select pr.{} from private.users "pr""#, - QueryWithCursorPosition::cursor_marker() - ); - assert_complete_results( - query.as_str(), - vec![CompletionAssertion::CompletionTextAndRange( - "email".into(), - TextRange::new(10.into(), 10.into()), - )], - None, - &pool, - ) - .await; - } - - { - let query = format!( - r#"select {} from private.users "pr" join public.names n on pr.id = n.uid;"#, - QueryWithCursorPosition::cursor_marker() - ); - assert_complete_results( - query.as_str(), - vec![ - CompletionAssertion::CompletionTextAndRange( - "n.name".into(), - TextRange::new(7.into(), 7.into()), - ), - CompletionAssertion::CompletionTextAndRange( - "n.uid".into(), - TextRange::new(7.into(), 7.into()), - ), - CompletionAssertion::CompletionTextAndRange( - r#""pr".email"#.into(), - TextRange::new(7.into(), 7.into()), - ), - CompletionAssertion::CompletionTextAndRange( - r#""pr".id"#.into(), - TextRange::new(7.into(), 7.into()), - ), - ], - None, - &pool, - ) - .await; - } - - { - // should suggest "pr"."email" - let query = format!( - r#"select "{}" from private.users "pr" join public.names "n" on pr.id = n.uid;"#, - QueryWithCursorPosition::cursor_marker() - ); - assert_complete_results( - query.as_str(), - vec![ - CompletionAssertion::CompletionTextAndRange( - r#"n"."name"#.into(), - TextRange::new(8.into(), 8.into()), - ), - CompletionAssertion::CompletionTextAndRange( - r#"n"."uid"#.into(), - TextRange::new(8.into(), 8.into()), - ), - CompletionAssertion::CompletionTextAndRange( - r#"pr"."email"#.into(), - TextRange::new(8.into(), 8.into()), - ), - CompletionAssertion::CompletionTextAndRange( - r#"pr"."id"#.into(), - TextRange::new(8.into(), 8.into()), - ), - ], - None, - &pool, - ) - .await; - } - - { - // should suggest pr"."email" - let query = format!( - r#"select "{} from private.users "pr";"#, - QueryWithCursorPosition::cursor_marker() - ); - assert_complete_results( - query.as_str(), - vec![ - CompletionAssertion::CompletionTextAndRange( - r#"pr"."email""#.into(), - TextRange::new(8.into(), 8.into()), - ), - CompletionAssertion::CompletionTextAndRange( - r#"pr"."id""#.into(), - TextRange::new(8.into(), 8.into()), - ), - ], - None, - &pool, - ) - .await; - } - - { - // should suggest email" - let query = format!( - r#"select pr."{} from private.users "pr";"#, - QueryWithCursorPosition::cursor_marker() - ); - assert_complete_results( - query.as_str(), - vec![CompletionAssertion::CompletionTextAndRange( - r#"email""#.into(), - TextRange::new(11.into(), 11.into()), - )], - None, - &pool, - ) - .await; - } - - { - // should suggest email" - let query = format!( - r#"select "pr"."{} from private.users "pr";"#, - QueryWithCursorPosition::cursor_marker() - ); - assert_complete_results( - query.as_str(), - vec![CompletionAssertion::CompletionTextAndRange( - r#"email""#.into(), - TextRange::new(13.into(), 13.into()), - )], - None, - &pool, - ) - .await; - } - - { - // should suggest "n".name - let query = format!( - r#"select {} from names "n";"#, - QueryWithCursorPosition::cursor_marker() - ); - assert_complete_results( - query.as_str(), - vec![CompletionAssertion::CompletionTextAndRange( - r#""n".name"#.into(), - TextRange::new(7.into(), 7.into()), - )], - None, - &pool, - ) + TestCompletionsSuite::new(&pool, Some(setup)) + .with_case( + TestCompletionsCase::new() + .type_sql(r#"select "pr"."email" from private.users "pr""#), + ) + .with_case( + TestCompletionsCase::new() + .inside_static_statement(r#" from private.users "pr""#) + .type_sql(r#"select "email""#), + ) + .with_case( + TestCompletionsCase::new() + .inside_static_statement(r#" from private.users "pr""#) + .type_sql(r#"select pr."email""#), + ) + .with_case( + TestCompletionsCase::new() + .inside_static_statement(r#" from private.users "pr""#) + .type_sql(r#"select "pr"."email""#), + ) + .with_case( + TestCompletionsCase::new() + .inside_static_statement(r#" from private.users "pr""#) + .type_sql(r#"select pr.<1>email"#) + .comment("not quoted here, since the alias isn't."), + ) + .with_case( + TestCompletionsCase::new() + .inside_static_statement( + r#" from private.users "pr" join public.names n on pr.id = n.uid;"#, + ) + .type_sql(r#"select "pr"."email", n.uid"#), + ) + .snapshot("completes_quoted_columns_with_aliases") .await; - } } } diff --git a/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__completes_in_join_on_clause.snap b/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__completes_in_join_on_clause.snap new file mode 100644 index 000000000..ed447c384 --- /dev/null +++ b/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__completes_in_join_on_clause.snap @@ -0,0 +1,104 @@ +--- +source: crates/pgls_completions/src/test_helper.rs +expression: final_snapshot +--- +***Setup*** + +create schema auth; + +create table auth.users ( + uid serial primary key, + name text not null, + email text unique not null +); + +create table auth.posts ( + pid serial primary key, + user_id int not null references auth.users(uid), + title text not null, + content text, + created_at timestamp default now() +); + + +-------------- + +select u.id, auth.posts.content from auth.users u join auth.posts p on | +**Should prioritize primary keys here.** + +Results: +p.pid - auth.posts.pid (Column) +u.uid - auth.users.uid (Column) +p.content - auth.posts.content (Column) +p.created_at - auth.posts.created_at (Column) +u.email - auth.users.email (Column) + +-------------- + +select u.id, auth.posts.content from auth.users u join auth.posts p on p.| +**Should only consider columns from auth.posts here.** + +Results: +pid - auth.posts.pid (Column) +content - auth.posts.content (Column) +created_at - auth.posts.created_at (Column) +title - auth.posts.title (Column) +user_id - auth.posts.user_id (Column) + +-------------- + +select u.id, auth.posts.content from auth.users u join auth.posts p on p.u| +**Should only consider columns from auth.posts here.** + +Results: +user_id - auth.posts.user_id (Column) +pid - auth.posts.pid (Column) +content - auth.posts.content (Column) +created_at - auth.posts.created_at (Column) +title - auth.posts.title (Column) + +-------------- + +select u.id, auth.posts.content from auth.users u join auth.posts p on p.user_id | + +Results: +p.pid - auth.posts.pid (Column) +u.uid - auth.users.uid (Column) +p.content - auth.posts.content (Column) +p.created_at - auth.posts.created_at (Column) +u.email - auth.users.email (Column) + +-------------- + +select u.id, auth.posts.content from auth.users u join auth.posts p on p.user_id = | + +Results: +p.pid - auth.posts.pid (Column) +u.uid - auth.users.uid (Column) +p.content - auth.posts.content (Column) +p.created_at - auth.posts.created_at (Column) +u.email - auth.users.email (Column) + +-------------- + +select u.id, auth.posts.content from auth.users u join auth.posts p on p.user_id = u.| +**Should only consider columns from auth.users here.** + +Results: +uid - auth.users.uid (Column) +email - auth.users.email (Column) +name - auth.users.name (Column) + +-------------- + +select u.id, auth.posts.content from auth.users u join auth.posts p on p.user_id = u.u| +**Should only consider columns from auth.users here.** + +Results: +uid - auth.users.uid (Column) +email - auth.users.email (Column) +name - auth.users.name (Column) + +-------------- + +select u.id, auth.posts.content from auth.users u join auth.posts p on p.user_id = u.uid; | diff --git a/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__completes_quoted_columns.snap b/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__completes_quoted_columns.snap new file mode 100644 index 000000000..2cb3f114e --- /dev/null +++ b/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__completes_quoted_columns.snap @@ -0,0 +1,124 @@ +--- +source: crates/pgls_completions/src/test_helper.rs +expression: final_snapshot +--- +***Setup*** + +create schema if not exists private; + +create table private.users ( + id serial primary key, + email text unique not null, + name text not null, + "quoted_column" text +); + + +-------------- + +s| +select | + +Results: +email - private.users.email (Column) +id - private.users.id (Column) +name - private.users.name (Column) +quoted_column - private.users.quoted_column (Column) +pg_catalog.RI_FKey_cascade_del() - Schema: pg_catalog.RI_FKey_cascade_del (Function) + +-------------- + +select "| + +Results: +email" - private.users.email (Column) +id" - private.users.id (Column) +name" - private.users.name (Column) +quoted_column" - private.users.quoted_column (Column) +abbrev" - pg_catalog.pg_timezone_abbrevs.abbrev (Column) + +-------------- + +select "|" + +Results: +email - private.users.email (Column) +id - private.users.id (Column) +name - private.users.name (Column) +quoted_column - private.users.quoted_column (Column) +abbrev - pg_catalog.pg_timezone_abbrevs.abbrev (Column) + +-------------- + +select "e|" + +Results: +email - private.users.email (Column) +name - private.users.name (Column) +quoted_column - private.users.quoted_column (Column) +id - private.users.id (Column) +elem_count_histogram - pg_catalog.pg_stats.elem_count_histogram (Column) + +-------------- + +select "email" | +select "email" f| +select "email" from | + +Results: +public - public (Schema) +private - private (Schema) +private.users - private.users (Table) +information_schema - information_schema (Schema) +pg_catalog - pg_catalog (Schema) + +-------------- + +select "email" from "| + +Results: +public - public (Schema) +private - private (Schema) +private"."users" - private.users (Table) +information_schema - information_schema (Schema) +pg_catalog - pg_catalog (Schema) + +-------------- + +select "email" from "|" + +Results: +public - public (Schema) +private - private (Schema) +private"."users - private.users (Table) +information_schema"."_pg_foreign_data_wrappers - information_schema._pg_foreign_data_wrappers (Table) +information_schema"."_pg_foreign_servers - information_schema._pg_foreign_servers (Table) + +-------------- + +select "email" from "p|" + +Results: +public - public (Schema) +private - private (Schema) +private"."users - private.users (Table) +information_schema"."parameters - information_schema.parameters (Table) +pg_catalog"."pg_aggregate - pg_catalog.pg_aggregate (Table) + +-------------- + +select "email" from "private".| + +Results: +users - private.users (Table) + +-------------- + +select "email" from "private".u| + +Results: +users - private.users (Table) + +-------------- + +select "email" from "private".users; | diff --git a/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__completes_quoted_columns_with_aliases.snap b/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__completes_quoted_columns_with_aliases.snap new file mode 100644 index 000000000..53a0d60c6 --- /dev/null +++ b/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__completes_quoted_columns_with_aliases.snap @@ -0,0 +1,481 @@ +--- +source: crates/pgls_completions/src/test_helper.rs +expression: final_snapshot +--- +***Setup*** + +create schema if not exists private; + +create table private.users ( + id serial primary key, + email text unique not null, + name text not null, + "quoted_column" text +); + +create table public.names ( + uid serial references private.users(id), + name text +); + + +-------------- + +***Case 1:*** + +s| +select | + +Results: +name - public.names.name (Column) +uid - public.names.uid (Column) +email - private.users.email (Column) +id - private.users.id (Column) +name - private.users.name (Column) + +-------------- + +select "| + +Results: +name" - public.names.name (Column) +uid" - public.names.uid (Column) +email" - private.users.email (Column) +id" - private.users.id (Column) +name" - private.users.name (Column) + +-------------- + +select "|" + +Results: +name - public.names.name (Column) +uid - public.names.uid (Column) +email - private.users.email (Column) +id - private.users.id (Column) +name - private.users.name (Column) + +-------------- + +select "p|" + +Results: +name - public.names.name (Column) +uid - public.names.uid (Column) +email - private.users.email (Column) +id - private.users.id (Column) +name - private.users.name (Column) + +-------------- + +select "pr".| +select "pr"."| +select "pr"."|" +select "pr"."e|" +select "pr"."email" | +select "pr"."email" f| +select "pr"."email" from | + +Results: +public - public (Schema) +private - private (Schema) +names - public.names (Table) +private.users - private.users (Table) +information_schema - information_schema (Schema) + +-------------- + +select "pr"."email" from p| + +Results: +public - public (Schema) +private - private (Schema) +names - public.names (Table) +private.users - private.users (Table) +information_schema.parameters - information_schema.parameters (Table) + +-------------- + +select "pr"."email" from private.| + +Results: +users - private.users (Table) + +-------------- + +select "pr"."email" from private.u| + +Results: +users - private.users (Table) + +-------------- + +select "pr"."email" from private.users | +select "pr"."email" from private.users "| +select "pr"."email" from private.users "|" +select "pr"."email" from private.users "p|" +select "pr"."email" from private.users "pr" | + + + + +***Case 2:*** + +s| from private.users "pr" +select | from private.users "pr" + +Results: +"pr".email - private.users.email (Column) +"pr".id - private.users.id (Column) +"pr".name - private.users.name (Column) +"pr".quoted_column - private.users.quoted_column (Column) +name - public.names.name (Column) + +-------------- + +select "| from private.users "pr" + +Results: +pr"."email" - private.users.email (Column) +pr"."id" - private.users.id (Column) +pr"."name" - private.users.name (Column) +pr"."quoted_column" - private.users.quoted_column (Column) +name" - public.names.name (Column) + +-------------- + +select "|" from private.users "pr" + +Results: +pr"."email - private.users.email (Column) +pr"."id - private.users.id (Column) +pr"."name - private.users.name (Column) +pr"."quoted_column - private.users.quoted_column (Column) +name - public.names.name (Column) + +-------------- + +select "e|" from private.users "pr" + +Results: +pr"."email - private.users.email (Column) +pr"."name - private.users.name (Column) +pr"."quoted_column - private.users.quoted_column (Column) +pr"."id - private.users.id (Column) +name - public.names.name (Column) + +-------------- + +select "email" | from private.users "pr" + + + + +***Case 3:*** + +s| from private.users "pr" +select | from private.users "pr" + +Results: +"pr".email - private.users.email (Column) +"pr".id - private.users.id (Column) +"pr".name - private.users.name (Column) +"pr".quoted_column - private.users.quoted_column (Column) +name - public.names.name (Column) + +-------------- + +select p| from private.users "pr" + +Results: +"pr".email - private.users.email (Column) +"pr".id - private.users.id (Column) +"pr".name - private.users.name (Column) +"pr".quoted_column - private.users.quoted_column (Column) +name - public.names.name (Column) + +-------------- + +select pr.| from private.users "pr" + +Results: +email - private.users.email (Column) +id - private.users.id (Column) +name - private.users.name (Column) +quoted_column - private.users.quoted_column (Column) + +-------------- + +select pr."| from private.users "pr" + +Results: +email" - private.users.email (Column) +id" - private.users.id (Column) +name" - private.users.name (Column) +quoted_column" - private.users.quoted_column (Column) + +-------------- + +select pr."|" from private.users "pr" + +Results: +email - private.users.email (Column) +id - private.users.id (Column) +name - private.users.name (Column) +quoted_column - private.users.quoted_column (Column) + +-------------- + +select pr."e|" from private.users "pr" + +Results: +email - private.users.email (Column) +name - private.users.name (Column) +quoted_column - private.users.quoted_column (Column) +id - private.users.id (Column) + +-------------- + +select pr."email" | from private.users "pr" + + + + +***Case 4:*** + +s| from private.users "pr" +select | from private.users "pr" + +Results: +"pr".email - private.users.email (Column) +"pr".id - private.users.id (Column) +"pr".name - private.users.name (Column) +"pr".quoted_column - private.users.quoted_column (Column) +name - public.names.name (Column) + +-------------- + +select "| from private.users "pr" + +Results: +pr"."email" - private.users.email (Column) +pr"."id" - private.users.id (Column) +pr"."name" - private.users.name (Column) +pr"."quoted_column" - private.users.quoted_column (Column) +name" - public.names.name (Column) + +-------------- + +select "|" from private.users "pr" + +Results: +pr"."email - private.users.email (Column) +pr"."id - private.users.id (Column) +pr"."name - private.users.name (Column) +pr"."quoted_column - private.users.quoted_column (Column) +name - public.names.name (Column) + +-------------- + +select "p|" from private.users "pr" + +Results: +pr"."email - private.users.email (Column) +pr"."id - private.users.id (Column) +pr"."name - private.users.name (Column) +pr"."quoted_column - private.users.quoted_column (Column) +name - public.names.name (Column) + +-------------- + +select "pr".| from private.users "pr" + +Results: +email - private.users.email (Column) +id - private.users.id (Column) +name - private.users.name (Column) +quoted_column - private.users.quoted_column (Column) + +-------------- + +select "pr"."| from private.users "pr" + +Results: +email" - private.users.email (Column) +id" - private.users.id (Column) +name" - private.users.name (Column) +quoted_column" - private.users.quoted_column (Column) + +-------------- + +select "pr"."|" from private.users "pr" + +Results: +email - private.users.email (Column) +id - private.users.id (Column) +name - private.users.name (Column) +quoted_column - private.users.quoted_column (Column) + +-------------- + +select "pr"."e|" from private.users "pr" + +Results: +email - private.users.email (Column) +name - private.users.name (Column) +quoted_column - private.users.quoted_column (Column) +id - private.users.id (Column) + +-------------- + +select "pr"."email" | from private.users "pr" + + + + +***Case 5:*** + +s| from private.users "pr" +select | from private.users "pr" + +Results: +"pr".email - private.users.email (Column) +"pr".id - private.users.id (Column) +"pr".name - private.users.name (Column) +"pr".quoted_column - private.users.quoted_column (Column) +name - public.names.name (Column) + +-------------- + +select p| from private.users "pr" + +Results: +"pr".email - private.users.email (Column) +"pr".id - private.users.id (Column) +"pr".name - private.users.name (Column) +"pr".quoted_column - private.users.quoted_column (Column) +name - public.names.name (Column) + +-------------- + +select pr.| from private.users "pr" +**not quoted here, since the alias isn't.** + +Results: +email - private.users.email (Column) +id - private.users.id (Column) +name - private.users.name (Column) +quoted_column - private.users.quoted_column (Column) + +-------------- + +select pr.e| from private.users "pr" + +Results: +email - private.users.email (Column) +name - private.users.name (Column) +quoted_column - private.users.quoted_column (Column) +id - private.users.id (Column) + +-------------- + +select pr.email | from private.users "pr" + + + + +***Case 6:*** + +s| from private.users "pr" join public.names n on pr.id = n.uid; +select | from private.users "pr" join public.names n on pr.id = n.uid; + +Results: +n.name - public.names.name (Column) +n.uid - public.names.uid (Column) +"pr".email - private.users.email (Column) +"pr".id - private.users.id (Column) +"pr".name - private.users.name (Column) + +-------------- + +select "| from private.users "pr" join public.names n on pr.id = n.uid; + +Results: +n"."name" - public.names.name (Column) +n"."uid" - public.names.uid (Column) +pr"."email" - private.users.email (Column) +pr"."id" - private.users.id (Column) +pr"."name" - private.users.name (Column) + +-------------- + +select "|" from private.users "pr" join public.names n on pr.id = n.uid; + +Results: +n"."name - public.names.name (Column) +n"."uid - public.names.uid (Column) +pr"."email - private.users.email (Column) +pr"."id - private.users.id (Column) +pr"."name - private.users.name (Column) + +-------------- + +select "p|" from private.users "pr" join public.names n on pr.id = n.uid; + +Results: +n"."name - public.names.name (Column) +n"."uid - public.names.uid (Column) +pr"."email - private.users.email (Column) +pr"."id - private.users.id (Column) +pr"."name - private.users.name (Column) + +-------------- + +select "pr".| from private.users "pr" join public.names n on pr.id = n.uid; + +Results: +email - private.users.email (Column) +id - private.users.id (Column) +name - private.users.name (Column) +quoted_column - private.users.quoted_column (Column) + +-------------- + +select "pr".e| from private.users "pr" join public.names n on pr.id = n.uid; + +Results: +email - private.users.email (Column) +name - private.users.name (Column) +quoted_column - private.users.quoted_column (Column) +id - private.users.id (Column) + +-------------- + +select "pr"."email", | from private.users "pr" join public.names n on pr.id = n.uid; + +Results: +n.name - public.names.name (Column) +n.uid - public.names.uid (Column) +"pr".id - private.users.id (Column) +"pr".name - private.users.name (Column) +"pr".quoted_column - private.users.quoted_column (Column) + +-------------- + +select "pr"."email", n.| from private.users "pr" join public.names n on pr.id = n.uid; + +Results: +name - public.names.name (Column) +uid - public.names.uid (Column) + +-------------- + +select "pr"."email", n.u| from private.users "pr" join public.names n on pr.id = n.uid; + +Results: +uid - public.names.uid (Column) +name - public.names.name (Column) + +-------------- + +select "pr"."email", n.uid | from private.users "pr" join public.names n on pr.id = n.uid; diff --git a/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__does_not_complete_cols_in_join_clauses.snap b/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__does_not_complete_cols_in_join_clauses.snap new file mode 100644 index 000000000..95f59dec2 --- /dev/null +++ b/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__does_not_complete_cols_in_join_clauses.snap @@ -0,0 +1,223 @@ +--- +source: crates/pgls_completions/src/test_helper.rs +expression: final_snapshot +--- +***Setup*** + +create schema auth; + +create table auth.users ( + uid serial primary key, + name text not null, + email text unique not null +); + +create table auth.posts ( + pid serial primary key, + user_id int not null references auth.users(uid), + title text not null, + content text, + created_at timestamp default now() +); + + +-------------- + +s| +select | + +Results: +content - auth.posts.content (Column) +created_at - auth.posts.created_at (Column) +email - auth.users.email (Column) +name - auth.users.name (Column) +pid - auth.posts.pid (Column) + +-------------- + +select u.| +select u.u| +select u.uid, | + +Results: +content - auth.posts.content (Column) +created_at - auth.posts.created_at (Column) +email - auth.users.email (Column) +name - auth.users.name (Column) +pid - auth.posts.pid (Column) + +-------------- + +select u.uid, p.| +select u.uid, p.c| +select u.uid, p.content | +select u.uid, p.content f| +select u.uid, p.content from | +**Schema suggestions should be prioritized, since we want to push users to specify them.** + +Results: +public - public (Schema) +auth - auth (Schema) +auth.posts - auth.posts (Table) +auth.users - auth.users (Table) +information_schema - information_schema (Schema) + +-------------- + +select u.uid, p.content from a| +**Schema suggestions should be prioritized, since we want to push users to specify them.** + +Results: +auth - auth (Schema) +public - public (Schema) +auth.posts - auth.posts (Table) +auth.users - auth.users (Table) +information_schema.administrable_role_authorizations - information_schema.administrable_role_authorizations (Table) + +-------------- + +select u.uid, p.content from auth.| +**Here, we shouldn't have schema completions.** + +Results: +posts - auth.posts (Table) +users - auth.users (Table) + +-------------- + +select u.uid, p.content from auth.u| +**Here, we shouldn't have schema completions.** + +Results: +users - auth.users (Table) +posts - auth.posts (Table) + +-------------- + +select u.uid, p.content from auth.users | +select u.uid, p.content from auth.users u | +select u.uid, p.content from auth.users u j| +select u.uid, p.content from auth.users u join | + +Results: +public - public (Schema) +auth - auth (Schema) +auth.posts - auth.posts (Table) +auth.users - auth.users (Table) +information_schema - information_schema (Schema) + +-------------- + +select u.uid, p.content from auth.users u join a| + +Results: +auth - auth (Schema) +public - public (Schema) +auth.posts - auth.posts (Table) +auth.users - auth.users (Table) +information_schema.administrable_role_authorizations - information_schema.administrable_role_authorizations (Table) + +-------------- + +select u.uid, p.content from auth.users u join auth.| + +Results: +posts - auth.posts (Table) +users - auth.users (Table) + +-------------- + +select u.uid, p.content from auth.users u join auth.p| + +Results: +posts - auth.posts (Table) +users - auth.users (Table) + +-------------- + +select u.uid, p.content from auth.users u join auth.posts | +select u.uid, p.content from auth.users u join auth.posts p | +select u.uid, p.content from auth.users u join auth.posts p o| +select u.uid, p.content from auth.users u join auth.posts p on | + +Results: +p.pid - auth.posts.pid (Column) +u.uid - auth.users.uid (Column) +p.content - auth.posts.content (Column) +p.created_at - auth.posts.created_at (Column) +u.email - auth.users.email (Column) + +-------------- + +select u.uid, p.content from auth.users u join auth.posts p on u.| + +Results: +uid - auth.users.uid (Column) +email - auth.users.email (Column) +name - auth.users.name (Column) + +-------------- + +select u.uid, p.content from auth.users u join auth.posts p on u.u| + +Results: +uid - auth.users.uid (Column) +email - auth.users.email (Column) +name - auth.users.name (Column) + +-------------- + +select u.uid, p.content from auth.users u join auth.posts p on u.uid | + +Results: +p.pid - auth.posts.pid (Column) +u.uid - auth.users.uid (Column) +p.content - auth.posts.content (Column) +p.created_at - auth.posts.created_at (Column) +u.email - auth.users.email (Column) + +-------------- + +select u.uid, p.content from auth.users u join auth.posts p on u.uid = | + +Results: +p.pid - auth.posts.pid (Column) +u.uid - auth.users.uid (Column) +p.content - auth.posts.content (Column) +p.created_at - auth.posts.created_at (Column) +u.email - auth.users.email (Column) + +-------------- + +select u.uid, p.content from auth.users u join auth.posts p on u.uid = p.| + +Results: +pid - auth.posts.pid (Column) +content - auth.posts.content (Column) +created_at - auth.posts.created_at (Column) +title - auth.posts.title (Column) +user_id - auth.posts.user_id (Column) + +-------------- + +select u.uid, p.content from auth.users u join auth.posts p on u.uid = p.u| + +Results: +user_id - auth.posts.user_id (Column) +pid - auth.posts.pid (Column) +content - auth.posts.content (Column) +created_at - auth.posts.created_at (Column) +title - auth.posts.title (Column) + +-------------- + +select u.uid, p.content from auth.users u join auth.posts p on u.uid = p.user_id | + +Results: +p.pid - auth.posts.pid (Column) +u.uid - auth.users.uid (Column) +p.content - auth.posts.content (Column) +p.created_at - auth.posts.created_at (Column) +u.email - auth.users.email (Column) + +-------------- diff --git a/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__filters_out_by_aliases_in_join_on.snap b/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__filters_out_by_aliases_in_join_on.snap new file mode 100644 index 000000000..8adcb3f56 --- /dev/null +++ b/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__filters_out_by_aliases_in_join_on.snap @@ -0,0 +1,112 @@ +--- +source: crates/pgls_completions/src/test_helper.rs +expression: final_snapshot +--- +***Setup*** + +create schema auth; + +create table auth.users ( + uid serial primary key, + name text not null, + email text unique not null +); + +create table auth.posts ( + pid serial primary key, + user_id int not null references auth.users(uid), + title text not null, + content text, + created_at timestamp default now() +); + + +-------------- + +select u.id, p.content from auth.users u join auth.posts p | +select u.id, p.content from auth.users u join auth.posts p o| +select u.id, p.content from auth.users u join auth.posts p on | +**Should prefer primary indices here.** + +Results: +p.pid - auth.posts.pid (Column) +u.uid - auth.users.uid (Column) +p.content - auth.posts.content (Column) +p.created_at - auth.posts.created_at (Column) +u.email - auth.users.email (Column) + +-------------- + +select u.id, p.content from auth.users u join auth.posts p on u.| + +Results: +uid - auth.users.uid (Column) +email - auth.users.email (Column) +name - auth.users.name (Column) + +-------------- + +select u.id, p.content from auth.users u join auth.posts p on u.i| + +Results: +uid - auth.users.uid (Column) +email - auth.users.email (Column) +name - auth.users.name (Column) + +-------------- + +select u.id, p.content from auth.users u join auth.posts p on u.id | + +Results: +p.pid - auth.posts.pid (Column) +u.uid - auth.users.uid (Column) +p.content - auth.posts.content (Column) +p.created_at - auth.posts.created_at (Column) +u.email - auth.users.email (Column) + +-------------- + +select u.id, p.content from auth.users u join auth.posts p on u.id = | + +Results: +p.pid - auth.posts.pid (Column) +u.uid - auth.users.uid (Column) +p.content - auth.posts.content (Column) +p.created_at - auth.posts.created_at (Column) +u.email - auth.users.email (Column) + +-------------- + +select u.id, p.content from auth.users u join auth.posts p on u.id = p.| +**We should only get columns from the auth.posts table.** + +Results: +pid - auth.posts.pid (Column) +content - auth.posts.content (Column) +created_at - auth.posts.created_at (Column) +title - auth.posts.title (Column) +user_id - auth.posts.user_id (Column) + +-------------- + +select u.id, p.content from auth.users u join auth.posts p on u.id = p.u| + +Results: +user_id - auth.posts.user_id (Column) +pid - auth.posts.pid (Column) +content - auth.posts.content (Column) +created_at - auth.posts.created_at (Column) +title - auth.posts.title (Column) + +-------------- + +select u.id, p.content from auth.users u join auth.posts p on u.id = p.user_id | + +Results: +p.pid - auth.posts.pid (Column) +u.uid - auth.users.uid (Column) +p.content - auth.posts.content (Column) +p.created_at - auth.posts.created_at (Column) +u.email - auth.users.email (Column) + +-------------- diff --git a/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__filters_out_by_aliases_in_select.snap b/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__filters_out_by_aliases_in_select.snap new file mode 100644 index 000000000..581ff6cdb --- /dev/null +++ b/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__filters_out_by_aliases_in_select.snap @@ -0,0 +1,91 @@ +--- +source: crates/pgls_completions/src/test_helper.rs +expression: final_snapshot +--- +***Setup*** + +create schema auth; + +create table auth.users ( + uid serial primary key, + name text not null, + email text unique not null +); + +create table auth.posts ( + pid serial primary key, + user_id int not null references auth.users(uid), + title text not null, + content text, + created_at timestamp default now() +); + + +-------------- + +s| from auth.users u join auth.posts p on u.id = p.user_id; +select | from auth.users u join auth.posts p on u.id = p.user_id; + +Results: +p.content - auth.posts.content (Column) +p.created_at - auth.posts.created_at (Column) +u.email - auth.users.email (Column) +u.name - auth.users.name (Column) +p.pid - auth.posts.pid (Column) + +-------------- + +select u.| from auth.users u join auth.posts p on u.id = p.user_id; + +Results: +email - auth.users.email (Column) +name - auth.users.name (Column) +uid - auth.users.uid (Column) + +-------------- + +select u.i| from auth.users u join auth.posts p on u.id = p.user_id; + +Results: +email - auth.users.email (Column) +uid - auth.users.uid (Column) +name - auth.users.name (Column) + +-------------- + +select u.id, | from auth.users u join auth.posts p on u.id = p.user_id; + +Results: +p.content - auth.posts.content (Column) +p.created_at - auth.posts.created_at (Column) +u.email - auth.users.email (Column) +u.name - auth.users.name (Column) +p.pid - auth.posts.pid (Column) + +-------------- + +select u.id, p.| from auth.users u join auth.posts p on u.id = p.user_id; +**We should only get columns from the auth.posts table.** + +Results: +content - auth.posts.content (Column) +created_at - auth.posts.created_at (Column) +pid - auth.posts.pid (Column) +title - auth.posts.title (Column) +user_id - auth.posts.user_id (Column) + +-------------- + +select u.id, p.p| from auth.users u join auth.posts p on u.id = p.user_id; +**We should only get columns from the auth.posts table.** + +Results: +pid - auth.posts.pid (Column) +content - auth.posts.content (Column) +created_at - auth.posts.created_at (Column) +title - auth.posts.title (Column) +user_id - auth.posts.user_id (Column) + +-------------- + +select u.id, p.pid | from auth.users u join auth.posts p on u.id = p.user_id; diff --git a/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__handles_nested_queries.snap b/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__handles_nested_queries.snap new file mode 100644 index 000000000..9f0fcc088 --- /dev/null +++ b/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__handles_nested_queries.snap @@ -0,0 +1,185 @@ +--- +source: crates/pgls_completions/src/test_helper.rs +expression: final_snapshot +--- +***Setup*** + +create schema private; + +create table public.users ( + id serial primary key, + name text +); + +create table public.audio_books ( + id serial primary key, + narrator text +); + +create table private.audio_books ( + id serial primary key, + narrator_id text +); + + +-------------- + +select * from ( + | +) as subquery +join public.users u +on u.id = subquery.id; + +Results: +audio_books - public.audio_books (Table) +users - public.users (Table) +private.audio_books - private.audio_books (Table) +information_schema._pg_foreign_data_wrappers - information_schema._pg_foreign_data_wrappers (Table) +information_schema._pg_foreign_servers - information_schema._pg_foreign_servers (Table) + +-------------- + +select * from ( + s| +) as subquery +join public.users u +on u.id = subquery.id; + +Results: +audio_books - public.audio_books (Table) +users - public.users (Table) +private.audio_books - private.audio_books (Table) +information_schema._pg_foreign_data_wrappers - information_schema._pg_foreign_data_wrappers (Table) +information_schema._pg_foreign_servers - information_schema._pg_foreign_servers (Table) + +-------------- + +select * from ( + select | +) as subquery +join public.users u +on u.id = subquery.id; + +Results: +pg_catalog.RI_FKey_cascade_del() - Schema: pg_catalog.RI_FKey_cascade_del (Function) +pg_catalog.RI_FKey_cascade_upd() - Schema: pg_catalog.RI_FKey_cascade_upd (Function) +pg_catalog.RI_FKey_check_ins() - Schema: pg_catalog.RI_FKey_check_ins (Function) +pg_catalog.RI_FKey_check_upd() - Schema: pg_catalog.RI_FKey_check_upd (Function) +pg_catalog.RI_FKey_noaction_del() - Schema: pg_catalog.RI_FKey_noaction_del (Function) + +-------------- + +select * from ( + select i| +) as subquery +join public.users u +on u.id = subquery.id; + +Results: +iclikejoinsel - Schema: pg_catalog.iclikejoinsel (Function) +iclikesel - Schema: pg_catalog.iclikesel (Function) +icnlikejoinsel - Schema: pg_catalog.icnlikejoinsel (Function) +icnlikesel - Schema: pg_catalog.icnlikesel (Function) +icregexeqjoinsel - Schema: pg_catalog.icregexeqjoinsel (Function) + +-------------- + +select * from ( + select id, | +) as subquery +join public.users u +on u.id = subquery.id; +**Should prefer the one from private.audio_audiobooks, since the other tables are out of scope.** + +Results: +pg_catalog.RI_FKey_cascade_del() - Schema: pg_catalog.RI_FKey_cascade_del (Function) +pg_catalog.RI_FKey_cascade_upd() - Schema: pg_catalog.RI_FKey_cascade_upd (Function) +pg_catalog.RI_FKey_check_ins() - Schema: pg_catalog.RI_FKey_check_ins (Function) +pg_catalog.RI_FKey_check_upd() - Schema: pg_catalog.RI_FKey_check_upd (Function) +pg_catalog.RI_FKey_noaction_del() - Schema: pg_catalog.RI_FKey_noaction_del (Function) + +-------------- + +select * from ( + select id, n| +) as subquery +join public.users u +on u.id = subquery.id; +**Should prefer the one from private.audio_audiobooks, since the other tables are out of scope.** + +Results: +name - Schema: pg_catalog.name (Function) +nameconcatoid - Schema: pg_catalog.nameconcatoid (Function) +nameeq - Schema: pg_catalog.nameeq (Function) +nameeqtext - Schema: pg_catalog.nameeqtext (Function) +namege - Schema: pg_catalog.namege (Function) + +-------------- + +select * from ( + select id, narrator_id | +) as subquery +join public.users u +on u.id = subquery.id; +select * from ( + select id, narrator_id f| +) as subquery +join public.users u +on u.id = subquery.id; +select * from ( + select id, narrator_id from | +) as subquery +join public.users u +on u.id = subquery.id; + +Results: +public - public (Schema) +private - private (Schema) +audio_books - public.audio_books (Table) +users - public.users (Table) +private.audio_books - private.audio_books (Table) + +-------------- + +select * from ( + select id, narrator_id from p| +) as subquery +join public.users u +on u.id = subquery.id; + +Results: +public - public (Schema) +private - private (Schema) +audio_books - public.audio_books (Table) +users - public.users (Table) +private.audio_books - private.audio_books (Table) + +-------------- + +select * from ( + select id, narrator_id from private.| +) as subquery +join public.users u +on u.id = subquery.id; + +Results: +audio_books - private.audio_books (Table) + +-------------- + +select * from ( + select id, narrator_id from private.a| +) as subquery +join public.users u +on u.id = subquery.id; + +Results: +audio_books - private.audio_books (Table) + +-------------- + +select * from ( + select id, narrator_id from private.audio_books | +) as subquery +join public.users u +on u.id = subquery.id; diff --git a/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__ignores_cols_in_from_clause.snap b/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__ignores_cols_in_from_clause.snap new file mode 100644 index 000000000..8c8b135a9 --- /dev/null +++ b/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__ignores_cols_in_from_clause.snap @@ -0,0 +1,58 @@ +--- +source: crates/pgls_completions/src/test_helper.rs +expression: final_snapshot +--- +***Setup*** + +create schema private; + +create table private.users ( + id serial primary key, + name text, + address text, + email text, + public boolean +); + + +-------------- + +select * from | +**No column suggestions.** + +Results: +public - public (Schema) +private - private (Schema) +private.users - private.users (Table) +information_schema - information_schema (Schema) +pg_catalog - pg_catalog (Schema) + +-------------- + +select * from p| +**No column suggestions.** + +Results: +public - public (Schema) +private - private (Schema) +private.users - private.users (Table) +information_schema.parameters - information_schema.parameters (Table) +pg_catalog.pg_aggregate - pg_catalog.pg_aggregate (Table) + +-------------- + +select * from private.| + +Results: +users - private.users (Table) + +-------------- + +select * from private.u| + +Results: +users - private.users (Table) + +-------------- + +select * from private.users | diff --git a/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__prefers_columns_of_mentioned_tables.snap b/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__prefers_columns_of_mentioned_tables.snap new file mode 100644 index 000000000..490549e94 --- /dev/null +++ b/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__prefers_columns_of_mentioned_tables.snap @@ -0,0 +1,119 @@ +--- +source: crates/pgls_completions/src/test_helper.rs +expression: final_snapshot +--- +***Setup*** + +create schema private; + +create table private.users ( + id1 serial primary key, + name1 text, + address1 text, + email1 text, + user_settings jsonb +); + +create table public.users ( + id2 serial primary key, + name2 text, + address2 text, + email2 text, + settings jsonb +); + + +-------------- + +***Case 1:*** + +s| from public.users +select | from public.users +**Should suggest address 2 from public table** + +Results: +address2 - public.users.address2 (Column) +email2 - public.users.email2 (Column) +id2 - public.users.id2 (Column) +name2 - public.users.name2 (Column) +settings - public.users.settings (Column) + +-------------- + +select a| from public.users +**Should suggest address 2 from public table** + +Results: +address2 - public.users.address2 (Column) +email2 - public.users.email2 (Column) +name2 - public.users.name2 (Column) +id2 - public.users.id2 (Column) +settings - public.users.settings (Column) + +-------------- + +select address2 | from public.users + + + + +***Case 2:*** + +s| from private.users +select | from private.users +**Should suggest address 1 from private table** + +Results: +address1 - private.users.address1 (Column) +email1 - private.users.email1 (Column) +id1 - private.users.id1 (Column) +name1 - private.users.name1 (Column) +user_settings - private.users.user_settings (Column) + +-------------- + +select a| from private.users +**Should suggest address 1 from private table** + +Results: +address1 - private.users.address1 (Column) +email1 - private.users.email1 (Column) +name1 - private.users.name1 (Column) +id1 - private.users.id1 (Column) +user_settings - private.users.user_settings (Column) + +-------------- + +select address1 | from private.users + + + + +***Case 3:*** + +s| from private.users +select | from private.users +**Should prioritize columns starting with s** + +Results: +address1 - private.users.address1 (Column) +email1 - private.users.email1 (Column) +id1 - private.users.id1 (Column) +name1 - private.users.name1 (Column) +user_settings - private.users.user_settings (Column) + +-------------- + +select s| from private.users +**Should prioritize columns starting with s** + +Results: +user_settings - private.users.user_settings (Column) +address1 - private.users.address1 (Column) +email1 - private.users.email1 (Column) +id1 - private.users.id1 (Column) +name1 - private.users.name1 (Column) + +-------------- + +select settings | from private.users diff --git a/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__prefers_not_mentioned_columns.snap b/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__prefers_not_mentioned_columns.snap new file mode 100644 index 000000000..cfd1f380a --- /dev/null +++ b/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__prefers_not_mentioned_columns.snap @@ -0,0 +1,335 @@ +--- +source: crates/pgls_completions/src/test_helper.rs +expression: final_snapshot +--- +***Setup*** + +create schema auth; + +create table public.one ( + id serial primary key, + a text, + b text, + z text +); + +create table public.two ( + id serial primary key, + c text, + d text, + e text +); + + +-------------- + +***Case 1:*** + +s| from public.one o join public.two on o.id = t.id; +select | from public.one o join public.two on o.id = t.id; + +Results: +o.a - public.one.a (Column) +o.b - public.one.b (Column) +c - public.two.c (Column) +d - public.two.d (Column) +e - public.two.e (Column) + +-------------- + +select o.| from public.one o join public.two on o.id = t.id; + +Results: +a - public.one.a (Column) +b - public.one.b (Column) +id - public.one.id (Column) +z - public.one.z (Column) + +-------------- + +select o.i| from public.one o join public.two on o.id = t.id; + +Results: +id - public.one.id (Column) +a - public.one.a (Column) +b - public.one.b (Column) +z - public.one.z (Column) + +-------------- + +select o.id, | from public.one o join public.two on o.id = t.id; + +Results: +o.a - public.one.a (Column) +o.b - public.one.b (Column) +c - public.two.c (Column) +d - public.two.d (Column) +e - public.two.e (Column) + +-------------- + +select o.id, a| from public.one o join public.two on o.id = t.id; + +Results: +o.a - public.one.a (Column) +o.b - public.one.b (Column) +c - public.two.c (Column) +d - public.two.d (Column) +e - public.two.e (Column) + +-------------- + +select o.id, a, | from public.one o join public.two on o.id = t.id; +**Should have low priority for `a`, since it's already mentioned.** + +Results: +o.b - public.one.b (Column) +c - public.two.c (Column) +d - public.two.d (Column) +e - public.two.e (Column) +id - public.two.id (Column) + +-------------- + +select o.id, a, b| from public.one o join public.two on o.id = t.id; + +Results: +o.b - public.one.b (Column) +c - public.two.c (Column) +d - public.two.d (Column) +e - public.two.e (Column) +id - public.two.id (Column) + +-------------- + +select o.id, a, b, | from public.one o join public.two on o.id = t.id; + +Results: +c - public.two.c (Column) +d - public.two.d (Column) +e - public.two.e (Column) +id - public.two.id (Column) +o.z - public.one.z (Column) + +-------------- + +select o.id, a, b, c| from public.one o join public.two on o.id = t.id; + +Results: +c - public.two.c (Column) +d - public.two.d (Column) +e - public.two.e (Column) +id - public.two.id (Column) +o.z - public.one.z (Column) + +-------------- + +select o.id, a, b, c, | from public.one o join public.two on o.id = t.id; + +Results: +d - public.two.d (Column) +e - public.two.e (Column) +id - public.two.id (Column) +o.z - public.one.z (Column) +o.a - public.one.a (Column) + +-------------- + +select o.id, a, b, c, d| from public.one o join public.two on o.id = t.id; + +Results: +id - public.two.id (Column) +d - public.two.d (Column) +e - public.two.e (Column) +o.z - public.one.z (Column) +o.id - public.one.id (Column) + +-------------- + +select o.id, a, b, c, d, | from public.one o join public.two on o.id = t.id; + +Results: +e - public.two.e (Column) +id - public.two.id (Column) +o.z - public.one.z (Column) +o.a - public.one.a (Column) +o.b - public.one.b (Column) + +-------------- + +select o.id, a, b, c, d, e| from public.one o join public.two on o.id = t.id; + +Results: +e - public.two.e (Column) +id - public.two.id (Column) +o.z - public.one.z (Column) +o.a - public.one.a (Column) +o.b - public.one.b (Column) + +-------------- + +select o.id, a, b, c, d, e, | from public.one o join public.two on o.id = t.id; +**Should have high priority of id of table two, but not one, since it's already mentioned.** + +Results: +id - public.two.id (Column) +o.z - public.one.z (Column) +o.a - public.one.a (Column) +o.b - public.one.b (Column) +c - public.two.c (Column) + +-------------- + +select o.id, a, b, c, d, e, z | from public.one o join public.two on o.id = t.id; + + + + +***Case 2:*** + +s| from public.one o join public.two on o.id = t.id; +select | from public.one o join public.two on o.id = t.id; + +Results: +o.a - public.one.a (Column) +o.b - public.one.b (Column) +c - public.two.c (Column) +d - public.two.d (Column) +e - public.two.e (Column) + +-------------- + +select i| from public.one o join public.two on o.id = t.id; + +Results: +o.id - public.one.id (Column) +o.a - public.one.a (Column) +o.b - public.one.b (Column) +c - public.two.c (Column) +d - public.two.d (Column) + +-------------- + +select id, | from public.one o join public.two on o.id = t.id; + +Results: +o.a - public.one.a (Column) +o.b - public.one.b (Column) +c - public.two.c (Column) +d - public.two.d (Column) +e - public.two.e (Column) + +-------------- + +select id, a| from public.one o join public.two on o.id = t.id; + +Results: +o.a - public.one.a (Column) +o.b - public.one.b (Column) +c - public.two.c (Column) +d - public.two.d (Column) +e - public.two.e (Column) + +-------------- + +select id, a, | from public.one o join public.two on o.id = t.id; + +Results: +o.b - public.one.b (Column) +c - public.two.c (Column) +d - public.two.d (Column) +e - public.two.e (Column) +o.z - public.one.z (Column) + +-------------- + +select id, a, b| from public.one o join public.two on o.id = t.id; + +Results: +o.b - public.one.b (Column) +c - public.two.c (Column) +d - public.two.d (Column) +e - public.two.e (Column) +o.z - public.one.z (Column) + +-------------- + +select id, a, b, | from public.one o join public.two on o.id = t.id; + +Results: +c - public.two.c (Column) +d - public.two.d (Column) +e - public.two.e (Column) +o.z - public.one.z (Column) +o.a - public.one.a (Column) + +-------------- + +select id, a, b, c| from public.one o join public.two on o.id = t.id; + +Results: +c - public.two.c (Column) +d - public.two.d (Column) +e - public.two.e (Column) +o.z - public.one.z (Column) +o.a - public.one.a (Column) + +-------------- + +select id, a, b, c, | from public.one o join public.two on o.id = t.id; + +Results: +d - public.two.d (Column) +e - public.two.e (Column) +o.z - public.one.z (Column) +o.a - public.one.a (Column) +o.b - public.one.b (Column) + +-------------- + +select id, a, b, c, d| from public.one o join public.two on o.id = t.id; + +Results: +d - public.two.d (Column) +e - public.two.e (Column) +o.z - public.one.z (Column) +o.id - public.one.id (Column) +o.a - public.one.a (Column) + +-------------- + +select id, a, b, c, d, | from public.one o join public.two on o.id = t.id; + +Results: +e - public.two.e (Column) +o.z - public.one.z (Column) +o.a - public.one.a (Column) +o.b - public.one.b (Column) +c - public.two.c (Column) + +-------------- + +select id, a, b, c, d, e| from public.one o join public.two on o.id = t.id; + +Results: +e - public.two.e (Column) +o.z - public.one.z (Column) +o.a - public.one.a (Column) +o.b - public.one.b (Column) +c - public.two.c (Column) + +-------------- + +select id, a, b, c, d, e, | from public.one o join public.two on o.id = t.id; +**`id` could be from both tables, so both priorities are lowered.** + +Results: +o.z - public.one.z (Column) +o.a - public.one.a (Column) +o.b - public.one.b (Column) +c - public.two.c (Column) +d - public.two.d (Column) + +-------------- + +select id, a, b, c, d, e, z | from public.one o join public.two on o.id = t.id; diff --git a/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__shows_multiple_columns_if_no_relation_specified.snap b/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__shows_multiple_columns_if_no_relation_specified.snap new file mode 100644 index 000000000..71823a4b6 --- /dev/null +++ b/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__shows_multiple_columns_if_no_relation_specified.snap @@ -0,0 +1,52 @@ +--- +source: crates/pgls_completions/src/test_helper.rs +expression: final_snapshot +--- +***Setup*** + +create schema private; + +create table public.users ( + id serial primary key, + name text +); + +create table public.audio_books ( + id serial primary key, + narrator text +); + +create table private.audio_books ( + id serial primary key, + narrator_id text +); + + +-------------- + +s| +select | +**Should suggest all columns with n first** + +Results: +id - public.audio_books.id (Column) +name - public.users.name (Column) +narrator - public.audio_books.narrator (Column) +id - private.audio_books.id (Column) +narrator_id - private.audio_books.narrator_id (Column) + +-------------- + +select n| +**Should suggest all columns with n first** + +Results: +name - public.users.name (Column) +narrator - public.audio_books.narrator (Column) +narrator_id - private.audio_books.narrator_id (Column) +id - public.audio_books.id (Column) +name - Schema: pg_catalog.name (Function) + +-------------- + +select narrator_id | diff --git a/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__suggests_columns_in_alter_table_and_drop_table.snap b/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__suggests_columns_in_alter_table_and_drop_table.snap new file mode 100644 index 000000000..e8af8cac0 --- /dev/null +++ b/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__suggests_columns_in_alter_table_and_drop_table.snap @@ -0,0 +1,760 @@ +--- +source: crates/pgls_completions/src/test_helper.rs +expression: final_snapshot +--- +***Setup*** + +create table instruments ( + id bigint primary key generated always as identity, + name text not null, + z text, + created_at timestamp with time zone default now() +); + + +-------------- + +***Case 1:*** + +a| +alter | +alter t| +alter table | + +Results: +instruments - public.instruments (Table) +public - public (Schema) +information_schema._pg_foreign_data_wrappers - information_schema._pg_foreign_data_wrappers (Table) +information_schema._pg_foreign_servers - information_schema._pg_foreign_servers (Table) +information_schema._pg_foreign_table_columns - information_schema._pg_foreign_table_columns (Table) + +-------------- + +alter table i| + +Results: +instruments - public.instruments (Table) +public - public (Schema) +information_schema - information_schema (Schema) +information_schema.information_schema_catalog_name - information_schema.information_schema_catalog_name (Table) +pg_catalog.pg_ident_file_mappings - pg_catalog.pg_ident_file_mappings (Table) + +-------------- + +alter table instruments | +alter table instruments d| +alter table instruments drop | + +Results: +created_at - public.instruments.created_at (Column) +id - public.instruments.id (Column) +name - public.instruments.name (Column) +z - public.instruments.z (Column) +abbrev - pg_catalog.pg_timezone_abbrevs.abbrev (Column) + +-------------- + +alter table instruments drop c| + +Results: +created_at - public.instruments.created_at (Column) +id - public.instruments.id (Column) +name - public.instruments.name (Column) +z - public.instruments.z (Column) +cache_size - pg_catalog.pg_sequences.cache_size (Column) + +-------------- + +alter table instruments drop column | + +Results: +created_at - public.instruments.created_at (Column) +id - public.instruments.id (Column) +name - public.instruments.name (Column) +z - public.instruments.z (Column) +abbrev - pg_catalog.pg_timezone_abbrevs.abbrev (Column) + +-------------- + +alter table instruments drop column n| + +Results: +name - public.instruments.name (Column) +created_at - public.instruments.created_at (Column) +id - public.instruments.id (Column) +z - public.instruments.z (Column) +n_dead_tup - pg_catalog.pg_stat_all_tables.n_dead_tup (Column) + +-------------- + +alter table instruments drop column name | + + + + +***Case 2:*** + +a| +alter | +alter t| +alter table | + +Results: +instruments - public.instruments (Table) +public - public (Schema) +information_schema._pg_foreign_data_wrappers - information_schema._pg_foreign_data_wrappers (Table) +information_schema._pg_foreign_servers - information_schema._pg_foreign_servers (Table) +information_schema._pg_foreign_table_columns - information_schema._pg_foreign_table_columns (Table) + +-------------- + +alter table i| + +Results: +instruments - public.instruments (Table) +public - public (Schema) +information_schema - information_schema (Schema) +information_schema.information_schema_catalog_name - information_schema.information_schema_catalog_name (Table) +pg_catalog.pg_ident_file_mappings - pg_catalog.pg_ident_file_mappings (Table) + +-------------- + +alter table instruments | +alter table instruments d| +alter table instruments drop | + +Results: +created_at - public.instruments.created_at (Column) +id - public.instruments.id (Column) +name - public.instruments.name (Column) +z - public.instruments.z (Column) +abbrev - pg_catalog.pg_timezone_abbrevs.abbrev (Column) + +-------------- + +alter table instruments drop c| + +Results: +created_at - public.instruments.created_at (Column) +id - public.instruments.id (Column) +name - public.instruments.name (Column) +z - public.instruments.z (Column) +cache_size - pg_catalog.pg_sequences.cache_size (Column) + +-------------- + +alter table instruments drop column | + +Results: +created_at - public.instruments.created_at (Column) +id - public.instruments.id (Column) +name - public.instruments.name (Column) +z - public.instruments.z (Column) +abbrev - pg_catalog.pg_timezone_abbrevs.abbrev (Column) + +-------------- + +alter table instruments drop column n| + +Results: +name - public.instruments.name (Column) +created_at - public.instruments.created_at (Column) +id - public.instruments.id (Column) +z - public.instruments.z (Column) +n_dead_tup - pg_catalog.pg_stat_all_tables.n_dead_tup (Column) + +-------------- + +alter table instruments drop column name | + + + + +***Case 3:*** + +a| +alter | +alter t| +alter table | + +Results: +instruments - public.instruments (Table) +public - public (Schema) +information_schema._pg_foreign_data_wrappers - information_schema._pg_foreign_data_wrappers (Table) +information_schema._pg_foreign_servers - information_schema._pg_foreign_servers (Table) +information_schema._pg_foreign_table_columns - information_schema._pg_foreign_table_columns (Table) + +-------------- + +alter table i| + +Results: +instruments - public.instruments (Table) +public - public (Schema) +information_schema - information_schema (Schema) +information_schema.information_schema_catalog_name - information_schema.information_schema_catalog_name (Table) +pg_catalog.pg_ident_file_mappings - pg_catalog.pg_ident_file_mappings (Table) + +-------------- + +alter table instruments | +alter table instruments d| +alter table instruments drop | + +Results: +created_at - public.instruments.created_at (Column) +id - public.instruments.id (Column) +name - public.instruments.name (Column) +z - public.instruments.z (Column) +abbrev - pg_catalog.pg_timezone_abbrevs.abbrev (Column) + +-------------- + +alter table instruments drop c| + +Results: +created_at - public.instruments.created_at (Column) +id - public.instruments.id (Column) +name - public.instruments.name (Column) +z - public.instruments.z (Column) +cache_size - pg_catalog.pg_sequences.cache_size (Column) + +-------------- + +alter table instruments drop column | + +Results: +created_at - public.instruments.created_at (Column) +id - public.instruments.id (Column) +name - public.instruments.name (Column) +z - public.instruments.z (Column) +abbrev - pg_catalog.pg_timezone_abbrevs.abbrev (Column) + +-------------- + +alter table instruments drop column i| + +Results: +id - public.instruments.id (Column) +created_at - public.instruments.created_at (Column) +name - public.instruments.name (Column) +z - public.instruments.z (Column) +ident - pg_catalog.pg_backend_memory_contexts.ident (Column) + +-------------- + +alter table instruments drop column if | + +Results: +created_at - public.instruments.created_at (Column) +id - public.instruments.id (Column) +name - public.instruments.name (Column) +z - public.instruments.z (Column) +abbrev - pg_catalog.pg_timezone_abbrevs.abbrev (Column) + +-------------- + +alter table instruments drop column if e| + +Results: +created_at - public.instruments.created_at (Column) +name - public.instruments.name (Column) +id - public.instruments.id (Column) +z - public.instruments.z (Column) +elem_count_histogram - pg_catalog.pg_stats.elem_count_histogram (Column) + +-------------- + +alter table instruments drop column if exists | + +Results: +created_at - public.instruments.created_at (Column) +id - public.instruments.id (Column) +name - public.instruments.name (Column) +z - public.instruments.z (Column) +abbrev - pg_catalog.pg_timezone_abbrevs.abbrev (Column) + +-------------- + +alter table instruments drop column if exists n| + +Results: +name - public.instruments.name (Column) +created_at - public.instruments.created_at (Column) +id - public.instruments.id (Column) +z - public.instruments.z (Column) +n_dead_tup - pg_catalog.pg_stat_all_tables.n_dead_tup (Column) + +-------------- + +alter table instruments drop column if exists name | + + + + +***Case 4:*** + +a| +alter | +alter t| +alter table | + +Results: +instruments - public.instruments (Table) +public - public (Schema) +information_schema._pg_foreign_data_wrappers - information_schema._pg_foreign_data_wrappers (Table) +information_schema._pg_foreign_servers - information_schema._pg_foreign_servers (Table) +information_schema._pg_foreign_table_columns - information_schema._pg_foreign_table_columns (Table) + +-------------- + +alter table i| + +Results: +instruments - public.instruments (Table) +public - public (Schema) +information_schema - information_schema (Schema) +information_schema.information_schema_catalog_name - information_schema.information_schema_catalog_name (Table) +pg_catalog.pg_ident_file_mappings - pg_catalog.pg_ident_file_mappings (Table) + +-------------- + +alter table instruments | +alter table instruments a| +alter table instruments alter | + +Results: +created_at - public.instruments.created_at (Column) +id - public.instruments.id (Column) +name - public.instruments.name (Column) +z - public.instruments.z (Column) +abbrev - pg_catalog.pg_timezone_abbrevs.abbrev (Column) + +-------------- + +alter table instruments alter c| + +Results: +created_at - public.instruments.created_at (Column) +id - public.instruments.id (Column) +name - public.instruments.name (Column) +z - public.instruments.z (Column) +cache_size - pg_catalog.pg_sequences.cache_size (Column) + +-------------- + +alter table instruments alter column | + +Results: +created_at - public.instruments.created_at (Column) +id - public.instruments.id (Column) +name - public.instruments.name (Column) +z - public.instruments.z (Column) +abbrev - pg_catalog.pg_timezone_abbrevs.abbrev (Column) + +-------------- + +alter table instruments alter column n| + +Results: +name - public.instruments.name (Column) +created_at - public.instruments.created_at (Column) +id - public.instruments.id (Column) +z - public.instruments.z (Column) +n_dead_tup - pg_catalog.pg_stat_all_tables.n_dead_tup (Column) + +-------------- + +alter table instruments alter column name | +alter table instruments alter column name s| +alter table instruments alter column name set | +alter table instruments alter column name set d| +alter table instruments alter column name set default | + +Results: +created_at - public.instruments.created_at (Column) +id - public.instruments.id (Column) +name - public.instruments.name (Column) +z - public.instruments.z (Column) +abbrev - pg_catalog.pg_timezone_abbrevs.abbrev (Column) + +-------------- + + + + + +***Case 5:*** + +a| +alter | +alter t| +alter table | + +Results: +instruments - public.instruments (Table) +public - public (Schema) +information_schema._pg_foreign_data_wrappers - information_schema._pg_foreign_data_wrappers (Table) +information_schema._pg_foreign_servers - information_schema._pg_foreign_servers (Table) +information_schema._pg_foreign_table_columns - information_schema._pg_foreign_table_columns (Table) + +-------------- + +alter table i| + +Results: +instruments - public.instruments (Table) +public - public (Schema) +information_schema - information_schema (Schema) +information_schema.information_schema_catalog_name - information_schema.information_schema_catalog_name (Table) +pg_catalog.pg_ident_file_mappings - pg_catalog.pg_ident_file_mappings (Table) + +-------------- + +alter table instruments | +alter table instruments a| +alter table instruments alter | + +Results: +created_at - public.instruments.created_at (Column) +id - public.instruments.id (Column) +name - public.instruments.name (Column) +z - public.instruments.z (Column) +abbrev - pg_catalog.pg_timezone_abbrevs.abbrev (Column) + +-------------- + +alter table instruments alter n| + +Results: +name - public.instruments.name (Column) +created_at - public.instruments.created_at (Column) +id - public.instruments.id (Column) +z - public.instruments.z (Column) +n_dead_tup - pg_catalog.pg_stat_all_tables.n_dead_tup (Column) + +-------------- + +alter table instruments alter name | +alter table instruments alter name s| +alter table instruments alter name set | +alter table instruments alter name set d| +alter table instruments alter name set default | + +Results: +created_at - public.instruments.created_at (Column) +id - public.instruments.id (Column) +name - public.instruments.name (Column) +z - public.instruments.z (Column) +abbrev - pg_catalog.pg_timezone_abbrevs.abbrev (Column) + +-------------- + + + + + +***Case 6:*** + +a| +alter | +alter t| +alter table | + +Results: +instruments - public.instruments (Table) +public - public (Schema) +information_schema._pg_foreign_data_wrappers - information_schema._pg_foreign_data_wrappers (Table) +information_schema._pg_foreign_servers - information_schema._pg_foreign_servers (Table) +information_schema._pg_foreign_table_columns - information_schema._pg_foreign_table_columns (Table) + +-------------- + +alter table p| + +Results: +public - public (Schema) +instruments - public.instruments (Table) +information_schema.parameters - information_schema.parameters (Table) +pg_catalog.pg_aggregate - pg_catalog.pg_aggregate (Table) +pg_catalog.pg_am - pg_catalog.pg_am (Table) + +-------------- + +alter table public.| + +Results: +instruments - public.instruments (Table) +_sqlx_migrations - public._sqlx_migrations (Table) + +-------------- + +alter table public.i| + +Results: +instruments - public.instruments (Table) +_sqlx_migrations - public._sqlx_migrations (Table) + +-------------- + +alter table public.instruments | +alter table public.instruments a| +alter table public.instruments alter | + +Results: +created_at - public.instruments.created_at (Column) +id - public.instruments.id (Column) +name - public.instruments.name (Column) +z - public.instruments.z (Column) +abbrev - pg_catalog.pg_timezone_abbrevs.abbrev (Column) + +-------------- + +alter table public.instruments alter c| + +Results: +created_at - public.instruments.created_at (Column) +id - public.instruments.id (Column) +name - public.instruments.name (Column) +z - public.instruments.z (Column) +cache_size - pg_catalog.pg_sequences.cache_size (Column) + +-------------- + +alter table public.instruments alter column | + +Results: +created_at - public.instruments.created_at (Column) +id - public.instruments.id (Column) +name - public.instruments.name (Column) +z - public.instruments.z (Column) +abbrev - pg_catalog.pg_timezone_abbrevs.abbrev (Column) + +-------------- + +alter table public.instruments alter column n| + +Results: +name - public.instruments.name (Column) +created_at - public.instruments.created_at (Column) +id - public.instruments.id (Column) +z - public.instruments.z (Column) +n_dead_tup - pg_catalog.pg_stat_all_tables.n_dead_tup (Column) + +-------------- + +alter table public.instruments alter column name | + + + + +***Case 7:*** + +a| +alter | +alter t| +alter table | + +Results: +instruments - public.instruments (Table) +public - public (Schema) +information_schema._pg_foreign_data_wrappers - information_schema._pg_foreign_data_wrappers (Table) +information_schema._pg_foreign_servers - information_schema._pg_foreign_servers (Table) +information_schema._pg_foreign_table_columns - information_schema._pg_foreign_table_columns (Table) + +-------------- + +alter table i| + +Results: +instruments - public.instruments (Table) +public - public (Schema) +information_schema - information_schema (Schema) +information_schema.information_schema_catalog_name - information_schema.information_schema_catalog_name (Table) +pg_catalog.pg_ident_file_mappings - pg_catalog.pg_ident_file_mappings (Table) + +-------------- + +alter table instruments | +alter table instruments a| +alter table instruments alter | + +Results: +created_at - public.instruments.created_at (Column) +id - public.instruments.id (Column) +name - public.instruments.name (Column) +z - public.instruments.z (Column) +abbrev - pg_catalog.pg_timezone_abbrevs.abbrev (Column) + +-------------- + +alter table instruments alter n| + +Results: +name - public.instruments.name (Column) +created_at - public.instruments.created_at (Column) +id - public.instruments.id (Column) +z - public.instruments.z (Column) +n_dead_tup - pg_catalog.pg_stat_all_tables.n_dead_tup (Column) + +-------------- + +alter table instruments alter name | + + + + +***Case 8:*** + +a| +alter | +alter t| +alter table | + +Results: +instruments - public.instruments (Table) +public - public (Schema) +information_schema._pg_foreign_data_wrappers - information_schema._pg_foreign_data_wrappers (Table) +information_schema._pg_foreign_servers - information_schema._pg_foreign_servers (Table) +information_schema._pg_foreign_table_columns - information_schema._pg_foreign_table_columns (Table) + +-------------- + +alter table i| + +Results: +instruments - public.instruments (Table) +public - public (Schema) +information_schema - information_schema (Schema) +information_schema.information_schema_catalog_name - information_schema.information_schema_catalog_name (Table) +pg_catalog.pg_ident_file_mappings - pg_catalog.pg_ident_file_mappings (Table) + +-------------- + +alter table instruments | +alter table instruments r| +alter table instruments rename | + +Results: +created_at - public.instruments.created_at (Column) +id - public.instruments.id (Column) +name - public.instruments.name (Column) +z - public.instruments.z (Column) +abbrev - pg_catalog.pg_timezone_abbrevs.abbrev (Column) + +-------------- + +alter table instruments rename n| + +Results: +name - public.instruments.name (Column) +created_at - public.instruments.created_at (Column) +id - public.instruments.id (Column) +z - public.instruments.z (Column) +n_dead_tup - pg_catalog.pg_stat_all_tables.n_dead_tup (Column) + +-------------- + +alter table instruments rename name | +alter table instruments rename name t| +alter table instruments rename name to | +alter table instruments rename name to n| +alter table instruments rename name to new_col | + + + + +***Case 9:*** + +a| +alter | +alter t| +alter table | + +Results: +instruments - public.instruments (Table) +public - public (Schema) +information_schema._pg_foreign_data_wrappers - information_schema._pg_foreign_data_wrappers (Table) +information_schema._pg_foreign_servers - information_schema._pg_foreign_servers (Table) +information_schema._pg_foreign_table_columns - information_schema._pg_foreign_table_columns (Table) + +-------------- + +alter table p| + +Results: +public - public (Schema) +instruments - public.instruments (Table) +information_schema.parameters - information_schema.parameters (Table) +pg_catalog.pg_aggregate - pg_catalog.pg_aggregate (Table) +pg_catalog.pg_am - pg_catalog.pg_am (Table) + +-------------- + +alter table public.| + +Results: +instruments - public.instruments (Table) +_sqlx_migrations - public._sqlx_migrations (Table) + +-------------- + +alter table public.i| + +Results: +instruments - public.instruments (Table) +_sqlx_migrations - public._sqlx_migrations (Table) + +-------------- + +alter table public.instruments | +alter table public.instruments r| +alter table public.instruments rename | + +Results: +created_at - public.instruments.created_at (Column) +id - public.instruments.id (Column) +name - public.instruments.name (Column) +z - public.instruments.z (Column) +abbrev - pg_catalog.pg_timezone_abbrevs.abbrev (Column) + +-------------- + +alter table public.instruments rename c| + +Results: +created_at - public.instruments.created_at (Column) +id - public.instruments.id (Column) +name - public.instruments.name (Column) +z - public.instruments.z (Column) +cache_size - pg_catalog.pg_sequences.cache_size (Column) + +-------------- + +alter table public.instruments rename column | + +Results: +created_at - public.instruments.created_at (Column) +id - public.instruments.id (Column) +name - public.instruments.name (Column) +z - public.instruments.z (Column) +abbrev - pg_catalog.pg_timezone_abbrevs.abbrev (Column) + +-------------- + +alter table public.instruments rename column n| + +Results: +name - public.instruments.name (Column) +created_at - public.instruments.created_at (Column) +id - public.instruments.id (Column) +z - public.instruments.z (Column) +n_dead_tup - pg_catalog.pg_stat_all_tables.n_dead_tup (Column) + +-------------- + +alter table public.instruments rename column name | +alter table public.instruments rename column name t| +alter table public.instruments rename column name to | +alter table public.instruments rename column name to n| +alter table public.instruments rename column name to new_col | diff --git a/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__suggests_columns_in_insert_clause.snap b/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__suggests_columns_in_insert_clause.snap new file mode 100644 index 000000000..0d99a891f --- /dev/null +++ b/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__suggests_columns_in_insert_clause.snap @@ -0,0 +1,282 @@ +--- +source: crates/pgls_completions/src/test_helper.rs +expression: final_snapshot +--- +***Setup*** + +create table instruments ( + id bigint primary key generated always as identity, + name text not null, + z text +); + +create table others ( + id serial primary key, + a text, + b text +); + + +-------------- + +***Case 1:*** + +i| +insert | +insert i| +insert into | + +Results: +instruments - public.instruments (Table) +others - public.others (Table) +public - public (Schema) +information_schema._pg_foreign_data_wrappers - information_schema._pg_foreign_data_wrappers (Table) +information_schema._pg_foreign_servers - information_schema._pg_foreign_servers (Table) + +-------------- + +insert into i| + +Results: +instruments - public.instruments (Table) +public - public (Schema) +others - public.others (Table) +information_schema - information_schema (Schema) +information_schema.information_schema_catalog_name - information_schema.information_schema_catalog_name (Table) + +-------------- + +insert into instruments | +insert into instruments (| +insert into instruments (|) + +Results: +id - public.instruments.id (Column) +name - public.instruments.name (Column) +z - public.instruments.z (Column) +a - public.others.a (Column) +b - public.others.b (Column) + +-------------- + +insert into instruments (i|) + +Results: +id - public.instruments.id (Column) +name - public.instruments.name (Column) +z - public.instruments.z (Column) +id - public.others.id (Column) +a - public.others.a (Column) + +-------------- + +insert into instruments (id, |) + +Results: +name - public.instruments.name (Column) +z - public.instruments.z (Column) +id - public.instruments.id (Column) +a - public.others.a (Column) +b - public.others.b (Column) + +-------------- + +insert into instruments (id, n|) + +Results: +name - public.instruments.name (Column) +z - public.instruments.z (Column) +id - public.instruments.id (Column) +a - public.others.a (Column) +b - public.others.b (Column) + +-------------- + +insert into instruments (id, name) | +insert into instruments (id, name) v| +insert into instruments (id, name) values | +insert into instruments (id, name) values (| +insert into instruments (id, name) values (|) + +Results: +z - public.instruments.z (Column) +id - public.instruments.id (Column) +name - public.instruments.name (Column) +a - public.others.a (Column) +b - public.others.b (Column) + +-------------- + +insert into instruments (id, name) values (1|) +insert into instruments (id, name) values (1, |) + +Results: +z - public.instruments.z (Column) +id - public.instruments.id (Column) +name - public.instruments.name (Column) +a - public.others.a (Column) +b - public.others.b (Column) + +-------------- + +insert into instruments (id, name) values (1, '|) +insert into instruments (id, name) values (1, 'my_bass'); | + + + + +***Case 2:*** + +i| +insert | +insert i| +insert into | + +Results: +instruments - public.instruments (Table) +others - public.others (Table) +public - public (Schema) +information_schema._pg_foreign_data_wrappers - information_schema._pg_foreign_data_wrappers (Table) +information_schema._pg_foreign_servers - information_schema._pg_foreign_servers (Table) + +-------------- + +insert into i| + +Results: +instruments - public.instruments (Table) +public - public (Schema) +others - public.others (Table) +information_schema - information_schema (Schema) +information_schema.information_schema_catalog_name - information_schema.information_schema_catalog_name (Table) + +-------------- + +insert into instruments | +insert into instruments (| +insert into instruments (|) + +Results: +id - public.instruments.id (Column) +name - public.instruments.name (Column) +z - public.instruments.z (Column) +a - public.others.a (Column) +b - public.others.b (Column) + +-------------- + +insert into instruments (i|) + +Results: +id - public.instruments.id (Column) +name - public.instruments.name (Column) +z - public.instruments.z (Column) +id - public.others.id (Column) +a - public.others.a (Column) + +-------------- + +insert into instruments ("id", |) + +Results: +id - public.instruments.id (Column) +name - public.instruments.name (Column) +z - public.instruments.z (Column) +a - public.others.a (Column) +b - public.others.b (Column) + +-------------- + +insert into instruments ("id", "|) + +Results: +id" - public.instruments.id (Column) +name" - public.instruments.name (Column) +z" - public.instruments.z (Column) +a" - public.others.a (Column) +b" - public.others.b (Column) + +-------------- + +insert into instruments ("id", "|") + +Results: +id - public.instruments.id (Column) +name - public.instruments.name (Column) +z - public.instruments.z (Column) +a - public.others.a (Column) +b - public.others.b (Column) + +-------------- + +insert into instruments ("id", "n|") + +Results: +name - public.instruments.name (Column) +id - public.instruments.id (Column) +z - public.instruments.z (Column) +a - public.others.a (Column) +b - public.others.b (Column) + +-------------- + +insert into instruments ("id", "name") | +insert into instruments ("id", "name") v| +insert into instruments ("id", "name") values | +insert into instruments ("id", "name") values (| +insert into instruments ("id", "name") values (|) + +Results: +id - public.instruments.id (Column) +name - public.instruments.name (Column) +z - public.instruments.z (Column) +a - public.others.a (Column) +b - public.others.b (Column) + +-------------- + +insert into instruments ("id", "name") values (1|) +insert into instruments ("id", "name") values (1, |) + +Results: +id - public.instruments.id (Column) +name - public.instruments.name (Column) +z - public.instruments.z (Column) +a - public.others.a (Column) +b - public.others.b (Column) + +-------------- + +insert into instruments ("id", "name") values (1, '|) +insert into instruments ("id", "name") values (1, 'my_bass'); | + + + + +***Case 3:*** + +insert into instruments (|, name) values ('my_bass'); + +Results: +id - public.instruments.id (Column) +z - public.instruments.z (Column) +name - public.instruments.name (Column) +a - public.others.a (Column) +b - public.others.b (Column) + +-------------- + +insert into instruments (i|, name) values ('my_bass'); +insert into instruments (id, |, name) values ('my_bass'); +**`name` is already written, so z should be suggested.** + +Results: +z - public.instruments.z (Column) +id - public.instruments.id (Column) +name - public.instruments.name (Column) +a - public.others.a (Column) +b - public.others.b (Column) + +-------------- + +insert into instruments (id, z |, name) values ('my_bass'); diff --git a/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__suggests_columns_in_where_clause.snap b/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__suggests_columns_in_where_clause.snap new file mode 100644 index 000000000..90022579d --- /dev/null +++ b/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__suggests_columns_in_where_clause.snap @@ -0,0 +1,149 @@ +--- +source: crates/pgls_completions/src/test_helper.rs +expression: final_snapshot +--- +***Setup*** + +create table instruments ( + id bigint primary key generated always as identity, + name text not null, + z text, + created_at timestamp with time zone default now() +); + +create table others ( + a text, + b text, + c text +); + + +-------------- + +select name from instruments i join others o on i.z = o.a | + +Results: +i.id - public.instruments.id (Column) +o.a - public.others.a (Column) +o.b - public.others.b (Column) +o.c - public.others.c (Column) +i.created_at - public.instruments.created_at (Column) + +-------------- + +select name from instruments i join others o on i.z = o.a w| + +Results: +i.id - public.instruments.id (Column) +o.a - public.others.a (Column) +o.b - public.others.b (Column) +o.c - public.others.c (Column) +i.created_at - public.instruments.created_at (Column) + +-------------- + +select name from instruments i join others o on i.z = o.a where | + +Results: +o.a - public.others.a (Column) +o.b - public.others.b (Column) +o.c - public.others.c (Column) +i.created_at - public.instruments.created_at (Column) +i.id - public.instruments.id (Column) + +-------------- + +select name from instruments i join others o on i.z = o.a where o.| +**should respect alias speciifcation** + +Results: +a - public.others.a (Column) +b - public.others.b (Column) +c - public.others.c (Column) + +-------------- + +select name from instruments i join others o on i.z = o.a where o.a | + +Results: +o.a - public.others.a (Column) +o.b - public.others.b (Column) +o.c - public.others.c (Column) +i.created_at - public.instruments.created_at (Column) +i.id - public.instruments.id (Column) + +-------------- + +select name from instruments i join others o on i.z = o.a where o.a = | +**should not prioritize suggest columns or schemas (right side of binary expression)** + +Results: +instruments - public.instruments (Table) +others - public.others (Table) +pg_catalog.RI_FKey_cascade_del() - Schema: pg_catalog.RI_FKey_cascade_del (Function) +pg_catalog.RI_FKey_cascade_upd() - Schema: pg_catalog.RI_FKey_cascade_upd (Function) +pg_catalog.RI_FKey_check_ins() - Schema: pg_catalog.RI_FKey_check_ins (Function) + +-------------- + +select name from instruments i join others o on i.z = o.a where o.a = i.| +select name from instruments i join others o on i.z = o.a where o.a = i.z | + +Results: +instruments - public.instruments (Table) +others - public.others (Table) +pg_catalog.RI_FKey_cascade_del() - Schema: pg_catalog.RI_FKey_cascade_del (Function) +pg_catalog.RI_FKey_cascade_upd() - Schema: pg_catalog.RI_FKey_cascade_upd (Function) +pg_catalog.RI_FKey_check_ins() - Schema: pg_catalog.RI_FKey_check_ins (Function) + +-------------- + +select name from instruments i join others o on i.z = o.a where o.a = i.z a| + +Results: +instruments - public.instruments (Table) +others - public.others (Table) +abbrev - Schema: pg_catalog.abbrev (Function) +abs - Schema: pg_catalog.abs (Function) +aclcontains - Schema: pg_catalog.aclcontains (Function) + +-------------- + +select name from instruments i join others o on i.z = o.a where o.a = i.z and | +**should prioritize columns that aren't already mentioned** + +Results: +o.b - public.others.b (Column) +o.c - public.others.c (Column) +i.created_at - public.instruments.created_at (Column) +i.id - public.instruments.id (Column) +i.name - public.instruments.name (Column) + +-------------- + +select name from instruments i join others o on i.z = o.a where o.a = i.z and i.| +select name from instruments i join others o on i.z = o.a where o.a = i.z and i.i| +select name from instruments i join others o on i.z = o.a where o.a = i.z and i.id | + +Results: +instruments - public.instruments (Table) +others - public.others (Table) +pg_catalog.RI_FKey_cascade_del() - Schema: pg_catalog.RI_FKey_cascade_del (Function) +pg_catalog.RI_FKey_cascade_upd() - Schema: pg_catalog.RI_FKey_cascade_upd (Function) +pg_catalog.RI_FKey_check_ins() - Schema: pg_catalog.RI_FKey_check_ins (Function) + +-------------- + +select name from instruments i join others o on i.z = o.a where o.a = i.z and i.id > | + +Results: +instruments - public.instruments (Table) +others - public.others (Table) +pg_catalog.RI_FKey_cascade_del() - Schema: pg_catalog.RI_FKey_cascade_del (Function) +pg_catalog.RI_FKey_cascade_upd() - Schema: pg_catalog.RI_FKey_cascade_upd (Function) +pg_catalog.RI_FKey_check_ins() - Schema: pg_catalog.RI_FKey_check_ins (Function) + +-------------- + +select name from instruments i join others o on i.z = o.a where o.a = i.z and i.id > 5| +select name from instruments i join others o on i.z = o.a where o.a = i.z and i.id > 5; | diff --git a/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__suggests_columns_policy_using_clause.snap b/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__suggests_columns_policy_using_clause.snap new file mode 100644 index 000000000..1f04bb0f8 --- /dev/null +++ b/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__suggests_columns_policy_using_clause.snap @@ -0,0 +1,148 @@ +--- +source: crates/pgls_completions/src/test_helper.rs +expression: final_snapshot +--- +***Setup*** + +create table instruments ( + id bigint primary key generated always as identity, + name text not null, + z text, + created_at timestamp with time zone default now() +); + + +-------------- + +***Case 1:*** + +create policy "my_pol" on public.instruments for select using (|) + +Results: +created_at - public.instruments.created_at (Column) +id - public.instruments.id (Column) +name - public.instruments.name (Column) +z - public.instruments.z (Column) +abbrev - pg_catalog.pg_timezone_abbrevs.abbrev (Column) + +-------------- + +create policy "my_pol" on public.instruments for select using (i|) + +Results: +id - public.instruments.id (Column) +created_at - public.instruments.created_at (Column) +name - public.instruments.name (Column) +z - public.instruments.z (Column) +ident - pg_catalog.pg_backend_memory_contexts.ident (Column) + +-------------- + +create policy "my_pol" on public.instruments for select using (id |) +create policy "my_pol" on public.instruments for select using (id = |) + +Results: +created_at - public.instruments.created_at (Column) +id - public.instruments.id (Column) +name - public.instruments.name (Column) +z - public.instruments.z (Column) +abbrev - pg_catalog.pg_timezone_abbrevs.abbrev (Column) + +-------------- + +create policy "my_pol" on public.instruments for select using (id = 1 |) +create policy "my_pol" on public.instruments for select using (id = 1 a|) +create policy "my_pol" on public.instruments for select using (id = 1 and |) + +Results: +created_at - public.instruments.created_at (Column) +id - public.instruments.id (Column) +name - public.instruments.name (Column) +z - public.instruments.z (Column) +abbrev - pg_catalog.pg_timezone_abbrevs.abbrev (Column) + +-------------- + +create policy "my_pol" on public.instruments for select using (id = 1 and c|) + +Results: +created_at - public.instruments.created_at (Column) +id - public.instruments.id (Column) +name - public.instruments.name (Column) +z - public.instruments.z (Column) +cache_size - pg_catalog.pg_sequences.cache_size (Column) + +-------------- + +create policy "my_pol" on public.instruments for select using (id = 1 and created_at |) +create policy "my_pol" on public.instruments for select using (id = 1 and created_at > |) +create policy "my_pol" on public.instruments for select using (id = 1 and created_at > '|) +create policy "my_pol" on public.instruments for select using (id = 1 and created_at > '2025-01-01' |) + + + + +***Case 2:*** + +create policy "my_pol" on public.instruments for insert with check (|) + +Results: +created_at - public.instruments.created_at (Column) +id - public.instruments.id (Column) +name - public.instruments.name (Column) +z - public.instruments.z (Column) +abbrev - pg_catalog.pg_timezone_abbrevs.abbrev (Column) + +-------------- + +create policy "my_pol" on public.instruments for insert with check (i|) + +Results: +id - public.instruments.id (Column) +created_at - public.instruments.created_at (Column) +name - public.instruments.name (Column) +z - public.instruments.z (Column) +ident - pg_catalog.pg_backend_memory_contexts.ident (Column) + +-------------- + +create policy "my_pol" on public.instruments for insert with check (id |) +create policy "my_pol" on public.instruments for insert with check (id = |) + +Results: +created_at - public.instruments.created_at (Column) +id - public.instruments.id (Column) +name - public.instruments.name (Column) +z - public.instruments.z (Column) +abbrev - pg_catalog.pg_timezone_abbrevs.abbrev (Column) + +-------------- + +create policy "my_pol" on public.instruments for insert with check (id = 1 |) +create policy "my_pol" on public.instruments for insert with check (id = 1 a|) +create policy "my_pol" on public.instruments for insert with check (id = 1 and |) + +Results: +created_at - public.instruments.created_at (Column) +id - public.instruments.id (Column) +name - public.instruments.name (Column) +z - public.instruments.z (Column) +abbrev - pg_catalog.pg_timezone_abbrevs.abbrev (Column) + +-------------- + +create policy "my_pol" on public.instruments for insert with check (id = 1 and c|) + +Results: +created_at - public.instruments.created_at (Column) +id - public.instruments.id (Column) +name - public.instruments.name (Column) +z - public.instruments.z (Column) +cache_size - pg_catalog.pg_sequences.cache_size (Column) + +-------------- + +create policy "my_pol" on public.instruments for insert with check (id = 1 and created_at |) +create policy "my_pol" on public.instruments for insert with check (id = 1 and created_at > |) +create policy "my_pol" on public.instruments for insert with check (id = 1 and created_at > '|) +create policy "my_pol" on public.instruments for insert with check (id = 1 and created_at > '2025-01-01' |) diff --git a/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__suggests_relevant_columns_without_letters.snap b/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__suggests_relevant_columns_without_letters.snap new file mode 100644 index 000000000..12dcca6a4 --- /dev/null +++ b/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__suggests_relevant_columns_without_letters.snap @@ -0,0 +1,64 @@ +--- +source: crates/pgls_completions/src/test_helper.rs +expression: final_snapshot +--- +***Setup*** + +create table users ( + id serial primary key, + name text, + address text, + email text +); + + +-------------- + +s| +select | + +Results: +address - public.users.address (Column) +email - public.users.email (Column) +id - public.users.id (Column) +name - public.users.name (Column) +pg_catalog.RI_FKey_cascade_del() - Schema: pg_catalog.RI_FKey_cascade_del (Function) + +-------------- + +select n| + +Results: +name - public.users.name (Column) +address - public.users.address (Column) +email - public.users.email (Column) +id - public.users.id (Column) +name - Schema: pg_catalog.name (Function) + +-------------- + +select name | +select name f| +select name from | + +Results: +public - public (Schema) +users - public.users (Table) +information_schema - information_schema (Schema) +pg_catalog - pg_catalog (Schema) +pg_toast - pg_toast (Schema) + +-------------- + +select name from u| + +Results: +users - public.users (Table) +public - public (Schema) +information_schema.udt_privileges - information_schema.udt_privileges (Table) +information_schema.usage_privileges - information_schema.usage_privileges (Table) +information_schema.user_defined_types - information_schema.user_defined_types (Table) + +-------------- + +select name from users | diff --git a/crates/pgls_completions/src/test_helper.rs b/crates/pgls_completions/src/test_helper.rs index cb27353a5..7613f2815 100644 --- a/crates/pgls_completions/src/test_helper.rs +++ b/crates/pgls_completions/src/test_helper.rs @@ -1,7 +1,11 @@ +use insta::assert_snapshot; use pgls_schema_cache::SchemaCache; use pgls_test_utils::QueryWithCursorPosition; use pgls_text_size::TextRange; +use regex::Regex; use sqlx::{Executor, PgPool}; +use std::{collections::HashMap, fmt::Write, sync::OnceLock}; +use unindent::unindent; use crate::{CompletionItem, CompletionItemKind, CompletionParams, complete}; @@ -75,8 +79,10 @@ pub(crate) fn get_test_params<'a>( pub(crate) enum CompletionAssertion { Label(String), LabelAndKind(String, CompletionItemKind), + #[allow(unused)] LabelAndDesc(String, String), LabelNotExists(String), + #[allow(unused)] KindNotExists(CompletionItemKind), CompletionTextAndRange(String, TextRange), } @@ -201,3 +207,439 @@ pub(crate) async fn assert_no_complete_results(query: &str, setup: Option<&str>, assert_eq!(items.len(), 0) } + +enum ChunkToType { + WithCompletions(String), + #[allow(unused)] + WithoutCompletions(String), +} + +static COMMENT_RE: OnceLock = OnceLock::new(); + +fn comment_regex() -> &'static Regex { + COMMENT_RE.get_or_init(|| Regex::new(r"<\d+>").unwrap()) +} + +pub(crate) struct TestCompletionsCase { + tokens_to_type: Vec, + surrounding_statement: String, + comments: std::collections::HashMap, + comment_position: usize, +} + +impl TestCompletionsCase { + pub(crate) fn new() -> Self { + Self { + tokens_to_type: Vec::new(), + surrounding_statement: String::new(), + comments: HashMap::new(), + comment_position: 0, + } + } + + pub(crate) fn inside_static_statement(mut self, it: &str) -> Self { + assert!(it.contains("")); + self.surrounding_statement = unindent(it); + self + } + + pub(crate) fn type_sql(mut self, it: &str) -> Self { + self.tokens_to_type + .push(ChunkToType::WithCompletions(it.trim().to_string())); + self + } + + #[allow(unused)] + pub(crate) fn type_without_completions(mut self, it: &str) -> Self { + self.tokens_to_type + .push(ChunkToType::WithoutCompletions(it.trim().to_string())); + self + } + + pub(crate) fn comment(mut self, comment: &str) -> Self { + self.comment_position += 1; + self.comments + .insert(self.comment_position, comment.to_string()); + self + } + + async fn generate_snapshot( + &self, + schema_cache: &SchemaCache, + parser: &mut tree_sitter::Parser, + ) -> String { + let mut stmt_parts = self.surrounding_statement.split(""); + let mut pre_sql = stmt_parts.next().unwrap().to_string(); + let post_sql = stmt_parts.next().unwrap_or("").to_string(); + + let mut snapshot_result = String::new(); + + for chunk in &self.tokens_to_type { + match chunk { + ChunkToType::WithCompletions(sql) => { + let whitespace_count = sql.chars().filter(|c| c.is_ascii_whitespace()).count(); + let whitespace_split = sql.split_ascii_whitespace().enumerate(); + + let mut should_close_with_paren = false; + + for (whitespace_idx, token) in whitespace_split { + let dot_count = token.chars().filter(|c| *c == '.').count(); + let dotted_split = token.split(".").enumerate(); + + for (dot_idx, og_part) in dotted_split { + let comment_indicator = comment_regex().find(og_part); + let comment = comment_indicator.and_then(|n| { + let num = n.as_str().replace("<", "").replace(">", ""); + let num: usize = num + .parse() + .expect("Regex should only find matches with numbers"); + self.comments.get(&num).map(|s| s.as_str()) + }); + + let part_without_comments = comment_regex().replace_all(og_part, ""); + + let starts_with_paren = part_without_comments.starts_with('('); + let ends_with_paren = part_without_comments.ends_with(')'); + + let part_without_parens = if starts_with_paren || ends_with_paren { + // we only want to sanitize when the token either starts or ends; that helps + // catch end tokens like `('something');` + part_without_comments.replace(['(', ')'], "") + } else { + part_without_comments.to_string() + }; + + let is_inside_quotes = part_without_parens.starts_with('"') + && part_without_parens.ends_with('"'); + + let part_without_quotes = part_without_parens.replace('"', ""); + + if !pre_sql.is_empty() { + let query = format!( + "{}{}{}{}{}", + pre_sql, + if dot_idx <= dot_count { "" } else { "." }, + QueryWithCursorPosition::cursor_marker(), + if should_close_with_paren { ")" } else { "" }, + post_sql, + ); + + self.completions_snapshot( + query.into(), + &mut snapshot_result, + schema_cache, + parser, + comment, + ) + .await; + } + + // try ` (|` and ` (|)` + if starts_with_paren { + let query1 = format!( + "{}{}({}{}", + pre_sql, + if dot_idx <= dot_count { "" } else { "." }, + QueryWithCursorPosition::cursor_marker(), + post_sql, + ); + + self.completions_snapshot( + query1.into(), + &mut snapshot_result, + schema_cache, + parser, + comment, + ) + .await; + + let query2 = format!( + "{}{}({}){}", + pre_sql, + if dot_idx <= dot_count { "" } else { "." }, + QueryWithCursorPosition::cursor_marker(), + post_sql, + ); + + self.completions_snapshot( + query2.into(), + &mut snapshot_result, + schema_cache, + parser, + comment, + ) + .await; + + pre_sql.push('('); + should_close_with_paren = true; + } + + // try ` "|` and ` "|"` + if is_inside_quotes { + let query1 = format!( + "{}{}\"{}{}{}", + pre_sql, + if dot_idx <= dot_count { "" } else { "." }, + QueryWithCursorPosition::cursor_marker(), + if should_close_with_paren { ")" } else { "" }, + post_sql, + ); + + self.completions_snapshot( + query1.into(), + &mut snapshot_result, + schema_cache, + parser, + comment, + ) + .await; + + let query2 = format!( + "{}{}\"{}\"{}{}", + pre_sql, + if dot_idx <= dot_count { "" } else { "." }, + QueryWithCursorPosition::cursor_marker(), + if should_close_with_paren { ")" } else { "" }, + post_sql, + ); + + self.completions_snapshot( + query2.into(), + &mut snapshot_result, + schema_cache, + parser, + comment, + ) + .await; + } + + if part_without_quotes.len() > 1 { + let first_token = &part_without_quotes[..1]; + + let query = format!( + "{}{}{}{}{}{}", + pre_sql, + if is_inside_quotes { + format!(r#""{first_token}"#) + } else { + first_token.to_string() + }, + QueryWithCursorPosition::cursor_marker(), + if is_inside_quotes { r#"""# } else { "" }, + if should_close_with_paren { ")" } else { "" }, + post_sql, + ); + + self.completions_snapshot( + query.into(), + &mut snapshot_result, + schema_cache, + parser, + if comment_indicator + .is_some_and(|txt| og_part.starts_with(txt.as_str())) + { + None + } else { + comment + }, + ) + .await; + }; + + if whitespace_idx == whitespace_count && dot_idx == dot_count { + let query = format!( + "{}{} {}{}", + pre_sql, + if is_inside_quotes { + format!(r#""{}""#, part_without_quotes.as_str()) + } else { + part_without_quotes.clone() + }, + QueryWithCursorPosition::cursor_marker(), + post_sql, + ); + + self.completions_snapshot( + query.into(), + &mut snapshot_result, + schema_cache, + parser, + None, + ) + .await; + } + + pre_sql.push_str(&part_without_parens); + + if dot_idx < dot_count { + pre_sql.push('.'); + } + + if ends_with_paren { + should_close_with_paren = false; + pre_sql.push(')'); + } + } + + if whitespace_idx < whitespace_count { + // note: we're sanitizing the white_space of typed SQL to simple spaces. + pre_sql.push(' '); + } + } + + pre_sql.push('\n'); + } + + ChunkToType::WithoutCompletions(sql) => { + pre_sql.push_str(sql.as_str()); + pre_sql.push('\n'); + } + } + } + + snapshot_result + } + + async fn completions_snapshot( + &self, + query: QueryWithCursorPosition, + writer: &mut String, + schema: &SchemaCache, + parser: &mut tree_sitter::Parser, + comment: Option<&str>, + ) { + let (pos, mut sql) = query.get_text_and_position(); + + let tree = parser.parse(&sql, None).expect("Invalid TS Tree!"); + + let params = CompletionParams { + text: sql.clone(), + position: (pos as u32).into(), + schema, + tree: &tree, + }; + + let items = complete(params); + + if pos < sql.len() { + sql.replace_range(pos..pos, "|"); + } else { + let diff = pos - sql.len(); + + sql.push_str(&" ".repeat(diff)); + sql.push('|'); + } + writeln!(writer, "{sql}").unwrap(); + + if let Some(c) = comment { + writeln!(writer, "**{c}**").unwrap(); + } + + if !items.is_empty() { + writeln!(writer).unwrap(); + writeln!(writer, "Results:").unwrap(); + + let max_idx = std::cmp::min(items.len(), 5); + for item in &items[..max_idx] { + write!( + writer, + "{}", + item.completion_text + .as_ref() + .filter(|c| !c.is_snippet) + .map(|c| c.text.as_str()) + .unwrap_or(item.label.as_str()) + ) + .unwrap(); + + write!(writer, " - ").unwrap(); + + match item.kind { + CompletionItemKind::Schema | CompletionItemKind::Role => {} + _ => { + write!(writer, "{}.", item.description).unwrap(); + } + } + + write!(writer, "{} ({})", item.label, item.kind).unwrap(); + + writeln!(writer).unwrap(); + } + + writeln!(writer).unwrap(); + + writeln!(writer, "--------------").unwrap(); + writeln!(writer).unwrap(); + } + } +} + +pub(crate) struct TestCompletionsSuite<'a> { + pool: &'a PgPool, + setup: Option<&'a str>, + cases: Vec, +} + +impl<'a> TestCompletionsSuite<'a> { + pub(crate) fn new(pool: &'a PgPool, setup: Option<&'a str>) -> Self { + Self { + pool, + setup, + cases: vec![], + } + } + + pub(crate) fn with_case(mut self, case: TestCompletionsCase) -> Self { + self.cases.push(case); + self + } + + pub(crate) async fn snapshot(self, snapshot_name: &str) { + assert!(!self.cases.is_empty(), "Needs at least one Snapshot case."); + + let mut final_snapshot = String::new(); + + if let Some(setup) = self.setup { + self.pool.execute(setup).await.expect("Problem with Setup"); + writeln!(final_snapshot, "***Setup***").unwrap(); + writeln!(final_snapshot).unwrap(); + write!(final_snapshot, "{}", unindent(setup)).unwrap(); + writeln!(final_snapshot).unwrap(); + writeln!(final_snapshot).unwrap(); + writeln!(final_snapshot, "--------------").unwrap(); + writeln!(final_snapshot).unwrap(); + } + + let cache = SchemaCache::load(self.pool) + .await + .expect("Problem loading SchemaCache"); + + let mut parser = tree_sitter::Parser::new(); + parser + .set_language(&pgls_treesitter_grammar::LANGUAGE.into()) + .expect("Problem with TreeSitter Grammar"); + + let has_more_than_one_case = self.cases.len() > 1; + + for (idx, additional) in self.cases.iter().enumerate() { + if idx > 0 { + writeln!(final_snapshot).unwrap(); + writeln!(final_snapshot).unwrap(); + writeln!(final_snapshot).unwrap(); + + writeln!(final_snapshot).unwrap(); + } + + if has_more_than_one_case { + writeln!(final_snapshot, "***Case {}:***", idx + 1).unwrap(); + writeln!(final_snapshot).unwrap(); + } + + let snap = additional.generate_snapshot(&cache, &mut parser).await; + + write!(final_snapshot, "{snap}").unwrap(); + } + + assert_snapshot!(snapshot_name, final_snapshot) + } +} diff --git a/crates/pgls_diagnostics/src/serde.rs b/crates/pgls_diagnostics/src/serde.rs index ab57d90fa..babb5d93d 100644 --- a/crates/pgls_diagnostics/src/serde.rs +++ b/crates/pgls_diagnostics/src/serde.rs @@ -164,7 +164,6 @@ impl From> for Location { #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[cfg_attr(test, derive(Eq, PartialEq))] - struct Advices { advices: Vec, } diff --git a/crates/pgls_hover/src/lib.rs b/crates/pgls_hover/src/lib.rs index 6276f7ba7..dbfdac68d 100644 --- a/crates/pgls_hover/src/lib.rs +++ b/crates/pgls_hover/src/lib.rs @@ -83,14 +83,15 @@ pub fn on_hover(params: OnHoverParams) -> Vec { _ => vec![], }, - HoveredNode::Function(node_identification) => match node_identification { - (maybe_schema, function_name) => params + HoveredNode::Function(node_identification) => { + let (maybe_schema, function_name) = node_identification; + params .schema_cache .find_functions(&function_name, maybe_schema.as_deref()) .into_iter() .map(Hoverable::from) - .collect(), - }, + .collect() + } HoveredNode::Role(role_name) => params .schema_cache