diff --git a/crates/pgls_statement_splitter/src/lib.rs b/crates/pgls_statement_splitter/src/lib.rs index 7e83489a3..8cde70a3f 100644 --- a/crates/pgls_statement_splitter/src/lib.rs +++ b/crates/pgls_statement_splitter/src/lib.rs @@ -553,6 +553,56 @@ 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('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')", + "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) => { 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;";