From 4ab05f1169bf2488fdd63bb14a9ca4b3c3923c44 Mon Sep 17 00:00:00 2001 From: JulesWritesCode Date: Sun, 1 Mar 2026 13:24:38 +0100 Subject: [PATCH 1/4] Initial --- Cargo.lock | 3 + crates/pgls_completions/Cargo.toml | 3 + .../src/providers/keywords.rs | 297 ++++++++++++++++++ 3 files changed, 303 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 2a9a24e30..476829f6d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2439,11 +2439,14 @@ dependencies = [ "criterion", "fuzzy-matcher", "insta", + "pgls_query", "pgls_schema_cache", "pgls_test_utils", "pgls_text_size", "pgls_treesitter", "pgls_treesitter_grammar", + "prost-reflect", + "protox", "regex", "schemars", "serde", diff --git a/crates/pgls_completions/Cargo.toml b/crates/pgls_completions/Cargo.toml index 367989420..095c17ab9 100644 --- a/crates/pgls_completions/Cargo.toml +++ b/crates/pgls_completions/Cargo.toml @@ -27,8 +27,11 @@ tree-sitter.workspace = true [dev-dependencies] criterion.workspace = true insta.workspace = true +pgls_query.workspace = true pgls_schema_cache = { workspace = true, features = ["db"] } pgls_test_utils.workspace = true +prost-reflect.workspace = true +protox.workspace = true regex = "1.12.2" sqlx.workspace = true unindent = "0.2.4" diff --git a/crates/pgls_completions/src/providers/keywords.rs b/crates/pgls_completions/src/providers/keywords.rs index aecf84381..e3ee0163c 100644 --- a/crates/pgls_completions/src/providers/keywords.rs +++ b/crates/pgls_completions/src/providers/keywords.rs @@ -35,13 +35,20 @@ impl SqlKeyword { } pub static ALL_KEYWORDS: &[SqlKeyword] = &[ + SqlKeyword::new("abort"), + SqlKeyword::new("absent"), + SqlKeyword::new("absolute"), + SqlKeyword::new("access"), SqlKeyword::new("action"), SqlKeyword::new("add"), SqlKeyword::new("admin"), SqlKeyword::new("after"), + SqlKeyword::new("aggregate"), SqlKeyword::new("all"), + SqlKeyword::new("also"), SqlKeyword::new("alter").starts_statement(), SqlKeyword::new("always"), + SqlKeyword::new("analyse"), SqlKeyword::new("analyze") .starts_statement() .require_prefix(), @@ -50,9 +57,16 @@ pub static ALL_KEYWORDS: &[SqlKeyword] = &[ SqlKeyword::new("array").require_prefix(), SqlKeyword::new("as").require_prefix(), SqlKeyword::new("asc"), + SqlKeyword::new("asensitive"), + SqlKeyword::new("assertion"), + SqlKeyword::new("assignment"), + SqlKeyword::new("asymmetric"), + SqlKeyword::new("at"), SqlKeyword::new("atomic"), + SqlKeyword::new("attach"), SqlKeyword::new("attribute"), SqlKeyword::new("authorization"), + SqlKeyword::new("backward"), SqlKeyword::new("before"), SqlKeyword::new("begin").starts_statement(), SqlKeyword::new("between").require_prefix(), @@ -61,78 +75,125 @@ pub static ALL_KEYWORDS: &[SqlKeyword] = &[ SqlKeyword::new("binary"), SqlKeyword::new("bit"), SqlKeyword::new("boolean"), + SqlKeyword::new("both"), + SqlKeyword::new("breadth"), SqlKeyword::new("brin"), SqlKeyword::new("btree"), SqlKeyword::new("by"), SqlKeyword::new("bytea"), SqlKeyword::new("cache"), + SqlKeyword::new("call"), SqlKeyword::new("called"), SqlKeyword::new("cascade"), SqlKeyword::new("cascaded"), SqlKeyword::new("case"), SqlKeyword::new("cast").require_prefix(), + SqlKeyword::new("catalog"), + SqlKeyword::new("chain"), SqlKeyword::new("char"), SqlKeyword::new("character"), SqlKeyword::new("characteristics"), SqlKeyword::new("check"), + SqlKeyword::new("checkpoint"), + SqlKeyword::new("class"), + SqlKeyword::new("close"), + SqlKeyword::new("cluster"), + SqlKeyword::new("coalesce"), SqlKeyword::new("collate"), + SqlKeyword::new("collation"), SqlKeyword::new("column"), SqlKeyword::new("columns"), SqlKeyword::new("comment").starts_statement(), + SqlKeyword::new("comments"), SqlKeyword::new("commit").starts_statement(), SqlKeyword::new("committed"), SqlKeyword::new("compression"), SqlKeyword::new("concurrently"), + SqlKeyword::new("conditional"), + SqlKeyword::new("configuration"), SqlKeyword::new("conflict"), SqlKeyword::new("connection"), SqlKeyword::new("constraint"), SqlKeyword::new("constraints"), + SqlKeyword::new("content"), + SqlKeyword::new("continue"), + SqlKeyword::new("conversion"), SqlKeyword::new("copy").starts_statement(), SqlKeyword::new("cost"), SqlKeyword::new("create").starts_statement(), SqlKeyword::new("cross"), SqlKeyword::new("csv"), + SqlKeyword::new("cube"), SqlKeyword::new("current"), + SqlKeyword::new("current_catalog"), + SqlKeyword::new("current_date"), SqlKeyword::new("current_role"), + SqlKeyword::new("current_schema"), + SqlKeyword::new("current_time"), SqlKeyword::new("current_timestamp"), SqlKeyword::new("current_user"), + SqlKeyword::new("cursor"), SqlKeyword::new("cycle"), SqlKeyword::new("data"), SqlKeyword::new("database"), SqlKeyword::new("date"), + SqlKeyword::new("day"), + SqlKeyword::new("deallocate"), + SqlKeyword::new("dec"), SqlKeyword::new("decimal"), SqlKeyword::new("declare").starts_statement(), SqlKeyword::new("default"), + SqlKeyword::new("defaults"), SqlKeyword::new("deferrable"), SqlKeyword::new("deferred"), SqlKeyword::new("definer"), SqlKeyword::new("delete").starts_statement(), SqlKeyword::new("delimiter"), + SqlKeyword::new("delimiters"), + SqlKeyword::new("depends"), + SqlKeyword::new("depth"), SqlKeyword::new("desc"), + SqlKeyword::new("detach"), + SqlKeyword::new("dictionary"), + SqlKeyword::new("disable"), SqlKeyword::new("disable_page_skipping"), + SqlKeyword::new("discard"), SqlKeyword::new("distinct"), SqlKeyword::new("do").starts_statement(), + SqlKeyword::new("document"), + SqlKeyword::new("domain"), SqlKeyword::new("double"), SqlKeyword::new("drop").starts_statement(), SqlKeyword::new("each"), SqlKeyword::new("else"), + SqlKeyword::new("empty"), + SqlKeyword::new("enable"), SqlKeyword::new("encoding"), SqlKeyword::new("encrypted"), SqlKeyword::new("end"), SqlKeyword::new("enum"), + SqlKeyword::new("error"), SqlKeyword::new("escape"), + SqlKeyword::new("event"), SqlKeyword::new("except"), SqlKeyword::new("exclude"), + SqlKeyword::new("excluding"), + SqlKeyword::new("exclusive"), SqlKeyword::new("execute"), SqlKeyword::new("exists").require_prefix(), SqlKeyword::new("explain") .starts_statement() .require_prefix(), + SqlKeyword::new("expression"), SqlKeyword::new("extended"), SqlKeyword::new("extension"), SqlKeyword::new("external"), + SqlKeyword::new("extract"), SqlKeyword::new("false").require_prefix(), + SqlKeyword::new("family"), + SqlKeyword::new("fetch"), SqlKeyword::new("filter"), + SqlKeyword::new("finalize"), SqlKeyword::new("first"), SqlKeyword::new("float"), SqlKeyword::new("following"), @@ -143,6 +204,7 @@ pub static ALL_KEYWORDS: &[SqlKeyword] = &[ SqlKeyword::new("force_quote"), SqlKeyword::new("foreign"), SqlKeyword::new("format"), + SqlKeyword::new("forward"), SqlKeyword::new("freeze"), SqlKeyword::new("from"), SqlKeyword::new("full"), @@ -151,81 +213,146 @@ pub static ALL_KEYWORDS: &[SqlKeyword] = &[ SqlKeyword::new("generated"), SqlKeyword::new("gin"), SqlKeyword::new("gist"), + SqlKeyword::new("global"), SqlKeyword::new("grant").starts_statement(), SqlKeyword::new("granted"), + SqlKeyword::new("greatest"), SqlKeyword::new("group"), + SqlKeyword::new("grouping"), SqlKeyword::new("groups"), + SqlKeyword::new("handler"), SqlKeyword::new("hash"), SqlKeyword::new("having"), SqlKeyword::new("header"), + SqlKeyword::new("hold"), + SqlKeyword::new("hour"), + SqlKeyword::new("identity"), SqlKeyword::new("if"), + SqlKeyword::new("ilike"), SqlKeyword::new("immediate"), SqlKeyword::new("immutable"), + SqlKeyword::new("implicit"), + SqlKeyword::new("import"), SqlKeyword::new("in").require_prefix(), + SqlKeyword::new("include"), + SqlKeyword::new("including"), SqlKeyword::new("increment"), + SqlKeyword::new("indent"), SqlKeyword::new("index"), SqlKeyword::new("index_cleanup"), + SqlKeyword::new("indexes"), SqlKeyword::new("inet"), SqlKeyword::new("inherit"), + SqlKeyword::new("inherits"), SqlKeyword::new("initially"), + SqlKeyword::new("inline"), SqlKeyword::new("inner"), SqlKeyword::new("inout"), SqlKeyword::new("input"), + SqlKeyword::new("insensitive"), SqlKeyword::new("insert").starts_statement(), SqlKeyword::new("instead"), SqlKeyword::new("int"), + SqlKeyword::new("integer"), SqlKeyword::new("intersect"), SqlKeyword::new("interval").require_prefix(), SqlKeyword::new("into"), SqlKeyword::new("invoker"), SqlKeyword::new("is").require_prefix(), + SqlKeyword::new("isnull"), SqlKeyword::new("isolation"), SqlKeyword::new("join"), SqlKeyword::new("json"), + SqlKeyword::new("json_array"), + SqlKeyword::new("json_arrayagg"), + SqlKeyword::new("json_exists"), + SqlKeyword::new("json_object"), + SqlKeyword::new("json_objectagg"), + SqlKeyword::new("json_query"), + SqlKeyword::new("json_scalar"), + SqlKeyword::new("json_serialize"), + SqlKeyword::new("json_table"), + SqlKeyword::new("json_value"), SqlKeyword::new("jsonb"), + SqlKeyword::new("keep"), SqlKeyword::new("key"), + SqlKeyword::new("keys"), + SqlKeyword::new("label"), SqlKeyword::new("language"), + SqlKeyword::new("large"), SqlKeyword::new("last"), SqlKeyword::new("lateral"), + SqlKeyword::new("leading"), SqlKeyword::new("leakproof"), + SqlKeyword::new("least"), SqlKeyword::new("left"), SqlKeyword::new("level"), SqlKeyword::new("like").require_prefix(), SqlKeyword::new("limit"), SqlKeyword::new("list"), + SqlKeyword::new("listen"), + SqlKeyword::new("load"), SqlKeyword::new("local"), + SqlKeyword::new("localtime"), + SqlKeyword::new("localtimestamp"), SqlKeyword::new("location"), + SqlKeyword::new("lock"), + SqlKeyword::new("locked"), SqlKeyword::new("logged"), SqlKeyword::new("main"), SqlKeyword::new("maintain"), + SqlKeyword::new("mapping"), SqlKeyword::new("match"), SqlKeyword::new("matched"), SqlKeyword::new("materialized"), SqlKeyword::new("maxvalue"), SqlKeyword::new("merge").starts_statement(), + SqlKeyword::new("merge_action"), + SqlKeyword::new("method"), + SqlKeyword::new("minute"), SqlKeyword::new("minvalue"), + SqlKeyword::new("mode"), SqlKeyword::new("money"), + SqlKeyword::new("month"), + SqlKeyword::new("move"), SqlKeyword::new("name"), SqlKeyword::new("names"), + SqlKeyword::new("national"), SqlKeyword::new("natural"), + SqlKeyword::new("nchar"), + SqlKeyword::new("nested"), SqlKeyword::new("new"), + SqlKeyword::new("next"), + SqlKeyword::new("nfc"), + SqlKeyword::new("nfd"), + SqlKeyword::new("nfkc"), + SqlKeyword::new("nfkd"), SqlKeyword::new("no"), SqlKeyword::new("none"), + SqlKeyword::new("normalize"), + SqlKeyword::new("normalized"), SqlKeyword::new("not").require_prefix(), SqlKeyword::new("nothing"), + SqlKeyword::new("notify"), + SqlKeyword::new("notnull"), SqlKeyword::new("nowait"), SqlKeyword::new("null").require_prefix(), + SqlKeyword::new("nullif"), SqlKeyword::new("nulls"), SqlKeyword::new("numeric"), + SqlKeyword::new("object"), SqlKeyword::new("of"), SqlKeyword::new("off"), SqlKeyword::new("offset"), SqlKeyword::new("oid"), SqlKeyword::new("oids"), SqlKeyword::new("old"), + SqlKeyword::new("omit"), SqlKeyword::new("on"), SqlKeyword::new("only").require_prefix(), + SqlKeyword::new("operator"), SqlKeyword::new("option"), + SqlKeyword::new("options"), SqlKeyword::new("or").require_prefix(), SqlKeyword::new("order"), SqlKeyword::new("ordinality"), @@ -233,40 +360,66 @@ pub static ALL_KEYWORDS: &[SqlKeyword] = &[ SqlKeyword::new("out"), SqlKeyword::new("outer"), SqlKeyword::new("over"), + SqlKeyword::new("overlaps"), + SqlKeyword::new("overlay"), SqlKeyword::new("overriding"), SqlKeyword::new("owned"), SqlKeyword::new("owner"), SqlKeyword::new("parallel"), + SqlKeyword::new("parameter"), + SqlKeyword::new("parser"), + SqlKeyword::new("partial"), SqlKeyword::new("partition"), SqlKeyword::new("partitioned"), + SqlKeyword::new("passing"), SqlKeyword::new("password"), + SqlKeyword::new("path"), SqlKeyword::new("permissive"), + SqlKeyword::new("placing"), SqlKeyword::new("plain"), + SqlKeyword::new("plan"), + SqlKeyword::new("plans"), SqlKeyword::new("policy"), + SqlKeyword::new("position"), SqlKeyword::new("precedes"), SqlKeyword::new("preceding"), SqlKeyword::new("precision"), + SqlKeyword::new("prepare"), + SqlKeyword::new("prepared"), + SqlKeyword::new("preserve"), SqlKeyword::new("primary"), + SqlKeyword::new("prior"), SqlKeyword::new("privileges"), + SqlKeyword::new("procedural"), SqlKeyword::new("procedure"), SqlKeyword::new("procedures"), SqlKeyword::new("process_toast"), SqlKeyword::new("program"), SqlKeyword::new("public"), + SqlKeyword::new("publication"), SqlKeyword::new("quote"), + SqlKeyword::new("quotes"), SqlKeyword::new("range"), SqlKeyword::new("read"), SqlKeyword::new("real"), + SqlKeyword::new("reassign"), + SqlKeyword::new("recheck"), SqlKeyword::new("recursive"), + SqlKeyword::new("ref"), SqlKeyword::new("references"), SqlKeyword::new("referencing"), + SqlKeyword::new("refresh"), SqlKeyword::new("regclass"), SqlKeyword::new("regnamespace"), SqlKeyword::new("regproc"), SqlKeyword::new("regtype"), + SqlKeyword::new("reindex"), + SqlKeyword::new("relative"), + SqlKeyword::new("release"), SqlKeyword::new("rename"), SqlKeyword::new("repeatable"), SqlKeyword::new("replace"), + SqlKeyword::new("replica"), SqlKeyword::new("replication"), SqlKeyword::new("reset").starts_statement(), SqlKeyword::new("restart"), @@ -281,43 +434,71 @@ pub static ALL_KEYWORDS: &[SqlKeyword] = &[ SqlKeyword::new("right"), SqlKeyword::new("role"), SqlKeyword::new("rollback").starts_statement(), + SqlKeyword::new("rollup"), SqlKeyword::new("routine"), SqlKeyword::new("routines"), SqlKeyword::new("row"), SqlKeyword::new("rows"), + SqlKeyword::new("rule"), SqlKeyword::new("safe"), + SqlKeyword::new("savepoint"), + SqlKeyword::new("scalar"), SqlKeyword::new("schema"), + SqlKeyword::new("schemas"), + SqlKeyword::new("scroll"), + SqlKeyword::new("search"), + SqlKeyword::new("second"), SqlKeyword::new("security"), SqlKeyword::new("select").starts_statement(), SqlKeyword::new("sequence"), + SqlKeyword::new("sequences"), SqlKeyword::new("serial"), SqlKeyword::new("serializable"), + SqlKeyword::new("server"), SqlKeyword::new("session"), SqlKeyword::new("session_user"), SqlKeyword::new("set").starts_statement(), SqlKeyword::new("setof"), + SqlKeyword::new("sets"), + SqlKeyword::new("share"), SqlKeyword::new("show").starts_statement(), SqlKeyword::new("similar").require_prefix(), + SqlKeyword::new("simple"), + SqlKeyword::new("skip"), SqlKeyword::new("skip_locked"), SqlKeyword::new("smallint"), SqlKeyword::new("smallserial"), SqlKeyword::new("snapshot"), SqlKeyword::new("some"), + SqlKeyword::new("source"), SqlKeyword::new("spgist"), + SqlKeyword::new("sql"), SqlKeyword::new("stable"), + SqlKeyword::new("standalone"), SqlKeyword::new("start"), SqlKeyword::new("statement"), SqlKeyword::new("statistics"), SqlKeyword::new("stdin"), + SqlKeyword::new("stdout"), SqlKeyword::new("storage"), SqlKeyword::new("stored"), SqlKeyword::new("strict"), + SqlKeyword::new("string"), + SqlKeyword::new("strip"), + SqlKeyword::new("subscription"), + SqlKeyword::new("substring"), SqlKeyword::new("support"), + SqlKeyword::new("symmetric"), + SqlKeyword::new("sysid"), SqlKeyword::new("system"), + SqlKeyword::new("system_user"), SqlKeyword::new("table"), SqlKeyword::new("tables"), + SqlKeyword::new("tablesample"), SqlKeyword::new("tablespace"), + SqlKeyword::new("target"), SqlKeyword::new("temp"), + SqlKeyword::new("template"), SqlKeyword::new("temporary"), SqlKeyword::new("text"), SqlKeyword::new("then").require_prefix(), @@ -326,15 +507,26 @@ pub static ALL_KEYWORDS: &[SqlKeyword] = &[ SqlKeyword::new("timestamp"), SqlKeyword::new("timestamptz"), SqlKeyword::new("to"), + SqlKeyword::new("trailing"), SqlKeyword::new("transaction"), + SqlKeyword::new("transform"), + SqlKeyword::new("treat"), SqlKeyword::new("trigger"), + SqlKeyword::new("trim"), SqlKeyword::new("true").require_prefix(), SqlKeyword::new("truncate").starts_statement(), + SqlKeyword::new("trusted"), SqlKeyword::new("type"), + SqlKeyword::new("types"), + SqlKeyword::new("uescape"), SqlKeyword::new("unbounded"), SqlKeyword::new("uncommitted"), + SqlKeyword::new("unconditional"), + SqlKeyword::new("unencrypted"), SqlKeyword::new("union"), SqlKeyword::new("unique"), + SqlKeyword::new("unknown"), + SqlKeyword::new("unlisten"), SqlKeyword::new("unlogged"), SqlKeyword::new("unsafe"), SqlKeyword::new("until"), @@ -344,6 +536,8 @@ pub static ALL_KEYWORDS: &[SqlKeyword] = &[ SqlKeyword::new("uuid"), SqlKeyword::new("vacuum").starts_statement(), SqlKeyword::new("valid"), + SqlKeyword::new("validate"), + SqlKeyword::new("validator"), SqlKeyword::new("value"), SqlKeyword::new("values"), SqlKeyword::new("varchar"), @@ -352,14 +546,32 @@ pub static ALL_KEYWORDS: &[SqlKeyword] = &[ SqlKeyword::new("verbose"), SqlKeyword::new("version"), SqlKeyword::new("view"), + SqlKeyword::new("views"), SqlKeyword::new("volatile"), SqlKeyword::new("when"), SqlKeyword::new("where"), + SqlKeyword::new("whitespace"), SqlKeyword::new("window"), SqlKeyword::new("with").starts_statement(), + SqlKeyword::new("within"), SqlKeyword::new("without"), + SqlKeyword::new("work"), + SqlKeyword::new("wrapper"), SqlKeyword::new("write"), SqlKeyword::new("xml"), + SqlKeyword::new("xmlattributes"), + SqlKeyword::new("xmlconcat"), + SqlKeyword::new("xmlelement"), + SqlKeyword::new("xmlexists"), + SqlKeyword::new("xmlforest"), + SqlKeyword::new("xmlnamespaces"), + SqlKeyword::new("xmlparse"), + SqlKeyword::new("xmlpi"), + SqlKeyword::new("xmlroot"), + SqlKeyword::new("xmlserialize"), + SqlKeyword::new("xmltable"), + SqlKeyword::new("year"), + SqlKeyword::new("yes"), SqlKeyword::new("zone"), ]; @@ -409,17 +621,102 @@ pub fn complete_keywords<'a>( #[cfg(test)] mod tests { + use std::{collections::BTreeSet, path::Path}; + + use pgls_query::protobuf::KeywordKind; use pgls_test_utils::QueryWithCursorPosition; + use prost_reflect::DescriptorPool; use sqlx::PgPool; use crate::{ CompletionItemKind, + providers::keywords::ALL_KEYWORDS, test_helper::{ CompletionAssertion, TestCompletionsCase, TestCompletionsSuite, assert_complete_results, assert_no_complete_results, }, }; + #[test] + fn has_all_keywords_from_pg_query_proto() { + let proto_file = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../pgls_query/vendor/libpg_query/protobuf/pg_query.proto"); + let include_path = proto_file + .parent() + .expect("pg_query.proto should have a parent directory"); + let file_name = proto_file + .file_name() + .expect("pg_query.proto should have a file name"); + + let descriptor_set = + protox::compile([file_name], [include_path]).expect("failed to parse pg_query.proto"); + let pool = DescriptorPool::from_file_descriptor_set(descriptor_set) + .expect("failed to load protobuf descriptor pool"); + let token_enum = pool + .get_enum_by_name(".pg_query.Token") + .expect(".pg_query.Token enum should exist"); + + let mut expected_keywords = BTreeSet::new(); + + for value in token_enum.values() { + for candidate in keyword_candidates(value.name()) { + if is_sql_keyword(&candidate) { + expected_keywords.insert(candidate); + break; + } + } + } + + let actual_keywords = ALL_KEYWORDS + .iter() + .map(|keyword| keyword.name.to_string()) + .collect::>(); + + let missing_keywords = expected_keywords + .difference(&actual_keywords) + .cloned() + .collect::>(); + + assert!( + missing_keywords.is_empty(), + "Found {} keyword(s) derived from pg_query.proto that are missing from ALL_KEYWORDS.\n\ + Add missing entries to ALL_KEYWORDS in crates/pgls_completions/src/providers/keywords.rs.\n\ + Missing keywords:\n{}", + missing_keywords.len(), + missing_keywords.join("\n") + ); + } + + fn keyword_candidates(enum_value_name: &str) -> Vec { + let mut candidates = Vec::with_capacity(3); + + candidates.push(enum_value_name.to_ascii_lowercase()); + + if let Some(without_suffix) = enum_value_name.strip_suffix("_P") { + candidates.push(without_suffix.to_ascii_lowercase()); + } + + if let Some(without_suffix) = enum_value_name.strip_suffix("_LA") { + candidates.push(without_suffix.to_ascii_lowercase()); + } + + candidates + } + + fn is_sql_keyword(candidate: &str) -> bool { + let Ok(scan_result) = pgls_query::scan(candidate) else { + return false; + }; + let Some(token) = scan_result.tokens.first() else { + return false; + }; + let Ok(keyword_kind) = KeywordKind::try_from(token.keyword_kind) else { + return false; + }; + + keyword_kind != KeywordKind::NoKeyword + } + #[sqlx::test] async fn completes_stmt_start_keywords(pool: PgPool) { let setup = r#" From ceacc880cbf38996939b34cb323eb17420812062 Mon Sep 17 00:00:00 2001 From: JulesWritesCode Date: Sun, 1 Mar 2026 13:28:48 +0100 Subject: [PATCH 2/4] beaut --- .../src/providers/keywords.rs | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/crates/pgls_completions/src/providers/keywords.rs b/crates/pgls_completions/src/providers/keywords.rs index e3ee0163c..921226a1b 100644 --- a/crates/pgls_completions/src/providers/keywords.rs +++ b/crates/pgls_completions/src/providers/keywords.rs @@ -621,7 +621,7 @@ pub fn complete_keywords<'a>( #[cfg(test)] mod tests { - use std::{collections::BTreeSet, path::Path}; + use std::{collections::HashSet, path::Path}; use pgls_query::protobuf::KeywordKind; use pgls_test_utils::QueryWithCursorPosition; @@ -638,7 +638,7 @@ mod tests { }; #[test] - fn has_all_keywords_from_pg_query_proto() { + fn has_all_core_keywords_from_pg_query_proto() { let proto_file = Path::new(env!("CARGO_MANIFEST_DIR")) .join("../pgls_query/vendor/libpg_query/protobuf/pg_query.proto"); let include_path = proto_file @@ -656,11 +656,11 @@ mod tests { .get_enum_by_name(".pg_query.Token") .expect(".pg_query.Token enum should exist"); - let mut expected_keywords = BTreeSet::new(); + let mut expected_keywords = HashSet::new(); for value in token_enum.values() { for candidate in keyword_candidates(value.name()) { - if is_sql_keyword(&candidate) { + if is_core_sql_keyword(&candidate) { expected_keywords.insert(candidate); break; } @@ -670,16 +670,18 @@ mod tests { let actual_keywords = ALL_KEYWORDS .iter() .map(|keyword| keyword.name.to_string()) - .collect::>(); + .collect::>(); - let missing_keywords = expected_keywords + let mut missing_keywords = expected_keywords .difference(&actual_keywords) .cloned() .collect::>(); + missing_keywords.sort_unstable(); assert!( missing_keywords.is_empty(), - "Found {} keyword(s) derived from pg_query.proto that are missing from ALL_KEYWORDS.\n\ + "Found {} core keyword(s) derived from pg_query.proto that are missing from ALL_KEYWORDS.\n\ + Core keywords are defined as reserved or unreserved parser keywords.\n\ Add missing entries to ALL_KEYWORDS in crates/pgls_completions/src/providers/keywords.rs.\n\ Missing keywords:\n{}", missing_keywords.len(), @@ -703,7 +705,7 @@ mod tests { candidates } - fn is_sql_keyword(candidate: &str) -> bool { + fn is_core_sql_keyword(candidate: &str) -> bool { let Ok(scan_result) = pgls_query::scan(candidate) else { return false; }; @@ -714,7 +716,10 @@ mod tests { return false; }; - keyword_kind != KeywordKind::NoKeyword + matches!( + keyword_kind, + KeywordKind::ReservedKeyword | KeywordKind::UnreservedKeyword + ) } #[sqlx::test] From 14a7da6b01aef89bc561706cd84e386420a4c975 Mon Sep 17 00:00:00 2001 From: JulesWritesCode Date: Sun, 1 Mar 2026 13:49:43 +0100 Subject: [PATCH 3/4] much better approach --- Cargo.lock | 3 - crates/pgls_completions/Cargo.toml | 3 - .../src/providers/keywords.rs | 347 ++++-------------- 3 files changed, 64 insertions(+), 289 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 476829f6d..2a9a24e30 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2439,14 +2439,11 @@ dependencies = [ "criterion", "fuzzy-matcher", "insta", - "pgls_query", "pgls_schema_cache", "pgls_test_utils", "pgls_text_size", "pgls_treesitter", "pgls_treesitter_grammar", - "prost-reflect", - "protox", "regex", "schemars", "serde", diff --git a/crates/pgls_completions/Cargo.toml b/crates/pgls_completions/Cargo.toml index 095c17ab9..367989420 100644 --- a/crates/pgls_completions/Cargo.toml +++ b/crates/pgls_completions/Cargo.toml @@ -27,11 +27,8 @@ tree-sitter.workspace = true [dev-dependencies] criterion.workspace = true insta.workspace = true -pgls_query.workspace = true pgls_schema_cache = { workspace = true, features = ["db"] } pgls_test_utils.workspace = true -prost-reflect.workspace = true -protox.workspace = true regex = "1.12.2" sqlx.workspace = true unindent = "0.2.4" diff --git a/crates/pgls_completions/src/providers/keywords.rs b/crates/pgls_completions/src/providers/keywords.rs index 921226a1b..f2fdeb444 100644 --- a/crates/pgls_completions/src/providers/keywords.rs +++ b/crates/pgls_completions/src/providers/keywords.rs @@ -35,20 +35,13 @@ impl SqlKeyword { } pub static ALL_KEYWORDS: &[SqlKeyword] = &[ - SqlKeyword::new("abort"), - SqlKeyword::new("absent"), - SqlKeyword::new("absolute"), - SqlKeyword::new("access"), SqlKeyword::new("action"), SqlKeyword::new("add"), SqlKeyword::new("admin"), SqlKeyword::new("after"), - SqlKeyword::new("aggregate"), SqlKeyword::new("all"), - SqlKeyword::new("also"), SqlKeyword::new("alter").starts_statement(), SqlKeyword::new("always"), - SqlKeyword::new("analyse"), SqlKeyword::new("analyze") .starts_statement() .require_prefix(), @@ -57,16 +50,9 @@ pub static ALL_KEYWORDS: &[SqlKeyword] = &[ SqlKeyword::new("array").require_prefix(), SqlKeyword::new("as").require_prefix(), SqlKeyword::new("asc"), - SqlKeyword::new("asensitive"), - SqlKeyword::new("assertion"), - SqlKeyword::new("assignment"), - SqlKeyword::new("asymmetric"), - SqlKeyword::new("at"), SqlKeyword::new("atomic"), - SqlKeyword::new("attach"), SqlKeyword::new("attribute"), SqlKeyword::new("authorization"), - SqlKeyword::new("backward"), SqlKeyword::new("before"), SqlKeyword::new("begin").starts_statement(), SqlKeyword::new("between").require_prefix(), @@ -75,125 +61,79 @@ pub static ALL_KEYWORDS: &[SqlKeyword] = &[ SqlKeyword::new("binary"), SqlKeyword::new("bit"), SqlKeyword::new("boolean"), - SqlKeyword::new("both"), - SqlKeyword::new("breadth"), SqlKeyword::new("brin"), SqlKeyword::new("btree"), + SqlKeyword::new("buffer_usage_limit"), + SqlKeyword::new("buffers"), SqlKeyword::new("by"), SqlKeyword::new("bytea"), SqlKeyword::new("cache"), - SqlKeyword::new("call"), SqlKeyword::new("called"), SqlKeyword::new("cascade"), SqlKeyword::new("cascaded"), SqlKeyword::new("case"), SqlKeyword::new("cast").require_prefix(), - SqlKeyword::new("catalog"), - SqlKeyword::new("chain"), SqlKeyword::new("char"), SqlKeyword::new("character"), SqlKeyword::new("characteristics"), SqlKeyword::new("check"), - SqlKeyword::new("checkpoint"), - SqlKeyword::new("class"), - SqlKeyword::new("close"), - SqlKeyword::new("cluster"), - SqlKeyword::new("coalesce"), SqlKeyword::new("collate"), - SqlKeyword::new("collation"), SqlKeyword::new("column"), - SqlKeyword::new("columns"), SqlKeyword::new("comment").starts_statement(), - SqlKeyword::new("comments"), SqlKeyword::new("commit").starts_statement(), SqlKeyword::new("committed"), SqlKeyword::new("compression"), SqlKeyword::new("concurrently"), - SqlKeyword::new("conditional"), - SqlKeyword::new("configuration"), SqlKeyword::new("conflict"), SqlKeyword::new("connection"), SqlKeyword::new("constraint"), SqlKeyword::new("constraints"), - SqlKeyword::new("content"), - SqlKeyword::new("continue"), - SqlKeyword::new("conversion"), SqlKeyword::new("copy").starts_statement(), SqlKeyword::new("cost"), + SqlKeyword::new("costs"), SqlKeyword::new("create").starts_statement(), SqlKeyword::new("cross"), SqlKeyword::new("csv"), - SqlKeyword::new("cube"), SqlKeyword::new("current"), - SqlKeyword::new("current_catalog"), - SqlKeyword::new("current_date"), SqlKeyword::new("current_role"), - SqlKeyword::new("current_schema"), - SqlKeyword::new("current_time"), SqlKeyword::new("current_timestamp"), SqlKeyword::new("current_user"), - SqlKeyword::new("cursor"), SqlKeyword::new("cycle"), SqlKeyword::new("data"), SqlKeyword::new("database"), SqlKeyword::new("date"), - SqlKeyword::new("day"), - SqlKeyword::new("deallocate"), - SqlKeyword::new("dec"), SqlKeyword::new("decimal"), SqlKeyword::new("declare").starts_statement(), SqlKeyword::new("default"), - SqlKeyword::new("defaults"), SqlKeyword::new("deferrable"), SqlKeyword::new("deferred"), SqlKeyword::new("definer"), SqlKeyword::new("delete").starts_statement(), SqlKeyword::new("delimiter"), - SqlKeyword::new("delimiters"), - SqlKeyword::new("depends"), - SqlKeyword::new("depth"), SqlKeyword::new("desc"), - SqlKeyword::new("detach"), - SqlKeyword::new("dictionary"), - SqlKeyword::new("disable"), - SqlKeyword::new("disable_page_skipping"), - SqlKeyword::new("discard"), SqlKeyword::new("distinct"), SqlKeyword::new("do").starts_statement(), - SqlKeyword::new("document"), - SqlKeyword::new("domain"), SqlKeyword::new("double"), SqlKeyword::new("drop").starts_statement(), SqlKeyword::new("each"), SqlKeyword::new("else"), - SqlKeyword::new("empty"), - SqlKeyword::new("enable"), SqlKeyword::new("encoding"), SqlKeyword::new("encrypted"), SqlKeyword::new("end"), SqlKeyword::new("enum"), - SqlKeyword::new("error"), SqlKeyword::new("escape"), - SqlKeyword::new("event"), SqlKeyword::new("except"), SqlKeyword::new("exclude"), - SqlKeyword::new("excluding"), - SqlKeyword::new("exclusive"), SqlKeyword::new("execute"), SqlKeyword::new("exists").require_prefix(), SqlKeyword::new("explain") .starts_statement() .require_prefix(), - SqlKeyword::new("expression"), SqlKeyword::new("extended"), SqlKeyword::new("extension"), SqlKeyword::new("external"), - SqlKeyword::new("extract"), SqlKeyword::new("false").require_prefix(), - SqlKeyword::new("family"), - SqlKeyword::new("fetch"), SqlKeyword::new("filter"), - SqlKeyword::new("finalize"), SqlKeyword::new("first"), SqlKeyword::new("float"), SqlKeyword::new("following"), @@ -204,155 +144,91 @@ pub static ALL_KEYWORDS: &[SqlKeyword] = &[ SqlKeyword::new("force_quote"), SqlKeyword::new("foreign"), SqlKeyword::new("format"), - SqlKeyword::new("forward"), SqlKeyword::new("freeze"), SqlKeyword::new("from"), SqlKeyword::new("full"), SqlKeyword::new("function"), SqlKeyword::new("functions"), SqlKeyword::new("generated"), + SqlKeyword::new("generic_plan"), SqlKeyword::new("gin"), SqlKeyword::new("gist"), - SqlKeyword::new("global"), SqlKeyword::new("grant").starts_statement(), SqlKeyword::new("granted"), - SqlKeyword::new("greatest"), SqlKeyword::new("group"), - SqlKeyword::new("grouping"), SqlKeyword::new("groups"), - SqlKeyword::new("handler"), SqlKeyword::new("hash"), SqlKeyword::new("having"), SqlKeyword::new("header"), - SqlKeyword::new("hold"), - SqlKeyword::new("hour"), - SqlKeyword::new("identity"), SqlKeyword::new("if"), - SqlKeyword::new("ilike"), + SqlKeyword::new("ignore"), SqlKeyword::new("immediate"), SqlKeyword::new("immutable"), - SqlKeyword::new("implicit"), - SqlKeyword::new("import"), SqlKeyword::new("in").require_prefix(), - SqlKeyword::new("include"), - SqlKeyword::new("including"), SqlKeyword::new("increment"), - SqlKeyword::new("indent"), SqlKeyword::new("index"), - SqlKeyword::new("index_cleanup"), - SqlKeyword::new("indexes"), SqlKeyword::new("inet"), SqlKeyword::new("inherit"), - SqlKeyword::new("inherits"), SqlKeyword::new("initially"), - SqlKeyword::new("inline"), SqlKeyword::new("inner"), SqlKeyword::new("inout"), SqlKeyword::new("input"), - SqlKeyword::new("insensitive"), SqlKeyword::new("insert").starts_statement(), SqlKeyword::new("instead"), SqlKeyword::new("int"), - SqlKeyword::new("integer"), SqlKeyword::new("intersect"), SqlKeyword::new("interval").require_prefix(), SqlKeyword::new("into"), SqlKeyword::new("invoker"), SqlKeyword::new("is").require_prefix(), - SqlKeyword::new("isnull"), SqlKeyword::new("isolation"), SqlKeyword::new("join"), SqlKeyword::new("json"), - SqlKeyword::new("json_array"), - SqlKeyword::new("json_arrayagg"), - SqlKeyword::new("json_exists"), - SqlKeyword::new("json_object"), - SqlKeyword::new("json_objectagg"), - SqlKeyword::new("json_query"), - SqlKeyword::new("json_scalar"), - SqlKeyword::new("json_serialize"), - SqlKeyword::new("json_table"), - SqlKeyword::new("json_value"), SqlKeyword::new("jsonb"), - SqlKeyword::new("keep"), SqlKeyword::new("key"), - SqlKeyword::new("keys"), - SqlKeyword::new("label"), SqlKeyword::new("language"), - SqlKeyword::new("large"), SqlKeyword::new("last"), SqlKeyword::new("lateral"), - SqlKeyword::new("leading"), SqlKeyword::new("leakproof"), - SqlKeyword::new("least"), SqlKeyword::new("left"), SqlKeyword::new("level"), SqlKeyword::new("like").require_prefix(), SqlKeyword::new("limit"), SqlKeyword::new("list"), - SqlKeyword::new("listen"), - SqlKeyword::new("load"), SqlKeyword::new("local"), - SqlKeyword::new("localtime"), - SqlKeyword::new("localtimestamp"), - SqlKeyword::new("location"), - SqlKeyword::new("lock"), - SqlKeyword::new("locked"), + SqlKeyword::new("log_verbosity"), SqlKeyword::new("logged"), SqlKeyword::new("main"), SqlKeyword::new("maintain"), - SqlKeyword::new("mapping"), SqlKeyword::new("match"), SqlKeyword::new("matched"), SqlKeyword::new("materialized"), SqlKeyword::new("maxvalue"), + SqlKeyword::new("memory"), SqlKeyword::new("merge").starts_statement(), - SqlKeyword::new("merge_action"), - SqlKeyword::new("method"), - SqlKeyword::new("minute"), SqlKeyword::new("minvalue"), - SqlKeyword::new("mode"), SqlKeyword::new("money"), - SqlKeyword::new("month"), - SqlKeyword::new("move"), SqlKeyword::new("name"), SqlKeyword::new("names"), - SqlKeyword::new("national"), SqlKeyword::new("natural"), - SqlKeyword::new("nchar"), - SqlKeyword::new("nested"), SqlKeyword::new("new"), - SqlKeyword::new("next"), - SqlKeyword::new("nfc"), - SqlKeyword::new("nfd"), - SqlKeyword::new("nfkc"), - SqlKeyword::new("nfkd"), SqlKeyword::new("no"), SqlKeyword::new("none"), - SqlKeyword::new("normalize"), - SqlKeyword::new("normalized"), SqlKeyword::new("not").require_prefix(), SqlKeyword::new("nothing"), - SqlKeyword::new("notify"), - SqlKeyword::new("notnull"), - SqlKeyword::new("nowait"), SqlKeyword::new("null").require_prefix(), - SqlKeyword::new("nullif"), SqlKeyword::new("nulls"), SqlKeyword::new("numeric"), - SqlKeyword::new("object"), SqlKeyword::new("of"), SqlKeyword::new("off"), SqlKeyword::new("offset"), SqlKeyword::new("oid"), SqlKeyword::new("oids"), SqlKeyword::new("old"), - SqlKeyword::new("omit"), SqlKeyword::new("on"), + SqlKeyword::new("on_error"), SqlKeyword::new("only").require_prefix(), - SqlKeyword::new("operator"), SqlKeyword::new("option"), - SqlKeyword::new("options"), SqlKeyword::new("or").require_prefix(), SqlKeyword::new("order"), SqlKeyword::new("ordinality"), @@ -360,67 +236,38 @@ pub static ALL_KEYWORDS: &[SqlKeyword] = &[ SqlKeyword::new("out"), SqlKeyword::new("outer"), SqlKeyword::new("over"), - SqlKeyword::new("overlaps"), - SqlKeyword::new("overlay"), SqlKeyword::new("overriding"), SqlKeyword::new("owned"), SqlKeyword::new("owner"), SqlKeyword::new("parallel"), - SqlKeyword::new("parameter"), - SqlKeyword::new("parser"), - SqlKeyword::new("partial"), SqlKeyword::new("partition"), - SqlKeyword::new("partitioned"), - SqlKeyword::new("passing"), SqlKeyword::new("password"), - SqlKeyword::new("path"), SqlKeyword::new("permissive"), - SqlKeyword::new("placing"), SqlKeyword::new("plain"), - SqlKeyword::new("plan"), - SqlKeyword::new("plans"), SqlKeyword::new("policy"), - SqlKeyword::new("position"), - SqlKeyword::new("precedes"), SqlKeyword::new("preceding"), SqlKeyword::new("precision"), - SqlKeyword::new("prepare"), - SqlKeyword::new("prepared"), - SqlKeyword::new("preserve"), SqlKeyword::new("primary"), - SqlKeyword::new("prior"), SqlKeyword::new("privileges"), - SqlKeyword::new("procedural"), SqlKeyword::new("procedure"), SqlKeyword::new("procedures"), - SqlKeyword::new("process_toast"), SqlKeyword::new("program"), SqlKeyword::new("public"), - SqlKeyword::new("publication"), SqlKeyword::new("quote"), - SqlKeyword::new("quotes"), SqlKeyword::new("range"), SqlKeyword::new("read"), SqlKeyword::new("real"), - SqlKeyword::new("reassign"), - SqlKeyword::new("recheck"), SqlKeyword::new("recursive"), - SqlKeyword::new("ref"), SqlKeyword::new("references"), SqlKeyword::new("referencing"), - SqlKeyword::new("refresh"), SqlKeyword::new("regclass"), SqlKeyword::new("regnamespace"), SqlKeyword::new("regproc"), SqlKeyword::new("regtype"), - SqlKeyword::new("reindex"), - SqlKeyword::new("relative"), - SqlKeyword::new("release"), + SqlKeyword::new("reject_limit"), SqlKeyword::new("rename"), SqlKeyword::new("repeatable"), SqlKeyword::new("replace"), - SqlKeyword::new("replica"), - SqlKeyword::new("replication"), SqlKeyword::new("reset").starts_statement(), SqlKeyword::new("restart"), SqlKeyword::new("restrict"), @@ -430,75 +277,52 @@ pub static ALL_KEYWORDS: &[SqlKeyword] = &[ SqlKeyword::new("returning"), SqlKeyword::new("returns"), SqlKeyword::new("revoke").starts_statement(), - SqlKeyword::new("rewrite"), SqlKeyword::new("right"), SqlKeyword::new("role"), SqlKeyword::new("rollback").starts_statement(), - SqlKeyword::new("rollup"), SqlKeyword::new("routine"), SqlKeyword::new("routines"), SqlKeyword::new("row"), SqlKeyword::new("rows"), - SqlKeyword::new("rule"), SqlKeyword::new("safe"), - SqlKeyword::new("savepoint"), - SqlKeyword::new("scalar"), SqlKeyword::new("schema"), - SqlKeyword::new("schemas"), - SqlKeyword::new("scroll"), - SqlKeyword::new("search"), - SqlKeyword::new("second"), SqlKeyword::new("security"), SqlKeyword::new("select").starts_statement(), SqlKeyword::new("sequence"), - SqlKeyword::new("sequences"), SqlKeyword::new("serial"), SqlKeyword::new("serializable"), - SqlKeyword::new("server"), SqlKeyword::new("session"), SqlKeyword::new("session_user"), SqlKeyword::new("set").starts_statement(), SqlKeyword::new("setof"), - SqlKeyword::new("sets"), + SqlKeyword::new("settings"), SqlKeyword::new("share"), SqlKeyword::new("show").starts_statement(), + SqlKeyword::new("silent"), SqlKeyword::new("similar").require_prefix(), - SqlKeyword::new("simple"), - SqlKeyword::new("skip"), SqlKeyword::new("skip_locked"), SqlKeyword::new("smallint"), SqlKeyword::new("smallserial"), SqlKeyword::new("snapshot"), SqlKeyword::new("some"), - SqlKeyword::new("source"), SqlKeyword::new("spgist"), - SqlKeyword::new("sql"), SqlKeyword::new("stable"), - SqlKeyword::new("standalone"), SqlKeyword::new("start"), SqlKeyword::new("statement"), SqlKeyword::new("statistics"), SqlKeyword::new("stdin"), SqlKeyword::new("stdout"), + SqlKeyword::new("stop"), SqlKeyword::new("storage"), SqlKeyword::new("stored"), SqlKeyword::new("strict"), - SqlKeyword::new("string"), - SqlKeyword::new("strip"), - SqlKeyword::new("subscription"), - SqlKeyword::new("substring"), + SqlKeyword::new("summary"), SqlKeyword::new("support"), - SqlKeyword::new("symmetric"), - SqlKeyword::new("sysid"), SqlKeyword::new("system"), - SqlKeyword::new("system_user"), SqlKeyword::new("table"), SqlKeyword::new("tables"), - SqlKeyword::new("tablesample"), SqlKeyword::new("tablespace"), - SqlKeyword::new("target"), SqlKeyword::new("temp"), - SqlKeyword::new("template"), SqlKeyword::new("temporary"), SqlKeyword::new("text"), SqlKeyword::new("then").require_prefix(), @@ -506,27 +330,17 @@ pub static ALL_KEYWORDS: &[SqlKeyword] = &[ SqlKeyword::new("time"), SqlKeyword::new("timestamp"), SqlKeyword::new("timestamptz"), + SqlKeyword::new("timing"), SqlKeyword::new("to"), - SqlKeyword::new("trailing"), SqlKeyword::new("transaction"), - SqlKeyword::new("transform"), - SqlKeyword::new("treat"), SqlKeyword::new("trigger"), - SqlKeyword::new("trim"), SqlKeyword::new("true").require_prefix(), SqlKeyword::new("truncate").starts_statement(), - SqlKeyword::new("trusted"), SqlKeyword::new("type"), - SqlKeyword::new("types"), - SqlKeyword::new("uescape"), SqlKeyword::new("unbounded"), SqlKeyword::new("uncommitted"), - SqlKeyword::new("unconditional"), - SqlKeyword::new("unencrypted"), SqlKeyword::new("union"), SqlKeyword::new("unique"), - SqlKeyword::new("unknown"), - SqlKeyword::new("unlisten"), SqlKeyword::new("unlogged"), SqlKeyword::new("unsafe"), SqlKeyword::new("until"), @@ -536,8 +350,6 @@ pub static ALL_KEYWORDS: &[SqlKeyword] = &[ SqlKeyword::new("uuid"), SqlKeyword::new("vacuum").starts_statement(), SqlKeyword::new("valid"), - SqlKeyword::new("validate"), - SqlKeyword::new("validator"), SqlKeyword::new("value"), SqlKeyword::new("values"), SqlKeyword::new("varchar"), @@ -546,32 +358,16 @@ pub static ALL_KEYWORDS: &[SqlKeyword] = &[ SqlKeyword::new("verbose"), SqlKeyword::new("version"), SqlKeyword::new("view"), - SqlKeyword::new("views"), SqlKeyword::new("volatile"), + SqlKeyword::new("wal"), SqlKeyword::new("when"), SqlKeyword::new("where"), - SqlKeyword::new("whitespace"), SqlKeyword::new("window"), SqlKeyword::new("with").starts_statement(), - SqlKeyword::new("within"), SqlKeyword::new("without"), - SqlKeyword::new("work"), - SqlKeyword::new("wrapper"), SqlKeyword::new("write"), SqlKeyword::new("xml"), - SqlKeyword::new("xmlattributes"), - SqlKeyword::new("xmlconcat"), - SqlKeyword::new("xmlelement"), - SqlKeyword::new("xmlexists"), - SqlKeyword::new("xmlforest"), - SqlKeyword::new("xmlnamespaces"), - SqlKeyword::new("xmlparse"), - SqlKeyword::new("xmlpi"), - SqlKeyword::new("xmlroot"), - SqlKeyword::new("xmlserialize"), - SqlKeyword::new("xmltable"), - SqlKeyword::new("year"), - SqlKeyword::new("yes"), + SqlKeyword::new("yaml"), SqlKeyword::new("zone"), ]; @@ -621,12 +417,12 @@ pub fn complete_keywords<'a>( #[cfg(test)] mod tests { - use std::{collections::HashSet, path::Path}; + use std::collections::HashSet; - use pgls_query::protobuf::KeywordKind; use pgls_test_utils::QueryWithCursorPosition; - use prost_reflect::DescriptorPool; + use regex::Regex; use sqlx::PgPool; + use tree_sitter::Language; use crate::{ CompletionItemKind, @@ -638,39 +434,9 @@ mod tests { }; #[test] - fn has_all_core_keywords_from_pg_query_proto() { - let proto_file = Path::new(env!("CARGO_MANIFEST_DIR")) - .join("../pgls_query/vendor/libpg_query/protobuf/pg_query.proto"); - let include_path = proto_file - .parent() - .expect("pg_query.proto should have a parent directory"); - let file_name = proto_file - .file_name() - .expect("pg_query.proto should have a file name"); - - let descriptor_set = - protox::compile([file_name], [include_path]).expect("failed to parse pg_query.proto"); - let pool = DescriptorPool::from_file_descriptor_set(descriptor_set) - .expect("failed to load protobuf descriptor pool"); - let token_enum = pool - .get_enum_by_name(".pg_query.Token") - .expect(".pg_query.Token enum should exist"); - - let mut expected_keywords = HashSet::new(); - - for value in token_enum.values() { - for candidate in keyword_candidates(value.name()) { - if is_core_sql_keyword(&candidate) { - expected_keywords.insert(candidate); - break; - } - } - } - - let actual_keywords = ALL_KEYWORDS - .iter() - .map(|keyword| keyword.name.to_string()) - .collect::>(); + fn has_all_keywords_from_treesitter_grammar() { + let expected_keywords = keywords_from_treesitter_grammar(); + let actual_keywords = all_keywords_set(); let mut missing_keywords = expected_keywords .difference(&actual_keywords) @@ -680,8 +446,7 @@ mod tests { assert!( missing_keywords.is_empty(), - "Found {} core keyword(s) derived from pg_query.proto that are missing from ALL_KEYWORDS.\n\ - Core keywords are defined as reserved or unreserved parser keywords.\n\ + "Found {} keyword(s) from tree-sitter grammar that are missing from ALL_KEYWORDS.\n\ Add missing entries to ALL_KEYWORDS in crates/pgls_completions/src/providers/keywords.rs.\n\ Missing keywords:\n{}", missing_keywords.len(), @@ -689,37 +454,53 @@ mod tests { ); } - fn keyword_candidates(enum_value_name: &str) -> Vec { - let mut candidates = Vec::with_capacity(3); - - candidates.push(enum_value_name.to_ascii_lowercase()); + #[test] + fn has_no_extra_keywords_outside_treesitter_grammar() { + let expected_keywords = keywords_from_treesitter_grammar(); + let actual_keywords = all_keywords_set(); - if let Some(without_suffix) = enum_value_name.strip_suffix("_P") { - candidates.push(without_suffix.to_ascii_lowercase()); - } + let mut extra_keywords = actual_keywords + .difference(&expected_keywords) + .cloned() + .collect::>(); + extra_keywords.sort_unstable(); - if let Some(without_suffix) = enum_value_name.strip_suffix("_LA") { - candidates.push(without_suffix.to_ascii_lowercase()); - } + assert!( + extra_keywords.is_empty(), + "Found {} keyword(s) in ALL_KEYWORDS that are not tree-sitter grammar keywords.\n\ + Remove these from ALL_KEYWORDS or add matching keyword_* symbols to grammar.js.\n\ + Extra keywords:\n{}", + extra_keywords.len(), + extra_keywords.join("\n") + ); + } - candidates + fn all_keywords_set() -> HashSet { + ALL_KEYWORDS + .iter() + .map(|keyword| keyword.name.to_string()) + .collect::>() } - fn is_core_sql_keyword(candidate: &str) -> bool { - let Ok(scan_result) = pgls_query::scan(candidate) else { - return false; - }; - let Some(token) = scan_result.tokens.first() else { - return false; - }; - let Ok(keyword_kind) = KeywordKind::try_from(token.keyword_kind) else { - return false; - }; + fn keywords_from_treesitter_grammar() -> HashSet { + let language: Language = pgls_treesitter_grammar::LANGUAGE.into(); + // Tree-sitter generates auxiliary symbol names like `keyword_int_token2` for keyword + // rules that use `choice(...)`. Strip the suffix to compare canonical keyword names. + let generated_token_suffix = + Regex::new(r"_token\d+$").expect("valid regex for generated token suffix"); + let mut keywords = HashSet::new(); + + for id in 0..language.node_kind_count() { + let Some(kind) = language.node_kind_for_id(id as u16) else { + continue; + }; + if let Some(keyword) = kind.strip_prefix("keyword_") { + let keyword = generated_token_suffix.replace(keyword, "").to_string(); + keywords.insert(keyword); + } + } - matches!( - keyword_kind, - KeywordKind::ReservedKeyword | KeywordKind::UnreservedKeyword - ) + keywords } #[sqlx::test] From 69e39ac3d23bd015c45fb4cfcb84aec32ba22f88 Mon Sep 17 00:00:00 2001 From: JulesWritesCode Date: Sun, 1 Mar 2026 13:56:39 +0100 Subject: [PATCH 4/4] removed unreachable keywords --- crates/pgls_treesitter_grammar/grammar.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/crates/pgls_treesitter_grammar/grammar.js b/crates/pgls_treesitter_grammar/grammar.js index 7ae6b46ff..896cf6b9a 100644 --- a/crates/pgls_treesitter_grammar/grammar.js +++ b/crates/pgls_treesitter_grammar/grammar.js @@ -145,7 +145,6 @@ module.exports = grammar({ keyword_tables: (_) => make_keyword("tables"), keyword_view: (_) => make_keyword("view"), keyword_column: (_) => make_keyword("column"), - keyword_columns: (_) => make_keyword("columns"), keyword_materialized: (_) => make_keyword("materialized"), keyword_tablespace: (_) => make_keyword("tablespace"), keyword_sequence: (_) => make_keyword("sequence"), @@ -263,7 +262,6 @@ module.exports = grammar({ keyword_check: (_) => make_keyword("check"), keyword_option: (_) => make_keyword("option"), keyword_vacuum: (_) => make_keyword("vacuum"), - keyword_nowait: (_) => make_keyword("nowait"), keyword_attribute: (_) => make_keyword("attribute"), keyword_authorization: (_) => make_keyword("authorization"), keyword_action: (_) => make_keyword("action"), @@ -300,7 +298,6 @@ module.exports = grammar({ keyword_timing: (_) => make_keyword("timing"), keyword_summary: (_) => make_keyword("summary"), keyword_memory: (_) => make_keyword("memory"), - keyword_serialize: (_) => make_keyword("serialize"), keyword_skip_locked: (_) => make_keyword("skip_locked"), keyword_buffer_usage_limit: (_) => make_keyword("buffer_usage_limit"), @@ -373,7 +370,6 @@ module.exports = grammar({ keyword_constraints: (_) => make_keyword("constraints"), keyword_snapshot: (_) => make_keyword("snapshot"), keyword_characteristics: (_) => make_keyword("characteristics"), - keyword_precedes: (_) => make_keyword("precedes"), keyword_each: (_) => make_keyword("each"), keyword_instead: (_) => make_keyword("instead"), keyword_of: (_) => make_keyword("of"), @@ -388,11 +384,7 @@ module.exports = grammar({ keyword_external: (_) => make_keyword("external"), keyword_stored: (_) => make_keyword("stored"), - keyword_replication: (_) => make_keyword("replication"), keyword_statistics: (_) => make_keyword("statistics"), - keyword_rewrite: (_) => make_keyword("rewrite"), - keyword_location: (_) => make_keyword("location"), - keyword_partitioned: (_) => make_keyword("partitioned"), keyword_comment: (_) => make_keyword("comment"), keyword_format: (_) => make_keyword("format"), keyword_delimiter: (_) => make_keyword("delimiter"),