From 1514d5ece53cbc33f1e179ee1609b2dc52454a1d Mon Sep 17 00:00:00 2001 From: Luis Neves Date: Thu, 23 Jul 2026 20:56:16 +0100 Subject: [PATCH 1/3] fix: parse support of delete() functions --- crates/pgls_statement_splitter/src/lib.rs | 48 +++++++++++++++++++ .../src/splitter/common.rs | 5 ++ 2 files changed, 53 insertions(+) diff --git a/crates/pgls_statement_splitter/src/lib.rs b/crates/pgls_statement_splitter/src/lib.rs index 7e83489a3..96b7649aa 100644 --- a/crates/pgls_statement_splitter/src/lib.rs +++ b/crates/pgls_statement_splitter/src/lib.rs @@ -553,6 +553,54 @@ values ('insert', new.id, now());", ]); } + #[test] + fn hstore_function_calls() { + let calls = [ + "hstore(ROW(1, 2))", + "akeys('a=>1'::hstore)", + "skeys('a=>1'::hstore)", + "avals('a=>1'::hstore)", + "svals('a=>1'::hstore)", + "hstore_to_array('a=>1'::hstore)", + "hstore_to_matrix('a=>1'::hstore)", + "hstore_to_json('a=>1'::hstore)", + "hstore_to_jsonb('a=>1'::hstore)", + "hstore_to_json_loose('a=>1'::hstore)", + "hstore_to_jsonb_loose('a=>1'::hstore)", + "slice('a=>1'::hstore, ARRAY['a'])", + "each('a=>1'::hstore)", + "exist('a=>1'::hstore, 'a')", + "defined('a=>1'::hstore, 'a')", + "delete(resource_attributes, 'gen_ai.system')", + "DeLeTe(resource_attributes, 'gen_ai.system')", + "public.delete(resource_attributes, 'gen_ai.system')", + "delete /* hstore */ (resource_attributes, 'gen_ai.system')", + "populate_record(ROW(1, 2), 'f1=>42'::hstore)", + ]; + + for call in calls { + let statement = format!("SELECT {call};"); + let tester = Tester::from(statement.as_str()); + tester + .expect_statements(vec![statement.as_str()]) + .assert_no_errors(); + + let range = tester.result.ranges[0]; + if let Err(error) = pgls_query::parse(&statement[range]) { + panic!("Expected hstore function call to parse: {error}"); + } + } + + Tester::from( + "UPDATE data SET attributes = delete(attributes, 'obsolete'); DELETE FROM data;", + ) + .expect_statements(vec![ + "UPDATE data SET attributes = delete(attributes, 'obsolete');", + "DELETE FROM data;", + ]) + .assert_no_errors(); + } + #[test] fn with_ordinality() { Tester::from("insert into table (col) select 1 from other t cross join lateral jsonb_array_elements(t.buttons) with ordinality as a(b, nr) where t.buttons is not null;").expect_statements(vec!["insert into table (col) select 1 from other t cross join lateral jsonb_array_elements(t.buttons) with ordinality as a(b, nr) where t.buttons is not null;"]); diff --git a/crates/pgls_statement_splitter/src/splitter/common.rs b/crates/pgls_statement_splitter/src/splitter/common.rs index 21619d75c..7b0b109a6 100644 --- a/crates/pgls_statement_splitter/src/splitter/common.rs +++ b/crates/pgls_statement_splitter/src/splitter/common.rs @@ -261,6 +261,11 @@ pub(crate) fn unknown(p: &mut Splitter, exclude: &[SyntaxKind]) -> SplitterResul p.advance()?; } + // DELETE is also an unreserved keyword used by hstore's delete() function. + // A DELETE statement must be followed by FROM, so `delete(` cannot start one. + Some(SyntaxKind::DELETE_KW) if p.look_ahead(true) == SyntaxKind::L_PAREN => { + p.advance()?; + } Some(SyntaxKind::INSERT_KW) | Some(SyntaxKind::UPDATE_KW) | Some(SyntaxKind::DELETE_KW) => { From 1b20ea12689a1dcb57ce0769bd1fecb1f0fe1be3 Mon Sep 17 00:00:00 2001 From: Luis Neves Date: Thu, 23 Jul 2026 21:06:57 +0100 Subject: [PATCH 2/3] adds one more test --- .../pgls_workspace/src/workspace/server/document.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/crates/pgls_workspace/src/workspace/server/document.rs b/crates/pgls_workspace/src/workspace/server/document.rs index 4a1f06b5c..19957b109 100644 --- a/crates/pgls_workspace/src/workspace/server/document.rs +++ b/crates/pgls_workspace/src/workspace/server/document.rs @@ -524,6 +524,19 @@ $$;"; assert_eq!(results[1].1.end(), 39.into()); } + #[test] + fn hstore_delete_function_has_no_syntax_diagnostic() { + let input = "SELECT delete(resource_attributes, 'gen_ai.system');"; + let d = Document::new(input.to_string(), 1); + + assert!(d.document_diagnostics().is_empty()); + + let results = d.iter(AnalyserDiagnosticsMapper).collect::>(); + assert_eq!(results.len(), 1); + assert!(results[0].0.is_some()); + assert!(results[0].1.is_none()); + } + #[test] fn test_execute_statement_mapper() { let input = "SELECT 1; INVALID SYNTAX HERE;"; From fc2341d3f98f289d6c230665f5c39abd59179182 Mon Sep 17 00:00:00 2001 From: Luis Neves Date: Thu, 23 Jul 2026 21:29:57 +0100 Subject: [PATCH 3/3] test: cover hstore delete overloads Ensure array- and hstore-argument variants remain valid function calls during statement splitting. --- crates/pgls_statement_splitter/src/lib.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/pgls_statement_splitter/src/lib.rs b/crates/pgls_statement_splitter/src/lib.rs index 96b7649aa..8cde70a3f 100644 --- a/crates/pgls_statement_splitter/src/lib.rs +++ b/crates/pgls_statement_splitter/src/lib.rs @@ -572,6 +572,8 @@ values ('insert', new.id, now());", "exist('a=>1'::hstore, 'a')", "defined('a=>1'::hstore, 'a')", "delete(resource_attributes, 'gen_ai.system')", + "delete('a=>1,b=>2'::hstore, ARRAY['a', 'b'])", + "delete('a=>1,b=>2'::hstore, 'a=>1'::hstore)", "DeLeTe(resource_attributes, 'gen_ai.system')", "public.delete(resource_attributes, 'gen_ai.system')", "delete /* hstore */ (resource_attributes, 'gen_ai.system')",