From ba281a5e6c3a9222218f67e62c35e14c4296f697 Mon Sep 17 00:00:00 2001 From: Steve Dignam Date: Wed, 16 Sep 2026 21:53:57 -0400 Subject: [PATCH 01/11] parser: rework test setup --- .../data/plpgsql/err/unrecognized_stmt.sql | 2 + .../tests/data/plpgsql/ok/null_stmt.sql | 7 ++ .../tests__plpgsql_null_stmt_ok.snap | 9 -- .../tests__plpgsql_unrecognized_stmt_err.snap | 24 ------ crates/squawk_parser/tests/tests.rs | 63 +------------- ...wk_syntax__test__plpgsql_null_stmt_ok.snap | 21 +++++ ...__test__plpgsql_unrecognized_stmt_err.snap | 25 ++++++ crates/squawk_syntax/src/test.rs | 83 +++++++++++++++++++ 8 files changed, 139 insertions(+), 95 deletions(-) delete mode 100644 crates/squawk_parser/tests/snapshots/tests__plpgsql_null_stmt_ok.snap delete mode 100644 crates/squawk_parser/tests/snapshots/tests__plpgsql_unrecognized_stmt_err.snap create mode 100644 crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_null_stmt_ok.snap create mode 100644 crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_unrecognized_stmt_err.snap diff --git a/crates/squawk_parser/tests/data/plpgsql/err/unrecognized_stmt.sql b/crates/squawk_parser/tests/data/plpgsql/err/unrecognized_stmt.sql index 117ecaad..ebf51ccc 100644 --- a/crates/squawk_parser/tests/data/plpgsql/err/unrecognized_stmt.sql +++ b/crates/squawk_parser/tests/data/plpgsql/err/unrecognized_stmt.sql @@ -1 +1,3 @@ +do $$ begin null; end +$$; diff --git a/crates/squawk_parser/tests/data/plpgsql/ok/null_stmt.sql b/crates/squawk_parser/tests/data/plpgsql/ok/null_stmt.sql index ff464d15..bfd70bd0 100644 --- a/crates/squawk_parser/tests/data/plpgsql/ok/null_stmt.sql +++ b/crates/squawk_parser/tests/data/plpgsql/ok/null_stmt.sql @@ -1 +1,8 @@ +do $$ null; +$$; + +do $$ +null; +null; +$$; diff --git a/crates/squawk_parser/tests/snapshots/tests__plpgsql_null_stmt_ok.snap b/crates/squawk_parser/tests/snapshots/tests__plpgsql_null_stmt_ok.snap deleted file mode 100644 index a9bf4088..00000000 --- a/crates/squawk_parser/tests/snapshots/tests__plpgsql_null_stmt_ok.snap +++ /dev/null @@ -1,9 +0,0 @@ ---- -source: crates/squawk_parser/tests/tests.rs -input_file: crates/squawk_parser/tests/data/plpgsql/ok/null_stmt.sql ---- -PLPGSQL - PLPGSQL_NULL_STMT - NULL_KW "null" - SEMICOLON ";" - WHITESPACE "\n" diff --git a/crates/squawk_parser/tests/snapshots/tests__plpgsql_unrecognized_stmt_err.snap b/crates/squawk_parser/tests/snapshots/tests__plpgsql_unrecognized_stmt_err.snap deleted file mode 100644 index a9562d8d..00000000 --- a/crates/squawk_parser/tests/snapshots/tests__plpgsql_unrecognized_stmt_err.snap +++ /dev/null @@ -1,24 +0,0 @@ ---- -source: crates/squawk_parser/tests/tests.rs -input_file: crates/squawk_parser/tests/data/plpgsql/err/unrecognized_stmt.sql ---- -PLPGSQL - ERROR - BEGIN_KW "begin" - WHITESPACE " " - PLPGSQL_NULL_STMT - NULL_KW "null" - SEMICOLON ";" - WHITESPACE " " - ERROR - END_KW "end" - WHITESPACE "\n" ---- -error[syntax-error]: expected a statement, found BEGIN_KW - ╭▸ -1 │ begin null; end - ╰╴━ -error[syntax-error]: expected a statement, found END_KW - ╭▸ -1 │ begin null; end - ╰╴ ━ diff --git a/crates/squawk_parser/tests/tests.rs b/crates/squawk_parser/tests/tests.rs index 1bcea277..162437dd 100644 --- a/crates/squawk_parser/tests/tests.rs +++ b/crates/squawk_parser/tests/tests.rs @@ -64,62 +64,6 @@ fn parser_err(fixture: Fixture<&str>) { ); } -#[dir_test( - dir: "$CARGO_MANIFEST_DIR/tests/data/plpgsql/ok", - glob: "*.sql", -)] -fn plpgsql_ok(fixture: Fixture<&str>) { - let content = fixture.content(); - let absolute_fixture_path = Utf8Path::new(fixture.path()); - let input_file = absolute_fixture_path; - let test_name = absolute_fixture_path - .file_name() - .and_then(|x| x.strip_suffix(".sql")) - .unwrap(); - - let (parsed, errors) = parse(content, EntryPoint::Plpgsql); - - with_settings!({ - omit_expression => true, - input_file => input_file, - }, { - assert_snapshot!(format!("plpgsql_{test_name}_ok"), parsed); - }); - - assert!( - errors.is_none(), - "tests defined in `plpgsql/ok` can't have parser errors." - ); -} - -#[dir_test( - dir: "$CARGO_MANIFEST_DIR/tests/data/plpgsql/err", - glob: "*.sql", -)] -fn plpgsql_err(fixture: Fixture<&str>) { - let content = fixture.content(); - let absolute_fixture_path = Utf8Path::new(fixture.path()); - let input_file = absolute_fixture_path; - let test_name = absolute_fixture_path - .file_name() - .and_then(|x| x.strip_suffix(".sql")) - .unwrap(); - - let (parsed, errors) = parse(content, EntryPoint::Plpgsql); - - with_settings!({ - omit_expression => true, - input_file => input_file, - }, { - assert_snapshot!(format!("plpgsql_{test_name}_err"), parsed); - }); - - assert!( - errors.is_some(), - "tests defined in `plpgsql/err` must have parser errors." - ); -} - #[dir_test( dir: "$CARGO_MANIFEST_DIR/../../postgres/regression_suite", glob: "*.sql", @@ -192,14 +136,9 @@ fn bom_after_start_is_an_ident_char() { #[must_use] fn parse_text(text: &str) -> (String, Option) { - parse(text, EntryPoint::SourceFile) -} - -#[must_use] -fn parse(text: &str, entry_point: EntryPoint) -> (String, Option) { let lexed = LexedStr::new(text); let input = lexed.to_input(); - let output = entry_point.parse(&input); + let output = EntryPoint::SourceFile.parse(&input); let mut buf = String::new(); let mut errors: Vec<(std::ops::Range, String)> = Vec::new(); diff --git a/crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_null_stmt_ok.snap b/crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_null_stmt_ok.snap new file mode 100644 index 00000000..b5a0af44 --- /dev/null +++ b/crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_null_stmt_ok.snap @@ -0,0 +1,21 @@ +--- +source: crates/squawk_syntax/src/test.rs +input_file: crates/squawk_parser/tests/data/plpgsql/ok/null_stmt.sql +--- +PLPGSQL@0..7 + WHITESPACE@0..1 "\n" + PLPGSQL_NULL_STMT@1..6 + NULL_KW@1..5 "null" + SEMICOLON@5..6 ";" + WHITESPACE@6..7 "\n" +--- +PLPGSQL@0..13 + WHITESPACE@0..1 "\n" + PLPGSQL_NULL_STMT@1..6 + NULL_KW@1..5 "null" + SEMICOLON@5..6 ";" + WHITESPACE@6..7 "\n" + PLPGSQL_NULL_STMT@7..12 + NULL_KW@7..11 "null" + SEMICOLON@11..12 ";" + WHITESPACE@12..13 "\n" diff --git a/crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_unrecognized_stmt_err.snap b/crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_unrecognized_stmt_err.snap new file mode 100644 index 00000000..9c2b6c89 --- /dev/null +++ b/crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_unrecognized_stmt_err.snap @@ -0,0 +1,25 @@ +--- +source: crates/squawk_syntax/src/test.rs +input_file: crates/squawk_parser/tests/data/plpgsql/err/unrecognized_stmt.sql +--- +PLPGSQL@0..17 + WHITESPACE@0..1 "\n" + ERROR@1..6 + BEGIN_KW@1..6 "begin" + WHITESPACE@6..7 " " + PLPGSQL_NULL_STMT@7..12 + NULL_KW@7..11 "null" + SEMICOLON@11..12 ";" + WHITESPACE@12..13 " " + ERROR@13..16 + END_KW@13..16 "end" + WHITESPACE@16..17 "\n" + +error[syntax-error]: expected a statement, found BEGIN_KW + ╭▸ +2 │ begin null; end + ╰╴━ +error[syntax-error]: expected a statement, found END_KW + ╭▸ +2 │ begin null; end + ╰╴ ━ diff --git a/crates/squawk_syntax/src/test.rs b/crates/squawk_syntax/src/test.rs index 9bb1e8f3..ec4de767 100644 --- a/crates/squawk_syntax/src/test.rs +++ b/crates/squawk_syntax/src/test.rs @@ -116,6 +116,89 @@ fn plpgsql_bodies(parse: &Parse) -> Vec { .collect() } +fn plpgsql_fixture(sql: &str) -> (String, Vec) { + let parse = SourceFile::parse(sql); + assert!( + parse.errors().is_empty(), + "plpgsql fixtures must be valid sql:\n{}", + render_errors(sql, &parse.errors()) + ); + + let bodies = plpgsql_bodies(&parse); + assert!(!bodies.is_empty(), "no plpgsql bodies found"); + + let mut buffer = String::new(); + let mut errors = vec![]; + for body in &bodies { + if !buffer.is_empty() { + buffer.push_str("---\n"); + } + buffer.push_str(&format!("{:#?}", body.syntax())); + errors.extend_from_slice(body.errors()); + } + + if !errors.is_empty() { + buffer.push('\n'); + buffer.push_str(&render_errors(sql, &errors)); + } + + (buffer, errors) +} + +#[dir_test( + dir: "$CARGO_MANIFEST_DIR/../squawk_parser/tests/data/plpgsql/ok", + glob: "*.sql", +)] +fn plpgsql_ok(fixture: Fixture<&str>) { + let content = fixture.content(); + let input_file = Utf8Path::new(fixture.path()); + let test_name = input_file + .file_name() + .and_then(|x| x.strip_suffix(".sql")) + .unwrap(); + + let (buffer, errors) = plpgsql_fixture(content); + + with_settings!({ + omit_expression => true, + input_file => input_file, + }, { + assert_snapshot!(format!("plpgsql_{test_name}_ok"), buffer); + }); + + assert!( + errors.is_empty(), + "tests defined in `plpgsql/ok` can't have parser errors." + ); +} + +#[dir_test( + dir: "$CARGO_MANIFEST_DIR/../squawk_parser/tests/data/plpgsql/err", + glob: "*.sql", +)] +fn plpgsql_err(fixture: Fixture<&str>) { + let content = fixture.content(); + let input_file = Utf8Path::new(fixture.path()); + let test_name = input_file + .file_name() + .and_then(|x| x.strip_suffix(".sql")) + .unwrap(); + + let (buffer, errors) = plpgsql_fixture(content); + + with_settings!({ + omit_expression => true, + input_file => input_file, + }, { + assert_snapshot!(format!("plpgsql_{test_name}_err"), buffer); + }); + + assert!( + !errors.is_empty(), + "tests defined in `plpgsql/err` must have parser errors." + ); +} + fn token_counts(node: &SyntaxNode) -> (usize, usize) { let mut total = 0; let mut unparsed = 0; From f4e5a017641ef8567cb5d701148d3a1602b2b490 Mon Sep 17 00:00:00 2001 From: Steve Dignam Date: Wed, 16 Sep 2026 22:11:20 -0400 Subject: [PATCH 02/11] comment out meta --- crates/squawk_syntax/src/test.rs | 60 ++++++++++++++++------------- crates/xtask/src/sync_pg.rs | 19 +++++---- postgres/plpgsql/plpgsql_copy.sql | 32 +++++++-------- postgres/plpgsql/plpgsql_misc.sql | 4 +- postgres/plpgsql/plpgsql_record.sql | 18 ++++----- postgres/plpgsql/plpgsql_simple.sql | 2 +- 6 files changed, 73 insertions(+), 62 deletions(-) diff --git a/crates/squawk_syntax/src/test.rs b/crates/squawk_syntax/src/test.rs index ec4de767..c10fc4bb 100644 --- a/crates/squawk_syntax/src/test.rs +++ b/crates/squawk_syntax/src/test.rs @@ -228,31 +228,39 @@ fn plpgsql_suite_score() { .collect::>(); files.sort(); - let row = |label: &str, counts: [usize; 5]| { - let [sql, bodies, tokens, unparsed, err] = counts; - format!("{label:<25}{sql:>6}{bodies:>8}{tokens:>8}{unparsed:>10}{err:>6}\n") + let row = |label: &str, counts: [usize; 4]| { + let [bodies, tokens, unparsed, err] = counts; + format!("{label:<25}{bodies:>8}{tokens:>8}{unparsed:>10}{err:>6}\n") }; let mut table = format!( - "{:<25}{:>6}{:>8}{:>8}{:>10}{:>6}\n", - "file", "sql", "bodies", "tokens", "unparsed", "err" + "{:<25}{:>8}{:>8}{:>10}{:>6}\n", + "file", "bodies", "tokens", "unparsed", "err" ); - let mut totals = [0; 5]; + let mut totals = [0; 4]; for path in &files { let content = std::fs::read_to_string(path).unwrap(); let parse = SourceFile::parse(&content); + let file_name = path.file_name().unwrap(); + + assert!( + parse.errors().is_empty(), + "`{file_name}` must parse as sql, otherwise the tree can hide bodies:\n{}", + render_errors(&content, &parse.errors()) + ); + let bodies = plpgsql_bodies(&parse); - let mut counts = [parse.errors().len(), bodies.len(), 0, 0, 0]; + let mut counts = [bodies.len(), 0, 0, 0]; for body in &bodies { let (tokens, unparsed) = token_counts(&body.syntax()); - counts[2] += tokens; - counts[3] += unparsed; - counts[4] += body.errors().len(); + counts[1] += tokens; + counts[2] += unparsed; + counts[3] += body.errors().len(); } - table.push_str(&row(path.file_name().unwrap(), counts)); + table.push_str(&row(file_name, counts)); for (total, count) in totals.iter_mut().zip(counts) { *total += count; } @@ -261,21 +269,21 @@ fn plpgsql_suite_score() { table.push_str(&row("total", totals)); assert_snapshot!(table, @" - file sql bodies tokens unparsed err - plpgsql_array.sql 0 26 949 949 949 - plpgsql_cache.sql 0 2 60 60 60 - plpgsql_call.sql 0 45 1698 1696 1696 - plpgsql_control.sql 0 27 1424 1422 1422 - plpgsql_copy.sql 79 4 28 28 28 - plpgsql_domain.sql 0 23 307 307 307 - plpgsql_misc.sql 6 16 261 261 261 - plpgsql_record.sql 27 65 2008 2000 2000 - plpgsql_simple.sql 3 9 217 217 217 - plpgsql_transaction.sql 0 37 1203 1199 1199 - plpgsql_trap.sql 0 7 354 354 354 - plpgsql_trigger.sql 0 1 55 55 55 - plpgsql_varprops.sql 0 33 736 706 706 - total 115 295 9300 9254 9254 + file bodies tokens unparsed err + plpgsql_array.sql 26 949 949 949 + plpgsql_cache.sql 2 60 60 60 + plpgsql_call.sql 45 1698 1696 1696 + plpgsql_control.sql 27 1424 1422 1422 + plpgsql_copy.sql 4 28 28 28 + plpgsql_domain.sql 23 307 307 307 + plpgsql_misc.sql 16 261 261 261 + plpgsql_record.sql 65 2008 2000 2000 + plpgsql_simple.sql 9 217 217 217 + plpgsql_transaction.sql 37 1203 1199 1199 + plpgsql_trap.sql 7 354 354 354 + plpgsql_trigger.sql 1 55 55 55 + plpgsql_varprops.sql 33 736 706 706 + total 295 9300 9254 9254 "); } diff --git a/crates/xtask/src/sync_pg.rs b/crates/xtask/src/sync_pg.rs index 00f08e88..71298402 100644 --- a/crates/xtask/src/sync_pg.rs +++ b/crates/xtask/src/sync_pg.rs @@ -151,6 +151,9 @@ const IGNORED_LINES: &[&str] = &[ r#"SELECT x' 0';"#, r#"SELECT b' 0';"#, r#"SELECT b'0 ';"#, + // the psql variable this reads is set by a \set we comment out, so the body + // would otherwise parse as the literal string `dobody` + "DO LANGUAGE plpgsql :'dobody';", ]; const VARIABLE_REPLACEMENTS: &[(&str, &str)] = &[ @@ -304,13 +307,15 @@ fn sync_regression_suite(clone_dir: &Utf8Path) -> Result<()> { } files.sort(); - let total_files = files.len(); + preprocess_files(&files, &output_dir) +} +fn preprocess_files(files: &[Utf8PathBuf], output_dir: &Utf8Path) -> Result<()> { for (index, input_path) in files.iter().enumerate() { let filename = input_path.file_name().unwrap(); let output_path = output_dir.join(filename); - println!("[{}/{}] Processing {}...", index + 1, total_files, filename); + println!("[{}/{}] Processing {}...", index + 1, files.len(), filename); let input_file = File::open(input_path)?; let reader = std::io::BufReader::new(input_file); @@ -338,19 +343,17 @@ fn sync_plpgsql_suite(clone_dir: &Utf8Path) -> Result<()> { } create_dir_all(&output_dir)?; - let mut file_count = 0; + let mut files: Vec = vec![]; for entry in std::fs::read_dir(&source_dir)? { let entry = entry?; let path = Utf8PathBuf::try_from(entry.path())?; if path.extension() == Some("sql") { - let filename = path.file_name().unwrap(); - std::fs::copy(&path, output_dir.join(filename))?; - file_count += 1; + files.push(path); } } - println!("Copied {file_count} PL/pgSQL files to {output_dir}"); - Ok(()) + files.sort(); + preprocess_files(&files, &output_dir) } // The regression suite from postgres has a mix of valid and invalid sql. We diff --git a/postgres/plpgsql/plpgsql_copy.sql b/postgres/plpgsql/plpgsql_copy.sql index 37f1fa13..5374cb33 100644 --- a/postgres/plpgsql/plpgsql_copy.sql +++ b/postgres/plpgsql/plpgsql_copy.sql @@ -1,10 +1,10 @@ -- directory paths are passed to us in environment variables -\getenv abs_srcdir PG_ABS_SRCDIR -\getenv abs_builddir PG_ABS_BUILDDIR +-- \getenv abs_srcdir PG_ABS_SRCDIR +-- \getenv abs_builddir PG_ABS_BUILDDIR -- set up file names to use -\set srcfilename :abs_srcdir '/data/copy1.data' -\set destfilename :abs_builddir '/results/copy1.data' +-- \set srcfilename :abs_srcdir '/data/copy1.data' +-- \set destfilename :abs_builddir '/results/copy1.data' CREATE TABLE copy1 (a int, b float); @@ -32,26 +32,26 @@ $$; -- Valid cases -- COPY FROM -\set dobody 'BEGIN COPY copy1 FROM ' :'srcfilename' '; END' -DO LANGUAGE plpgsql :'dobody'; +-- \set dobody 'BEGIN COPY copy1 FROM ' :'srcfilename' '; END' +-- DO LANGUAGE plpgsql :'dobody'; SELECT * FROM copy1 ORDER BY 1; TRUNCATE copy1; -\set cmd 'COPY copy1 FROM ' :'srcfilename' -\set dobody 'BEGIN EXECUTE ' :'cmd' '; END' -DO LANGUAGE plpgsql :'dobody'; +-- \set cmd 'COPY copy1 FROM ' :'srcfilename' +-- \set dobody 'BEGIN EXECUTE ' :'cmd' '; END' +-- DO LANGUAGE plpgsql :'dobody'; SELECT * FROM copy1 ORDER BY 1; -- COPY TO -- Copy the data externally once, then process it back to the table. -\set dobody 'BEGIN COPY copy1 TO ' :'destfilename' '; END' -DO LANGUAGE plpgsql :'dobody'; +-- \set dobody 'BEGIN COPY copy1 TO ' :'destfilename' '; END' +-- DO LANGUAGE plpgsql :'dobody'; TRUNCATE copy1; -\set dobody 'BEGIN COPY copy1 FROM ' :'destfilename' '; END' -DO LANGUAGE plpgsql :'dobody'; +-- \set dobody 'BEGIN COPY copy1 FROM ' :'destfilename' '; END' +-- DO LANGUAGE plpgsql :'dobody'; -\set cmd 'COPY copy1 FROM ' :'destfilename' -\set dobody 'BEGIN EXECUTE ' :'cmd' '; END' -DO LANGUAGE plpgsql :'dobody'; +-- \set cmd 'COPY copy1 FROM ' :'destfilename' +-- \set dobody 'BEGIN EXECUTE ' :'cmd' '; END' +-- DO LANGUAGE plpgsql :'dobody'; SELECT * FROM copy1 ORDER BY 1; diff --git a/postgres/plpgsql/plpgsql_misc.sql b/postgres/plpgsql/plpgsql_misc.sql index 0bc39fcf..d4d3c79c 100644 --- a/postgres/plpgsql/plpgsql_misc.sql +++ b/postgres/plpgsql/plpgsql_misc.sql @@ -18,8 +18,8 @@ $$ end $$; -\sf test1 -\sf test2 +-- \sf test1 +-- \sf test2 -- Test %TYPE and %ROWTYPE error cases create table misc_table(f1 int); diff --git a/postgres/plpgsql/plpgsql_record.sql b/postgres/plpgsql/plpgsql_record.sql index 4fbed38b..5b7809ab 100644 --- a/postgres/plpgsql/plpgsql_record.sql +++ b/postgres/plpgsql/plpgsql_record.sql @@ -302,9 +302,9 @@ select getf1(row(1,2)::two_int4s); select getf1(row('foo',123,456)::more_int4s); -- the context stack is different when debug_discard_caches -- is set, so suppress context output -\set SHOW_CONTEXT never +-- \set SHOW_CONTEXT never select getf1(row(1,2)::two_int8s); -\set SHOW_CONTEXT errors +-- \set SHOW_CONTEXT errors select getf1(row(1,2)); -- this seemingly-equivalent case behaves a bit differently, @@ -377,9 +377,9 @@ select sillyaddone(42); alter table mutable drop column f1; -- the context stack is different when debug_discard_caches -- is set, so suppress context output -\set SHOW_CONTEXT never +-- \set SHOW_CONTEXT never select sillyaddone(42); -- fail -\set SHOW_CONTEXT errors +-- \set SHOW_CONTEXT errors create function getf3(x mutable) returns int language plpgsql as $$ begin return x.f3; end $$; @@ -389,9 +389,9 @@ select getf3(null::mutable); -- now it works alter table mutable drop column f3; -- the context stack is different when debug_discard_caches -- is set, so suppress context output -\set SHOW_CONTEXT never +-- \set SHOW_CONTEXT never select getf3(null::mutable); -- fails again -\set SHOW_CONTEXT errors +-- \set SHOW_CONTEXT errors -- check behavior with creating/dropping a named rowtype set check_function_bodies = off; -- else reference to nonexistent type fails @@ -407,9 +407,9 @@ select sillyaddtwo(42); drop table mutable2; -- the context stack is different when debug_discard_caches -- is set, so suppress context output -\set SHOW_CONTEXT never +-- \set SHOW_CONTEXT never select sillyaddtwo(42); -- fail -\set SHOW_CONTEXT errors +-- \set SHOW_CONTEXT errors create table mutable2(f0 text, f1 int, f2 text); select sillyaddtwo(42); select sillyaddtwo(43); @@ -575,5 +575,5 @@ $$ declare r record; begin r := row($1,$1); return r; end $$; create table two_int8s_tab (f1 two_int8s); insert into two_int8s_tab values (compresult(42)); -- reconnect so we lose any local knowledge of anonymous record types -\c - +-- \c - table two_int8s_tab; diff --git a/postgres/plpgsql/plpgsql_simple.sql b/postgres/plpgsql/plpgsql_simple.sql index d64e7918..d03d092d 100644 --- a/postgres/plpgsql/plpgsql_simple.sql +++ b/postgres/plpgsql/plpgsql_simple.sql @@ -56,7 +56,7 @@ alter function simpletarget(int) immutable; select simplecaller(); -- make sure flushing local caches changes nothing -\c - +-- \c - select simplecaller(); From 3e4fdaaa39088ea3a83c7427a0e7f1055852c0cd Mon Sep 17 00:00:00 2001 From: Steve Dignam Date: Wed, 16 Sep 2026 23:39:11 -0400 Subject: [PATCH 03/11] another test --- crates/squawk_syntax/src/test.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/crates/squawk_syntax/src/test.rs b/crates/squawk_syntax/src/test.rs index c10fc4bb..0a9bc4d6 100644 --- a/crates/squawk_syntax/src/test.rs +++ b/crates/squawk_syntax/src/test.rs @@ -220,13 +220,14 @@ fn token_counts(node: &SyntaxNode) -> (usize, usize) { #[test] fn plpgsql_suite_score() { - let dir = Utf8Path::new(env!("CARGO_MANIFEST_DIR")).join("../../postgres/plpgsql"); - let mut files = std::fs::read_dir(&dir) + let root = Utf8Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); + let mut files = std::fs::read_dir(root.join("postgres/plpgsql")) .unwrap() .map(|entry| Utf8PathBuf::try_from(entry.unwrap().path()).unwrap()) .filter(|path| path.extension() == Some("sql")) .collect::>(); - files.sort(); + files.push(root.join("postgres/regression_suite/plpgsql.sql")); + files.sort_by(|a, b| a.file_name().cmp(&b.file_name())); let row = |label: &str, counts: [usize; 4]| { let [bodies, tokens, unparsed, err] = counts; @@ -270,6 +271,7 @@ fn plpgsql_suite_score() { assert_snapshot!(table, @" file bodies tokens unparsed err + plpgsql.sql 254 9965 9931 9931 plpgsql_array.sql 26 949 949 949 plpgsql_cache.sql 2 60 60 60 plpgsql_call.sql 45 1698 1696 1696 @@ -283,7 +285,7 @@ fn plpgsql_suite_score() { plpgsql_trap.sql 7 354 354 354 plpgsql_trigger.sql 1 55 55 55 plpgsql_varprops.sql 33 736 706 706 - total 295 9300 9254 9254 + total 549 19265 19185 19185 "); } From 4f1563098496dfb7490f482838784b4c3f72b6be Mon Sep 17 00:00:00 2001 From: Steve Dignam Date: Thu, 17 Sep 2026 00:34:24 -0400 Subject: [PATCH 04/11] block & partial declare --- .../src/generated/syntax_kind.rs | 3 + crates/squawk_parser/src/lib.rs | 1 - crates/squawk_parser/src/plpgsql_grammar.rs | 88 +++++++++- .../tests/data/plpgsql/err/missing_end.sql | 4 + .../data/plpgsql/err/unrecognized_stmt.sql | 3 - .../tests/data/plpgsql/ok/block.sql | 20 +++ .../tests/data/plpgsql/ok/null_stmt.sql | 10 +- .../squawk_syntax/src/ast/generated/nodes.rs | 152 ++++++++++++++++- crates/squawk_syntax/src/plpgsql.rs | 156 +++++++++++------- crates/squawk_syntax/src/postgresql.ungram | 19 ++- ...squawk_syntax__test__plpgsql_block_ok.snap | 52 ++++++ ...syntax__test__plpgsql_missing_end_err.snap | 19 +++ ...wk_syntax__test__plpgsql_null_stmt_ok.snap | 31 ++-- ...__test__plpgsql_unrecognized_stmt_err.snap | 25 --- crates/squawk_syntax/src/test.rs | 30 ++-- 15 files changed, 475 insertions(+), 138 deletions(-) create mode 100644 crates/squawk_parser/tests/data/plpgsql/err/missing_end.sql delete mode 100644 crates/squawk_parser/tests/data/plpgsql/err/unrecognized_stmt.sql create mode 100644 crates/squawk_parser/tests/data/plpgsql/ok/block.sql create mode 100644 crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_block_ok.snap create mode 100644 crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_missing_end_err.snap delete mode 100644 crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_unrecognized_stmt_err.snap diff --git a/crates/squawk_parser/src/generated/syntax_kind.rs b/crates/squawk_parser/src/generated/syntax_kind.rs index dd613d90..9386d2d6 100644 --- a/crates/squawk_parser/src/generated/syntax_kind.rs +++ b/crates/squawk_parser/src/generated/syntax_kind.rs @@ -1504,6 +1504,9 @@ pub enum SyntaxKind { PERCENT_TYPE_CLAUSE, PERIOD_COLUMN, PLPGSQL, + PLPGSQL_BLOCK, + PLPGSQL_BODY, + PLPGSQL_DECLARE_SECTION, PLPGSQL_NULL_STMT, POLICY, POLICY_COMMAND, diff --git a/crates/squawk_parser/src/lib.rs b/crates/squawk_parser/src/lib.rs index ea767f9f..dfbf4b0a 100644 --- a/crates/squawk_parser/src/lib.rs +++ b/crates/squawk_parser/src/lib.rs @@ -597,7 +597,6 @@ impl<'t> Parser<'t> { } /// Checks if the nth token is contextual keyword `kw`. - #[expect(dead_code, reason = "used by the PL/pgSQL grammar")] pub(crate) fn nth_at_contextual_kw(&self, n: usize, kw: SyntaxKind) -> bool { self.inp.contextual_kind(self.pos + n) == kw } diff --git a/crates/squawk_parser/src/plpgsql_grammar.rs b/crates/squawk_parser/src/plpgsql_grammar.rs index b9749ca4..ad7c2bf7 100644 --- a/crates/squawk_parser/src/plpgsql_grammar.rs +++ b/crates/squawk_parser/src/plpgsql_grammar.rs @@ -1,17 +1,89 @@ -use crate::{Parser, syntax_kind::SyntaxKind::*}; +use crate::{Parser, syntax_kind::SyntaxKind::*, token_set::TokenSet}; pub(crate) fn plpgsql_entry_point(p: &mut Parser) { let m = p.start(); while !p.at(EOF) { - if p.at(NULL_KW) && p.nth_at(1, SEMICOLON) { - let m = p.start(); - p.bump(NULL_KW); - p.bump(SEMICOLON); - m.complete(p, PLPGSQL_NULL_STMT); + if p.at_ts(BLOCK_FIRST) { + block(p); } else { - let kind = p.current(); - p.err_and_bump(&format!("expected a statement, found {kind:?}")); + temp_unknown(p, "expected a block"); } } m.complete(p, PLPGSQL); } + +const BLOCK_FIRST: TokenSet = TokenSet::new(&[BEGIN_KW, DECLARE_KW]); + +fn block(p: &mut Parser) { + assert!(p.at_ts(BLOCK_FIRST)); + let m = p.start(); + opt_declare_section(p); + p.expect(BEGIN_KW); + body(p); + p.expect(END_KW); + // TODO: add validation, sometimes this is required + p.eat(SEMICOLON); + m.complete(p, PLPGSQL_BLOCK); +} + +fn opt_declare_section(p: &mut Parser) { + if !p.at(DECLARE_KW) { + return; + } + let m = p.start(); + p.bump(DECLARE_KW); + while !p.at(EOF) && !p.at(BEGIN_KW) { + temp_unknown(p, "expected a declaration"); + } + m.complete(p, PLPGSQL_DECLARE_SECTION); +} + +fn body(p: &mut Parser) { + let m = p.start(); + while !p.at(EOF) && !at_block_end(p) { + stmt(p); + } + m.complete(p, PLPGSQL_BODY); +} + +fn stmt(p: &mut Parser) { + if p.at_ts(BLOCK_FIRST) { + block(p); + } else if p.at(NULL_KW) && p.nth_at(1, SEMICOLON) { + let m = p.start(); + p.bump(NULL_KW); + p.bump(SEMICOLON); + m.complete(p, PLPGSQL_NULL_STMT); + } else { + temp_unknown(p, "expected a statement"); + } +} + +// TODO: remove this once we get all the ast nodes working +fn temp_unknown(p: &mut Parser, message: &str) { + let m = p.start(); + let kind = p.current(); + p.error(format!("{message}, found {kind:?}")); + + let mut depth = 0; + while !p.at(EOF) { + if depth == 0 && p.at(SEMICOLON) { + break; + } + if p.at(BEGIN_KW) { + depth += 1; + } else if p.at(END_KW) && depth > 0 { + depth -= 1; + } + p.bump_any(); + } + p.eat(SEMICOLON); + m.complete(p, ERROR); +} + +fn at_block_end(p: &Parser) -> bool { + if !p.at(END_KW) { + return false; + } + !p.nth_at(1, IF_KW) && !p.nth_at(1, CASE_KW) && !p.nth_at_contextual_kw(1, LOOP_KW) +} diff --git a/crates/squawk_parser/tests/data/plpgsql/err/missing_end.sql b/crates/squawk_parser/tests/data/plpgsql/err/missing_end.sql new file mode 100644 index 00000000..3fa0e7e7 --- /dev/null +++ b/crates/squawk_parser/tests/data/plpgsql/err/missing_end.sql @@ -0,0 +1,4 @@ +do $$ +begin + null; +$$; diff --git a/crates/squawk_parser/tests/data/plpgsql/err/unrecognized_stmt.sql b/crates/squawk_parser/tests/data/plpgsql/err/unrecognized_stmt.sql deleted file mode 100644 index ebf51ccc..00000000 --- a/crates/squawk_parser/tests/data/plpgsql/err/unrecognized_stmt.sql +++ /dev/null @@ -1,3 +0,0 @@ -do $$ -begin null; end -$$; diff --git a/crates/squawk_parser/tests/data/plpgsql/ok/block.sql b/crates/squawk_parser/tests/data/plpgsql/ok/block.sql new file mode 100644 index 00000000..326ccbd1 --- /dev/null +++ b/crates/squawk_parser/tests/data/plpgsql/ok/block.sql @@ -0,0 +1,20 @@ +do $$ +begin + begin + null; + end; +end +$$; + +create function f() returns int language plpgsql as $$ +begin + null; +end +$$; + +do $$ +declare +begin + null; +end +$$; diff --git a/crates/squawk_parser/tests/data/plpgsql/ok/null_stmt.sql b/crates/squawk_parser/tests/data/plpgsql/ok/null_stmt.sql index bfd70bd0..d9054f2a 100644 --- a/crates/squawk_parser/tests/data/plpgsql/ok/null_stmt.sql +++ b/crates/squawk_parser/tests/data/plpgsql/ok/null_stmt.sql @@ -1,8 +1,6 @@ do $$ -null; -$$; - -do $$ -null; -null; +begin + null; + null; +end $$; diff --git a/crates/squawk_syntax/src/ast/generated/nodes.rs b/crates/squawk_syntax/src/ast/generated/nodes.rs index c57660f9..29148d73 100644 --- a/crates/squawk_syntax/src/ast/generated/nodes.rs +++ b/crates/squawk_syntax/src/ast/generated/nodes.rs @@ -19399,11 +19399,60 @@ pub struct Plpgsql { } impl Plpgsql { #[inline] - pub fn stmts(&self) -> AstChildren { + pub fn plpgsql_block(&self) -> Option { + support::child(&self.syntax) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct PlpgsqlBlock { + pub(crate) syntax: SyntaxNode, +} +impl PlpgsqlBlock { + #[inline] + pub fn plpgsql_body(&self) -> Option { + support::child(&self.syntax) + } + #[inline] + pub fn plpgsql_declare_section(&self) -> Option { + support::child(&self.syntax) + } + #[inline] + pub fn semicolon_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::SEMICOLON) + } + #[inline] + pub fn begin_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::BEGIN_KW) + } + #[inline] + pub fn end_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::END_KW) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct PlpgsqlBody { + pub(crate) syntax: SyntaxNode, +} +impl PlpgsqlBody { + #[inline] + pub fn stmts(&self) -> AstChildren { support::children(&self.syntax) } } +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct PlpgsqlDeclareSection { + pub(crate) syntax: SyntaxNode, +} +impl PlpgsqlDeclareSection { + #[inline] + pub fn declare_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::DECLARE_KW) + } +} + #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct PlpgsqlNullStmt { pub(crate) syntax: SyntaxNode, @@ -29426,6 +29475,12 @@ pub enum Persistence { Unlogged(Unlogged), } +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum PlpgsqlStmt { + PlpgsqlBlock(PlpgsqlBlock), + PlpgsqlNullStmt(PlpgsqlNullStmt), +} + #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum PolicyCommandKind { PolicyCommandAll(PolicyCommandAll), @@ -46054,6 +46109,60 @@ impl AstNode for Plpgsql { &self.syntax } } +impl AstNode for PlpgsqlBlock { + #[inline] + fn can_cast(kind: SyntaxKind) -> bool { + kind == SyntaxKind::PLPGSQL_BLOCK + } + #[inline] + fn cast(syntax: SyntaxNode) -> Option { + if Self::can_cast(syntax.kind()) { + Some(Self { syntax }) + } else { + None + } + } + #[inline] + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +impl AstNode for PlpgsqlBody { + #[inline] + fn can_cast(kind: SyntaxKind) -> bool { + kind == SyntaxKind::PLPGSQL_BODY + } + #[inline] + fn cast(syntax: SyntaxNode) -> Option { + if Self::can_cast(syntax.kind()) { + Some(Self { syntax }) + } else { + None + } + } + #[inline] + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +impl AstNode for PlpgsqlDeclareSection { + #[inline] + fn can_cast(kind: SyntaxKind) -> bool { + kind == SyntaxKind::PLPGSQL_DECLARE_SECTION + } + #[inline] + fn cast(syntax: SyntaxNode) -> Option { + if Self::can_cast(syntax.kind()) { + Some(Self { syntax }) + } else { + None + } + } + #[inline] + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} impl AstNode for PlpgsqlNullStmt { #[inline] fn can_cast(kind: SyntaxKind) -> bool { @@ -65755,6 +65864,47 @@ impl From for Persistence { Persistence::Unlogged(node) } } +impl AstNode for PlpgsqlStmt { + #[inline] + fn can_cast(kind: SyntaxKind) -> bool { + matches!( + kind, + SyntaxKind::PLPGSQL_BLOCK | SyntaxKind::PLPGSQL_NULL_STMT + ) + } + #[inline] + fn cast(syntax: SyntaxNode) -> Option { + let res = match syntax.kind() { + SyntaxKind::PLPGSQL_BLOCK => PlpgsqlStmt::PlpgsqlBlock(PlpgsqlBlock { syntax }), + SyntaxKind::PLPGSQL_NULL_STMT => { + PlpgsqlStmt::PlpgsqlNullStmt(PlpgsqlNullStmt { syntax }) + } + _ => { + return None; + } + }; + Some(res) + } + #[inline] + fn syntax(&self) -> &SyntaxNode { + match self { + PlpgsqlStmt::PlpgsqlBlock(it) => &it.syntax, + PlpgsqlStmt::PlpgsqlNullStmt(it) => &it.syntax, + } + } +} +impl From for PlpgsqlStmt { + #[inline] + fn from(node: PlpgsqlBlock) -> PlpgsqlStmt { + PlpgsqlStmt::PlpgsqlBlock(node) + } +} +impl From for PlpgsqlStmt { + #[inline] + fn from(node: PlpgsqlNullStmt) -> PlpgsqlStmt { + PlpgsqlStmt::PlpgsqlNullStmt(node) + } +} impl AstNode for PolicyCommandKind { #[inline] fn can_cast(kind: SyntaxKind) -> bool { diff --git a/crates/squawk_syntax/src/plpgsql.rs b/crates/squawk_syntax/src/plpgsql.rs index 36e8e1c3..6c71b209 100644 --- a/crates/squawk_syntax/src/plpgsql.rs +++ b/crates/squawk_syntax/src/plpgsql.rs @@ -140,16 +140,22 @@ mod tests { #[test] fn language_after_as() { assert_snapshot!( - body("create function f() returns int as $$ null; $$ language plpgsql;"), + body("create function f() returns int as $$ begin null; end $$ language plpgsql;"), @r#" - PLPGSQL@0..7 + PLPGSQL@0..17 WHITESPACE@0..1 " " - PLPGSQL_NULL_STMT@1..6 - NULL_KW@1..5 "null" - SEMICOLON@5..6 ";" - WHITESPACE@6..7 " " + PLPGSQL_BLOCK@1..16 + BEGIN_KW@1..6 "begin" + WHITESPACE@6..7 " " + PLPGSQL_BODY@7..12 + PLPGSQL_NULL_STMT@7..12 + NULL_KW@7..11 "null" + SEMICOLON@11..12 ";" + WHITESPACE@12..13 " " + END_KW@13..16 "end" + WHITESPACE@16..17 " " --- - source 37..44 " null; " + source 37..54 " begin null; end " "# ); } @@ -157,16 +163,22 @@ mod tests { #[test] fn language_before_as() { assert_snapshot!( - body("create function f() returns int language plpgsql as $$ null; $$;"), + body("create function f() returns int language plpgsql as $$ begin null; end $$;"), @r#" - PLPGSQL@0..7 + PLPGSQL@0..17 WHITESPACE@0..1 " " - PLPGSQL_NULL_STMT@1..6 - NULL_KW@1..5 "null" - SEMICOLON@5..6 ";" - WHITESPACE@6..7 " " + PLPGSQL_BLOCK@1..16 + BEGIN_KW@1..6 "begin" + WHITESPACE@6..7 " " + PLPGSQL_BODY@7..12 + PLPGSQL_NULL_STMT@7..12 + NULL_KW@7..11 "null" + SEMICOLON@11..12 ";" + WHITESPACE@12..13 " " + END_KW@13..16 "end" + WHITESPACE@16..17 " " --- - source 54..61 " null; " + source 54..71 " begin null; end " "# ); } @@ -179,31 +191,43 @@ mod tests { #[test] fn procedure() { assert_snapshot!( - body("create procedure p() as $$ null; $$ language plpgsql;"), + body("create procedure p() as $$ begin null; end $$ language plpgsql;"), @r#" - PLPGSQL@0..7 + PLPGSQL@0..17 WHITESPACE@0..1 " " - PLPGSQL_NULL_STMT@1..6 - NULL_KW@1..5 "null" - SEMICOLON@5..6 ";" - WHITESPACE@6..7 " " + PLPGSQL_BLOCK@1..16 + BEGIN_KW@1..6 "begin" + WHITESPACE@6..7 " " + PLPGSQL_BODY@7..12 + PLPGSQL_NULL_STMT@7..12 + NULL_KW@7..11 "null" + SEMICOLON@11..12 ";" + WHITESPACE@12..13 " " + END_KW@13..16 "end" + WHITESPACE@16..17 " " --- - source 26..33 " null; " + source 26..43 " begin null; end " "# ); } #[test] fn do_defaults_to_plpgsql() { - assert_snapshot!(body("do $$ null; $$;"), @r#" - PLPGSQL@0..7 + assert_snapshot!(body("do $$ begin null; end $$;"), @r#" + PLPGSQL@0..17 WHITESPACE@0..1 " " - PLPGSQL_NULL_STMT@1..6 - NULL_KW@1..5 "null" - SEMICOLON@5..6 ";" - WHITESPACE@6..7 " " + PLPGSQL_BLOCK@1..16 + BEGIN_KW@1..6 "begin" + WHITESPACE@6..7 " " + PLPGSQL_BODY@7..12 + PLPGSQL_NULL_STMT@7..12 + NULL_KW@7..11 "null" + SEMICOLON@11..12 ";" + WHITESPACE@12..13 " " + END_KW@13..16 "end" + WHITESPACE@16..17 " " --- - source 5..12 " null; " + source 5..22 " begin null; end " "# ); } @@ -216,58 +240,66 @@ mod tests { #[test] fn escaped_body_maps_back_through_the_escapes() { assert_snapshot!( - body(r"create function f() returns int as E'null;\n' language plpgsql;"), + body(r"create function f() returns int as E'begin null; end\n' language plpgsql;"), @r#" - PLPGSQL@0..6 - PLPGSQL_NULL_STMT@0..5 - NULL_KW@0..4 "null" - SEMICOLON@4..5 ";" - WHITESPACE@5..6 "\n" + PLPGSQL@0..16 + PLPGSQL_BLOCK@0..15 + BEGIN_KW@0..5 "begin" + WHITESPACE@5..6 " " + PLPGSQL_BODY@6..11 + PLPGSQL_NULL_STMT@6..11 + NULL_KW@6..10 "null" + SEMICOLON@10..11 ";" + WHITESPACE@11..12 " " + END_KW@12..15 "end" + WHITESPACE@15..16 "\n" --- - source 37..44 "null;\\n" + source 37..54 "begin null; end\\n" "# ); } #[test] fn unparsed_tokens_are_errors() { - assert_snapshot!(body("do $$ begin null; end $$;"), @r#" - PLPGSQL@0..17 + assert_snapshot!(body("do $$ begin perform 1; end $$;"), @r#" + PLPGSQL@0..22 WHITESPACE@0..1 " " - ERROR@1..6 + PLPGSQL_BLOCK@1..21 BEGIN_KW@1..6 "begin" - WHITESPACE@6..7 " " - PLPGSQL_NULL_STMT@7..12 - NULL_KW@7..11 "null" - SEMICOLON@11..12 ";" - WHITESPACE@12..13 " " - ERROR@13..16 - END_KW@13..16 "end" - WHITESPACE@16..17 " " + WHITESPACE@6..7 " " + PLPGSQL_BODY@7..17 + ERROR@7..17 + IDENT@7..14 "perform" + WHITESPACE@14..15 " " + INT_NUMBER@15..16 "1" + SEMICOLON@16..17 ";" + WHITESPACE@17..18 " " + END_KW@18..21 "end" + WHITESPACE@21..22 " " --- - source 5..22 " begin null; end " - error 6..6 expected a statement, found BEGIN_KW - error 18..18 expected a statement, found END_KW + source 5..27 " begin perform 1; end " + error 12..12 expected a statement, found IDENT "#); } #[test] fn errors_in_an_escaped_body_map_into_the_file() { - assert_snapshot!(body(r"do E'begin\n null;\n end';"), @r#" - PLPGSQL@0..17 - ERROR@0..5 + assert_snapshot!(body(r"do E'begin\n perform 1;\n end';"), @r#" + PLPGSQL@0..22 + PLPGSQL_BLOCK@0..22 BEGIN_KW@0..5 "begin" - WHITESPACE@5..7 "\n " - PLPGSQL_NULL_STMT@7..12 - NULL_KW@7..11 "null" - SEMICOLON@11..12 ";" - WHITESPACE@12..14 "\n " - ERROR@14..17 - END_KW@14..17 "end" + WHITESPACE@5..7 "\n " + PLPGSQL_BODY@7..17 + ERROR@7..17 + IDENT@7..14 "perform" + WHITESPACE@14..15 " " + INT_NUMBER@15..16 "1" + SEMICOLON@16..17 ";" + WHITESPACE@17..19 "\n " + END_KW@19..22 "end" --- - source 5..24 "begin\\n null;\\n end" - error 5..5 expected a statement, found BEGIN_KW - error 21..21 expected a statement, found END_KW + source 5..29 "begin\\n perform 1;\\n end" + error 13..13 expected a statement, found IDENT "#); } } diff --git a/crates/squawk_syntax/src/postgresql.ungram b/crates/squawk_syntax/src/postgresql.ungram index c11012a4..715cdf3f 100644 --- a/crates/squawk_syntax/src/postgresql.ungram +++ b/crates/squawk_syntax/src/postgresql.ungram @@ -2037,7 +2037,24 @@ RoutineBody = | AtomicBody Plpgsql = - stmts:(PlpgsqlNullStmt*) + PlpgsqlBlock? + +PlpgsqlBlock = + PlpgsqlDeclareSection? + 'begin' + PlpgsqlBody + 'end' + ';'? + +PlpgsqlDeclareSection = + 'declare' + +PlpgsqlBody = + stmts:(PlpgsqlStmt*) + +PlpgsqlStmt = + PlpgsqlBlock +| PlpgsqlNullStmt PlpgsqlNullStmt = 'null' ';' diff --git a/crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_block_ok.snap b/crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_block_ok.snap new file mode 100644 index 00000000..edb68537 --- /dev/null +++ b/crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_block_ok.snap @@ -0,0 +1,52 @@ +--- +source: crates/squawk_syntax/src/test.rs +input_file: crates/squawk_parser/tests/data/plpgsql/ok/block.sql +--- +PLPGSQL@0..36 + WHITESPACE@0..1 "\n" + PLPGSQL_BLOCK@1..35 + BEGIN_KW@1..6 "begin" + WHITESPACE@6..9 "\n " + PLPGSQL_BODY@9..31 + PLPGSQL_BLOCK@9..31 + BEGIN_KW@9..14 "begin" + WHITESPACE@14..19 "\n " + PLPGSQL_BODY@19..24 + PLPGSQL_NULL_STMT@19..24 + NULL_KW@19..23 "null" + SEMICOLON@23..24 ";" + WHITESPACE@24..27 "\n " + END_KW@27..30 "end" + SEMICOLON@30..31 ";" + WHITESPACE@31..32 "\n" + END_KW@32..35 "end" + WHITESPACE@35..36 "\n" +--- +PLPGSQL@0..19 + WHITESPACE@0..1 "\n" + PLPGSQL_BLOCK@1..18 + BEGIN_KW@1..6 "begin" + WHITESPACE@6..9 "\n " + PLPGSQL_BODY@9..14 + PLPGSQL_NULL_STMT@9..14 + NULL_KW@9..13 "null" + SEMICOLON@13..14 ";" + WHITESPACE@14..15 "\n" + END_KW@15..18 "end" + WHITESPACE@18..19 "\n" +--- +PLPGSQL@0..27 + WHITESPACE@0..1 "\n" + PLPGSQL_BLOCK@1..26 + PLPGSQL_DECLARE_SECTION@1..8 + DECLARE_KW@1..8 "declare" + WHITESPACE@8..9 "\n" + BEGIN_KW@9..14 "begin" + WHITESPACE@14..17 "\n " + PLPGSQL_BODY@17..22 + PLPGSQL_NULL_STMT@17..22 + NULL_KW@17..21 "null" + SEMICOLON@21..22 ";" + WHITESPACE@22..23 "\n" + END_KW@23..26 "end" + WHITESPACE@26..27 "\n" diff --git a/crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_missing_end_err.snap b/crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_missing_end_err.snap new file mode 100644 index 00000000..97a25717 --- /dev/null +++ b/crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_missing_end_err.snap @@ -0,0 +1,19 @@ +--- +source: crates/squawk_syntax/src/test.rs +input_file: crates/squawk_parser/tests/data/plpgsql/err/missing_end.sql +--- +PLPGSQL@0..15 + WHITESPACE@0..1 "\n" + PLPGSQL_BLOCK@1..14 + BEGIN_KW@1..6 "begin" + WHITESPACE@6..9 "\n " + PLPGSQL_BODY@9..14 + PLPGSQL_NULL_STMT@9..14 + NULL_KW@9..13 "null" + SEMICOLON@13..14 ";" + WHITESPACE@14..15 "\n" + +error[syntax-error]: expected END_KW + ╭▸ +3 │ null; + ╰╴ ━ diff --git a/crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_null_stmt_ok.snap b/crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_null_stmt_ok.snap index b5a0af44..a4181e3c 100644 --- a/crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_null_stmt_ok.snap +++ b/crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_null_stmt_ok.snap @@ -2,20 +2,19 @@ source: crates/squawk_syntax/src/test.rs input_file: crates/squawk_parser/tests/data/plpgsql/ok/null_stmt.sql --- -PLPGSQL@0..7 +PLPGSQL@0..27 WHITESPACE@0..1 "\n" - PLPGSQL_NULL_STMT@1..6 - NULL_KW@1..5 "null" - SEMICOLON@5..6 ";" - WHITESPACE@6..7 "\n" ---- -PLPGSQL@0..13 - WHITESPACE@0..1 "\n" - PLPGSQL_NULL_STMT@1..6 - NULL_KW@1..5 "null" - SEMICOLON@5..6 ";" - WHITESPACE@6..7 "\n" - PLPGSQL_NULL_STMT@7..12 - NULL_KW@7..11 "null" - SEMICOLON@11..12 ";" - WHITESPACE@12..13 "\n" + PLPGSQL_BLOCK@1..26 + BEGIN_KW@1..6 "begin" + WHITESPACE@6..9 "\n " + PLPGSQL_BODY@9..22 + PLPGSQL_NULL_STMT@9..14 + NULL_KW@9..13 "null" + SEMICOLON@13..14 ";" + WHITESPACE@14..17 "\n " + PLPGSQL_NULL_STMT@17..22 + NULL_KW@17..21 "null" + SEMICOLON@21..22 ";" + WHITESPACE@22..23 "\n" + END_KW@23..26 "end" + WHITESPACE@26..27 "\n" diff --git a/crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_unrecognized_stmt_err.snap b/crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_unrecognized_stmt_err.snap deleted file mode 100644 index 9c2b6c89..00000000 --- a/crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_unrecognized_stmt_err.snap +++ /dev/null @@ -1,25 +0,0 @@ ---- -source: crates/squawk_syntax/src/test.rs -input_file: crates/squawk_parser/tests/data/plpgsql/err/unrecognized_stmt.sql ---- -PLPGSQL@0..17 - WHITESPACE@0..1 "\n" - ERROR@1..6 - BEGIN_KW@1..6 "begin" - WHITESPACE@6..7 " " - PLPGSQL_NULL_STMT@7..12 - NULL_KW@7..11 "null" - SEMICOLON@11..12 ";" - WHITESPACE@12..13 " " - ERROR@13..16 - END_KW@13..16 "end" - WHITESPACE@16..17 "\n" - -error[syntax-error]: expected a statement, found BEGIN_KW - ╭▸ -2 │ begin null; end - ╰╴━ -error[syntax-error]: expected a statement, found END_KW - ╭▸ -2 │ begin null; end - ╰╴ ━ diff --git a/crates/squawk_syntax/src/test.rs b/crates/squawk_syntax/src/test.rs index 0a9bc4d6..59c9b825 100644 --- a/crates/squawk_syntax/src/test.rs +++ b/crates/squawk_syntax/src/test.rs @@ -271,21 +271,21 @@ fn plpgsql_suite_score() { assert_snapshot!(table, @" file bodies tokens unparsed err - plpgsql.sql 254 9965 9931 9931 - plpgsql_array.sql 26 949 949 949 - plpgsql_cache.sql 2 60 60 60 - plpgsql_call.sql 45 1698 1696 1696 - plpgsql_control.sql 27 1424 1422 1422 - plpgsql_copy.sql 4 28 28 28 - plpgsql_domain.sql 23 307 307 307 - plpgsql_misc.sql 16 261 261 261 - plpgsql_record.sql 65 2008 2000 2000 - plpgsql_simple.sql 9 217 217 217 - plpgsql_transaction.sql 37 1203 1199 1199 - plpgsql_trap.sql 7 354 354 354 - plpgsql_trigger.sql 1 55 55 55 - plpgsql_varprops.sql 33 736 706 706 - total 549 19265 19185 19185 + plpgsql.sql 254 9965 9099 1181 + plpgsql_array.sql 26 949 867 110 + plpgsql_cache.sql 2 60 53 8 + plpgsql_call.sql 45 1698 1557 224 + plpgsql_control.sql 27 1424 1351 167 + plpgsql_copy.sql 4 28 16 4 + plpgsql_domain.sql 23 307 252 39 + plpgsql_misc.sql 16 261 213 29 + plpgsql_record.sql 65 2008 1836 252 + plpgsql_simple.sql 9 217 191 31 + plpgsql_transaction.sql 37 1203 1084 153 + plpgsql_trap.sql 7 354 315 52 + plpgsql_trigger.sql 1 55 53 6 + plpgsql_varprops.sql 33 736 635 92 + total 549 19265 17522 2348 "); } From 7c34c5ff68183da9a30b9a2e11e5089505a8eafc Mon Sep 17 00:00:00 2001 From: Steve Dignam Date: Thu, 17 Sep 2026 00:54:27 -0400 Subject: [PATCH 05/11] label --- crates/squawk_ide/src/goto_definition.rs | 1 + crates/squawk_ide/src/hover.rs | 2 + .../src/generated/syntax_kind.rs | 3 + .../squawk_parser/src/generated/token_sets.rs | 28 ++++ crates/squawk_parser/src/lib.rs | 6 + crates/squawk_parser/src/plpgsql_grammar.rs | 57 ++++++- .../tests/data/plpgsql/ok/label.sql | 17 +++ .../squawk_syntax/src/ast/generated/nodes.rs | 139 ++++++++++++++++++ crates/squawk_syntax/src/postgresql.ungram | 11 ++ ...squawk_syntax__test__plpgsql_label_ok.snap | 65 ++++++++ crates/squawk_syntax/src/test.rs | 8 +- crates/xtask/src/codegen.rs | 18 +++ crates/xtask/src/keywords.rs | 18 +++ 13 files changed, 365 insertions(+), 8 deletions(-) create mode 100644 crates/squawk_parser/tests/data/plpgsql/ok/label.sql create mode 100644 crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_label_ok.snap diff --git a/crates/squawk_ide/src/goto_definition.rs b/crates/squawk_ide/src/goto_definition.rs index 4ecb596e..df990634 100644 --- a/crates/squawk_ide/src/goto_definition.rs +++ b/crates/squawk_ide/src/goto_definition.rs @@ -232,6 +232,7 @@ pub fn goto_definition(db: &dyn Db, position: InFile) -> SmallVec<[Loc resolve::resolve_name_ref(db, InFile::new(definition_file, &name_ref)) }) } + ast::AnyNameRef::PlpgsqlLabelNameRef(_) => None, ast::AnyNameRef::PolicyRef(name_ref) => resolve_in_files(db, file, |definition_file| { resolve::resolve_policy_ref(db, InFile::new(definition_file, &name_ref)) }), diff --git a/crates/squawk_ide/src/hover.rs b/crates/squawk_ide/src/hover.rs index a3583ac2..aad4430f 100644 --- a/crates/squawk_ide/src/hover.rs +++ b/crates/squawk_ide/src/hover.rs @@ -227,6 +227,8 @@ pub fn hover(db: &dyn Db, position: InFile) -> Option { | ast::AnyName::OptionItemValueName(_) | ast::AnyName::ParamNameRef(_) | ast::AnyName::PathSegmentRef(_) + | ast::AnyName::PlpgsqlLabelName(_) + | ast::AnyName::PlpgsqlLabelNameRef(_) | ast::AnyName::PolicyRef(_) | ast::AnyName::PreparedStatementRef(_) | ast::AnyName::PropertyName(_) diff --git a/crates/squawk_parser/src/generated/syntax_kind.rs b/crates/squawk_parser/src/generated/syntax_kind.rs index 9386d2d6..92f57d94 100644 --- a/crates/squawk_parser/src/generated/syntax_kind.rs +++ b/crates/squawk_parser/src/generated/syntax_kind.rs @@ -1507,6 +1507,9 @@ pub enum SyntaxKind { PLPGSQL_BLOCK, PLPGSQL_BODY, PLPGSQL_DECLARE_SECTION, + PLPGSQL_LABEL, + PLPGSQL_LABEL_NAME, + PLPGSQL_LABEL_NAME_REF, PLPGSQL_NULL_STMT, POLICY, POLICY_COMMAND, diff --git a/crates/squawk_parser/src/generated/token_sets.rs b/crates/squawk_parser/src/generated/token_sets.rs index 5033790d..b83c3601 100644 --- a/crates/squawk_parser/src/generated/token_sets.rs +++ b/crates/squawk_parser/src/generated/token_sets.rs @@ -2362,3 +2362,31 @@ pub(crate) const RESERVED_KEYWORDS: TokenSet = TokenSet::new(&[ SyntaxKind::WINDOW_KW, SyntaxKind::WITH_KW, ]); + +pub(crate) const PLPGSQL_RESERVED_KEYWORDS: TokenSet = TokenSet::new(&[ + SyntaxKind::ALL_KW, + SyntaxKind::BEGIN_KW, + SyntaxKind::BY_KW, + SyntaxKind::CASE_KW, + SyntaxKind::DECLARE_KW, + SyntaxKind::ELSE_KW, + SyntaxKind::END_KW, + SyntaxKind::FOR_KW, + SyntaxKind::FROM_KW, + SyntaxKind::IF_KW, + SyntaxKind::IN_KW, + SyntaxKind::INTO_KW, + SyntaxKind::NOT_KW, + SyntaxKind::NULL_KW, + SyntaxKind::OR_KW, + SyntaxKind::THEN_KW, + SyntaxKind::TO_KW, + SyntaxKind::USING_KW, + SyntaxKind::WHEN_KW, +]); + +pub(crate) const PLPGSQL_RESERVED_CONTEXTUAL_KEYWORDS: TokenSet = TokenSet::new(&[ + SyntaxKind::FOREACH_KW, + SyntaxKind::LOOP_KW, + SyntaxKind::WHILE_KW, +]); diff --git a/crates/squawk_parser/src/lib.rs b/crates/squawk_parser/src/lib.rs index dfbf4b0a..fe75083e 100644 --- a/crates/squawk_parser/src/lib.rs +++ b/crates/squawk_parser/src/lib.rs @@ -670,6 +670,12 @@ impl<'t> Parser<'t> { kinds.contains(self.nth(n)) } + /// Checks if the nth token is a contextual keyword in `kinds`. + #[must_use] + pub(crate) fn nth_at_contextual_ts(&self, n: usize, kinds: TokenSet) -> bool { + kinds.contains(self.inp.contextual_kind(self.pos + n)) + } + #[must_use] pub(crate) fn nth_at(&self, n: usize, kind: SyntaxKind) -> bool { match kind { diff --git a/crates/squawk_parser/src/plpgsql_grammar.rs b/crates/squawk_parser/src/plpgsql_grammar.rs index ad7c2bf7..1c029b1c 100644 --- a/crates/squawk_parser/src/plpgsql_grammar.rs +++ b/crates/squawk_parser/src/plpgsql_grammar.rs @@ -1,9 +1,16 @@ -use crate::{Parser, syntax_kind::SyntaxKind::*, token_set::TokenSet}; +use crate::{ + Parser, SyntaxKind, + generated::token_sets::{ + ALL_KEYWORDS, PLPGSQL_RESERVED_CONTEXTUAL_KEYWORDS, PLPGSQL_RESERVED_KEYWORDS, + }, + syntax_kind::SyntaxKind::*, + token_set::TokenSet, +}; pub(crate) fn plpgsql_entry_point(p: &mut Parser) { let m = p.start(); while !p.at(EOF) { - if p.at_ts(BLOCK_FIRST) { + if at_block_start(p) { block(p); } else { temp_unknown(p, "expected a block"); @@ -15,17 +22,44 @@ pub(crate) fn plpgsql_entry_point(p: &mut Parser) { const BLOCK_FIRST: TokenSet = TokenSet::new(&[BEGIN_KW, DECLARE_KW]); fn block(p: &mut Parser) { - assert!(p.at_ts(BLOCK_FIRST)); + assert!(at_block_start(p)); let m = p.start(); + opt_block_label(p); opt_declare_section(p); p.expect(BEGIN_KW); body(p); p.expect(END_KW); + opt_label_name_ref(p); // TODO: add validation, sometimes this is required p.eat(SEMICOLON); m.complete(p, PLPGSQL_BLOCK); } +// <> +fn opt_block_label(p: &mut Parser) { + if !at_block_label(p) { + return; + } + let m = p.start(); + p.bump(LESS_LESS); + name(p, PLPGSQL_LABEL_NAME); + p.bump(GREATER_GREATER); + m.complete(p, PLPGSQL_LABEL); +} + +fn opt_label_name_ref(p: &mut Parser) { + if !at_name(p, 0) { + return; + } + name(p, PLPGSQL_LABEL_NAME_REF); +} + +fn name(p: &mut Parser, kind: SyntaxKind) { + let m = p.start(); + p.bump_any(); + m.complete(p, kind); +} + fn opt_declare_section(p: &mut Parser) { if !p.at(DECLARE_KW) { return; @@ -47,7 +81,7 @@ fn body(p: &mut Parser) { } fn stmt(p: &mut Parser) { - if p.at_ts(BLOCK_FIRST) { + if at_block_start(p) { block(p); } else if p.at(NULL_KW) && p.nth_at(1, SEMICOLON) { let m = p.start(); @@ -81,6 +115,21 @@ fn temp_unknown(p: &mut Parser, message: &str) { m.complete(p, ERROR); } +fn at_block_start(p: &Parser) -> bool { + p.at_ts(BLOCK_FIRST) || at_block_label(p) +} + +fn at_block_label(p: &Parser) -> bool { + p.at(LESS_LESS) && at_name(p, 2) && p.nth_at(3, GREATER_GREATER) && p.nth_at_ts(5, BLOCK_FIRST) +} + +fn at_name(p: &Parser, n: usize) -> bool { + if p.nth_at(n, IDENT) { + return !p.nth_at_contextual_ts(n, PLPGSQL_RESERVED_CONTEXTUAL_KEYWORDS); + } + p.nth_at_ts(n, ALL_KEYWORDS) && !p.nth_at_ts(n, PLPGSQL_RESERVED_KEYWORDS) +} + fn at_block_end(p: &Parser) -> bool { if !p.at(END_KW) { return false; diff --git a/crates/squawk_parser/tests/data/plpgsql/ok/label.sql b/crates/squawk_parser/tests/data/plpgsql/ok/label.sql new file mode 100644 index 00000000..013a04c4 --- /dev/null +++ b/crates/squawk_parser/tests/data/plpgsql/ok/label.sql @@ -0,0 +1,17 @@ +do $$ +<> +begin + <> + declare + begin + null; + end inner; +end outer; +$$; + +do $$ +<> +begin + null; +end; +$$; diff --git a/crates/squawk_syntax/src/ast/generated/nodes.rs b/crates/squawk_syntax/src/ast/generated/nodes.rs index 29148d73..0357499f 100644 --- a/crates/squawk_syntax/src/ast/generated/nodes.rs +++ b/crates/squawk_syntax/src/ast/generated/nodes.rs @@ -19418,6 +19418,14 @@ impl PlpgsqlBlock { support::child(&self.syntax) } #[inline] + pub fn plpgsql_label(&self) -> Option { + support::child(&self.syntax) + } + #[inline] + pub fn plpgsql_label_name_ref(&self) -> Option { + support::child(&self.syntax) + } + #[inline] pub fn semicolon_token(&self) -> Option { support::token(&self.syntax, SyntaxKind::SEMICOLON) } @@ -19453,6 +19461,47 @@ impl PlpgsqlDeclareSection { } } +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct PlpgsqlLabel { + pub(crate) syntax: SyntaxNode, +} +impl PlpgsqlLabel { + #[inline] + pub fn plpgsql_label_name(&self) -> Option { + support::child(&self.syntax) + } + #[inline] + pub fn less_less_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::LESS_LESS) + } + #[inline] + pub fn greater_greater_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::GREATER_GREATER) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct PlpgsqlLabelName { + pub(crate) syntax: SyntaxNode, +} +impl PlpgsqlLabelName { + #[inline] + pub fn ident_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::IDENT) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct PlpgsqlLabelNameRef { + pub(crate) syntax: SyntaxNode, +} +impl PlpgsqlLabelNameRef { + #[inline] + pub fn ident_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::IDENT) + } +} + #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct PlpgsqlNullStmt { pub(crate) syntax: SyntaxNode, @@ -28576,6 +28625,8 @@ pub enum AnyName { ParamNameRef(ParamNameRef), PathSegment(PathSegment), PathSegmentRef(PathSegmentRef), + PlpgsqlLabelName(PlpgsqlLabelName), + PlpgsqlLabelNameRef(PlpgsqlLabelNameRef), Policy(Policy), PolicyRef(PolicyRef), PreparedStatement(PreparedStatement), @@ -28632,6 +28683,7 @@ pub enum AnyNameRef { NameRef(NameRef), ParamNameRef(ParamNameRef), PathSegmentRef(PathSegmentRef), + PlpgsqlLabelNameRef(PlpgsqlLabelNameRef), PolicyRef(PolicyRef), PreparedStatementRef(PreparedStatementRef), PropertyNameRef(PropertyNameRef), @@ -46163,6 +46215,60 @@ impl AstNode for PlpgsqlDeclareSection { &self.syntax } } +impl AstNode for PlpgsqlLabel { + #[inline] + fn can_cast(kind: SyntaxKind) -> bool { + kind == SyntaxKind::PLPGSQL_LABEL + } + #[inline] + fn cast(syntax: SyntaxNode) -> Option { + if Self::can_cast(syntax.kind()) { + Some(Self { syntax }) + } else { + None + } + } + #[inline] + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +impl AstNode for PlpgsqlLabelName { + #[inline] + fn can_cast(kind: SyntaxKind) -> bool { + kind == SyntaxKind::PLPGSQL_LABEL_NAME + } + #[inline] + fn cast(syntax: SyntaxNode) -> Option { + if Self::can_cast(syntax.kind()) { + Some(Self { syntax }) + } else { + None + } + } + #[inline] + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +impl AstNode for PlpgsqlLabelNameRef { + #[inline] + fn can_cast(kind: SyntaxKind) -> bool { + kind == SyntaxKind::PLPGSQL_LABEL_NAME_REF + } + #[inline] + fn cast(syntax: SyntaxNode) -> Option { + if Self::can_cast(syntax.kind()) { + Some(Self { syntax }) + } else { + None + } + } + #[inline] + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} impl AstNode for PlpgsqlNullStmt { #[inline] fn can_cast(kind: SyntaxKind) -> bool { @@ -58556,6 +58662,8 @@ impl AstNode for AnyName { | SyntaxKind::PARAM_NAME_REF | SyntaxKind::PATH_SEGMENT | SyntaxKind::PATH_SEGMENT_REF + | SyntaxKind::PLPGSQL_LABEL_NAME + | SyntaxKind::PLPGSQL_LABEL_NAME_REF | SyntaxKind::POLICY | SyntaxKind::POLICY_REF | SyntaxKind::PREPARED_STATEMENT @@ -58667,6 +58775,12 @@ impl AstNode for AnyName { SyntaxKind::PARAM_NAME_REF => AnyName::ParamNameRef(ParamNameRef { syntax }), SyntaxKind::PATH_SEGMENT => AnyName::PathSegment(PathSegment { syntax }), SyntaxKind::PATH_SEGMENT_REF => AnyName::PathSegmentRef(PathSegmentRef { syntax }), + SyntaxKind::PLPGSQL_LABEL_NAME => { + AnyName::PlpgsqlLabelName(PlpgsqlLabelName { syntax }) + } + SyntaxKind::PLPGSQL_LABEL_NAME_REF => { + AnyName::PlpgsqlLabelNameRef(PlpgsqlLabelNameRef { syntax }) + } SyntaxKind::POLICY => AnyName::Policy(Policy { syntax }), SyntaxKind::POLICY_REF => AnyName::PolicyRef(PolicyRef { syntax }), SyntaxKind::PREPARED_STATEMENT => { @@ -58772,6 +58886,8 @@ impl AstNode for AnyName { AnyName::ParamNameRef(it) => &it.syntax, AnyName::PathSegment(it) => &it.syntax, AnyName::PathSegmentRef(it) => &it.syntax, + AnyName::PlpgsqlLabelName(it) => &it.syntax, + AnyName::PlpgsqlLabelNameRef(it) => &it.syntax, AnyName::Policy(it) => &it.syntax, AnyName::PolicyRef(it) => &it.syntax, AnyName::PreparedStatement(it) => &it.syntax, @@ -59092,6 +59208,18 @@ impl From for AnyName { AnyName::PathSegmentRef(node) } } +impl From for AnyName { + #[inline] + fn from(node: PlpgsqlLabelName) -> AnyName { + AnyName::PlpgsqlLabelName(node) + } +} +impl From for AnyName { + #[inline] + fn from(node: PlpgsqlLabelNameRef) -> AnyName { + AnyName::PlpgsqlLabelNameRef(node) + } +} impl From for AnyName { #[inline] fn from(node: Policy) -> AnyName { @@ -59324,6 +59452,7 @@ impl AstNode for AnyNameRef { | SyntaxKind::NAME_REF | SyntaxKind::PARAM_NAME_REF | SyntaxKind::PATH_SEGMENT_REF + | SyntaxKind::PLPGSQL_LABEL_NAME_REF | SyntaxKind::POLICY_REF | SyntaxKind::PREPARED_STATEMENT_REF | SyntaxKind::PROPERTY_NAME_REF @@ -59375,6 +59504,9 @@ impl AstNode for AnyNameRef { SyntaxKind::NAME_REF => AnyNameRef::NameRef(NameRef { syntax }), SyntaxKind::PARAM_NAME_REF => AnyNameRef::ParamNameRef(ParamNameRef { syntax }), SyntaxKind::PATH_SEGMENT_REF => AnyNameRef::PathSegmentRef(PathSegmentRef { syntax }), + SyntaxKind::PLPGSQL_LABEL_NAME_REF => { + AnyNameRef::PlpgsqlLabelNameRef(PlpgsqlLabelNameRef { syntax }) + } SyntaxKind::POLICY_REF => AnyNameRef::PolicyRef(PolicyRef { syntax }), SyntaxKind::PREPARED_STATEMENT_REF => { AnyNameRef::PreparedStatementRef(PreparedStatementRef { syntax }) @@ -59422,6 +59554,7 @@ impl AstNode for AnyNameRef { AnyNameRef::NameRef(it) => &it.syntax, AnyNameRef::ParamNameRef(it) => &it.syntax, AnyNameRef::PathSegmentRef(it) => &it.syntax, + AnyNameRef::PlpgsqlLabelNameRef(it) => &it.syntax, AnyNameRef::PolicyRef(it) => &it.syntax, AnyNameRef::PreparedStatementRef(it) => &it.syntax, AnyNameRef::PropertyNameRef(it) => &it.syntax, @@ -59542,6 +59675,12 @@ impl From for AnyNameRef { AnyNameRef::PathSegmentRef(node) } } +impl From for AnyNameRef { + #[inline] + fn from(node: PlpgsqlLabelNameRef) -> AnyNameRef { + AnyNameRef::PlpgsqlLabelNameRef(node) + } +} impl From for AnyNameRef { #[inline] fn from(node: PolicyRef) -> AnyNameRef { diff --git a/crates/squawk_syntax/src/postgresql.ungram b/crates/squawk_syntax/src/postgresql.ungram index 715cdf3f..fd905e5f 100644 --- a/crates/squawk_syntax/src/postgresql.ungram +++ b/crates/squawk_syntax/src/postgresql.ungram @@ -2040,12 +2040,23 @@ Plpgsql = PlpgsqlBlock? PlpgsqlBlock = + PlpgsqlLabel? PlpgsqlDeclareSection? 'begin' PlpgsqlBody 'end' + PlpgsqlLabelNameRef? ';'? +PlpgsqlLabel = + '<<' PlpgsqlLabelName '>>' + +PlpgsqlLabelName = + '#name'? + +PlpgsqlLabelNameRef = + '#name'? + PlpgsqlDeclareSection = 'declare' diff --git a/crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_label_ok.snap b/crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_label_ok.snap new file mode 100644 index 00000000..5f47d664 --- /dev/null +++ b/crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_label_ok.snap @@ -0,0 +1,65 @@ +--- +source: crates/squawk_syntax/src/test.rs +input_file: crates/squawk_parser/tests/data/plpgsql/ok/label.sql +--- +PLPGSQL@0..81 + WHITESPACE@0..1 "\n" + PLPGSQL_BLOCK@1..80 + PLPGSQL_LABEL@1..10 + LESS_LESS@1..3 "<<" + PLPGSQL_LABEL_NAME@3..8 + OUTER_KW@3..8 "outer" + GREATER_GREATER@8..10 ">>" + WHITESPACE@10..11 "\n" + BEGIN_KW@11..16 "begin" + WHITESPACE@16..19 "\n " + PLPGSQL_BODY@19..69 + PLPGSQL_BLOCK@19..69 + PLPGSQL_LABEL@19..28 + LESS_LESS@19..21 "<<" + PLPGSQL_LABEL_NAME@21..26 + INNER_KW@21..26 "inner" + GREATER_GREATER@26..28 ">>" + WHITESPACE@28..31 "\n " + PLPGSQL_DECLARE_SECTION@31..38 + DECLARE_KW@31..38 "declare" + WHITESPACE@38..41 "\n " + BEGIN_KW@41..46 "begin" + WHITESPACE@46..51 "\n " + PLPGSQL_BODY@51..56 + PLPGSQL_NULL_STMT@51..56 + NULL_KW@51..55 "null" + SEMICOLON@55..56 ";" + WHITESPACE@56..59 "\n " + END_KW@59..62 "end" + WHITESPACE@62..63 " " + PLPGSQL_LABEL_NAME_REF@63..68 + INNER_KW@63..68 "inner" + SEMICOLON@68..69 ";" + WHITESPACE@69..70 "\n" + END_KW@70..73 "end" + WHITESPACE@73..74 " " + PLPGSQL_LABEL_NAME_REF@74..79 + OUTER_KW@74..79 "outer" + SEMICOLON@79..80 ";" + WHITESPACE@80..81 "\n" +--- +PLPGSQL@0..31 + WHITESPACE@0..1 "\n" + PLPGSQL_BLOCK@1..30 + PLPGSQL_LABEL@1..11 + LESS_LESS@1..3 "<<" + PLPGSQL_LABEL_NAME@3..9 + UPDATE_KW@3..9 "update" + GREATER_GREATER@9..11 ">>" + WHITESPACE@11..12 "\n" + BEGIN_KW@12..17 "begin" + WHITESPACE@17..20 "\n " + PLPGSQL_BODY@20..25 + PLPGSQL_NULL_STMT@20..25 + NULL_KW@20..24 "null" + SEMICOLON@24..25 ";" + WHITESPACE@25..26 "\n" + END_KW@26..29 "end" + SEMICOLON@29..30 ";" + WHITESPACE@30..31 "\n" diff --git a/crates/squawk_syntax/src/test.rs b/crates/squawk_syntax/src/test.rs index 59c9b825..36940fcf 100644 --- a/crates/squawk_syntax/src/test.rs +++ b/crates/squawk_syntax/src/test.rs @@ -271,21 +271,21 @@ fn plpgsql_suite_score() { assert_snapshot!(table, @" file bodies tokens unparsed err - plpgsql.sql 254 9965 9099 1181 + plpgsql.sql 254 9959 9081 1181 plpgsql_array.sql 26 949 867 110 plpgsql_cache.sql 2 60 53 8 plpgsql_call.sql 45 1698 1557 224 - plpgsql_control.sql 27 1424 1351 167 + plpgsql_control.sql 27 1402 1263 175 plpgsql_copy.sql 4 28 16 4 plpgsql_domain.sql 23 307 252 39 plpgsql_misc.sql 16 261 213 29 - plpgsql_record.sql 65 2008 1836 252 + plpgsql_record.sql 65 1994 1794 252 plpgsql_simple.sql 9 217 191 31 plpgsql_transaction.sql 37 1203 1084 153 plpgsql_trap.sql 7 354 315 52 plpgsql_trigger.sql 1 55 53 6 plpgsql_varprops.sql 33 736 635 92 - total 549 19265 17522 2348 + total 549 19223 17374 2356 "); } diff --git a/crates/xtask/src/codegen.rs b/crates/xtask/src/codegen.rs index aeb7f655..e4e96a96 100644 --- a/crates/xtask/src/codegen.rs +++ b/crates/xtask/src/codegen.rs @@ -460,6 +460,16 @@ fn generate_token_sets(keyword_kinds: &KeywordKinds) -> Result { .iter() .map(|key| format_ident!("{}_KW", key.to_case(Case::UpperSnake))) .collect::>(); + let plpgsql_reserved_keywords = &keyword_kinds + .plpgsql_reserved_keywords + .iter() + .map(|key| format_ident!("{}_KW", key.to_case(Case::UpperSnake))) + .collect::>(); + let plpgsql_reserved_contextual_keywords = &keyword_kinds + .plpgsql_reserved_contextual_keywords + .iter() + .map(|key| format_ident!("{}_KW", key.to_case(Case::UpperSnake))) + .collect::>(); let output = reformat( quote! { @@ -497,6 +507,14 @@ fn generate_token_sets(keyword_kinds: &KeywordKinds) -> Result { pub(crate) const RESERVED_KEYWORDS: TokenSet = TokenSet::new(&[ #(SyntaxKind::#reserved_keywords),* ]); + + pub(crate) const PLPGSQL_RESERVED_KEYWORDS: TokenSet = TokenSet::new(&[ + #(SyntaxKind::#plpgsql_reserved_keywords),* + ]); + + pub(crate) const PLPGSQL_RESERVED_CONTEXTUAL_KEYWORDS: TokenSet = TokenSet::new(&[ + #(SyntaxKind::#plpgsql_reserved_contextual_keywords),* + ]); } .to_string(), ) diff --git a/crates/xtask/src/keywords.rs b/crates/xtask/src/keywords.rs index 2c22caf6..3163108f 100644 --- a/crates/xtask/src/keywords.rs +++ b/crates/xtask/src/keywords.rs @@ -132,6 +132,8 @@ pub(crate) struct KeywordKinds { pub(crate) type_func_name_keywords: Vec, pub(crate) col_table_keywords: Vec, pub(crate) type_keywords: Vec, + pub(crate) plpgsql_reserved_keywords: Vec, + pub(crate) plpgsql_reserved_contextual_keywords: Vec, } pub(crate) fn keyword_kinds() -> Result { @@ -220,6 +222,20 @@ pub(crate) fn keyword_kinds() -> Result { .collect::>(); type_keywords.sort(); + let pl_reserved = parse_pl_header(PL_RESERVED_KWLIST)?; + let mut plpgsql_reserved_keywords = pl_reserved + .iter() + .filter(|keyword| keywords.contains_key(*keyword)) + .map(|keyword| keyword.to_owned()) + .collect::>(); + plpgsql_reserved_keywords.sort(); + let mut plpgsql_reserved_contextual_keywords = pl_reserved + .iter() + .filter(|keyword| !keywords.contains_key(*keyword)) + .map(|keyword| keyword.to_owned()) + .collect::>(); + plpgsql_reserved_contextual_keywords.sort(); + Ok(KeywordKinds { all_keywords, bare_label_keywords, @@ -230,6 +246,8 @@ pub(crate) fn keyword_kinds() -> Result { type_func_name_keywords, col_table_keywords, type_keywords, + plpgsql_reserved_keywords, + plpgsql_reserved_contextual_keywords, }) } From 44e6604e62f8311aa2af3a196f32339c98d0392f Mon Sep 17 00:00:00 2001 From: Steve Dignam Date: Thu, 17 Sep 2026 01:06:16 -0400 Subject: [PATCH 06/11] exception section --- crates/squawk_ide/src/hover.rs | 1 + .../src/generated/syntax_kind.rs | 3 + crates/squawk_parser/src/lib.rs | 1 - crates/squawk_parser/src/plpgsql_grammar.rs | 61 ++++++++- .../tests/data/plpgsql/ok/exception.sql | 22 ++++ .../squawk_syntax/src/ast/generated/nodes.rs | 121 ++++++++++++++++++ crates/squawk_syntax/src/postgresql.ungram | 15 +++ ...wk_syntax__test__plpgsql_exception_ok.snap | 98 ++++++++++++++ crates/squawk_syntax/src/test.rs | 12 +- 9 files changed, 324 insertions(+), 10 deletions(-) create mode 100644 crates/squawk_parser/tests/data/plpgsql/ok/exception.sql create mode 100644 crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_exception_ok.snap diff --git a/crates/squawk_ide/src/hover.rs b/crates/squawk_ide/src/hover.rs index aad4430f..026694b1 100644 --- a/crates/squawk_ide/src/hover.rs +++ b/crates/squawk_ide/src/hover.rs @@ -227,6 +227,7 @@ pub fn hover(db: &dyn Db, position: InFile) -> Option { | ast::AnyName::OptionItemValueName(_) | ast::AnyName::ParamNameRef(_) | ast::AnyName::PathSegmentRef(_) + | ast::AnyName::PlpgsqlCondition(_) | ast::AnyName::PlpgsqlLabelName(_) | ast::AnyName::PlpgsqlLabelNameRef(_) | ast::AnyName::PolicyRef(_) diff --git a/crates/squawk_parser/src/generated/syntax_kind.rs b/crates/squawk_parser/src/generated/syntax_kind.rs index 92f57d94..37b077ed 100644 --- a/crates/squawk_parser/src/generated/syntax_kind.rs +++ b/crates/squawk_parser/src/generated/syntax_kind.rs @@ -1506,7 +1506,10 @@ pub enum SyntaxKind { PLPGSQL, PLPGSQL_BLOCK, PLPGSQL_BODY, + PLPGSQL_CONDITION, PLPGSQL_DECLARE_SECTION, + PLPGSQL_EXCEPTION_HANDLER, + PLPGSQL_EXCEPTION_SECTION, PLPGSQL_LABEL, PLPGSQL_LABEL_NAME, PLPGSQL_LABEL_NAME_REF, diff --git a/crates/squawk_parser/src/lib.rs b/crates/squawk_parser/src/lib.rs index fe75083e..5ffc613d 100644 --- a/crates/squawk_parser/src/lib.rs +++ b/crates/squawk_parser/src/lib.rs @@ -587,7 +587,6 @@ impl<'t> Parser<'t> { /// Advances the parser by one token, remapping its kind. /// This is useful to create contextual keywords from /// identifiers. - #[expect(dead_code, reason = "used by the PL/pgSQL grammar")] pub(crate) fn bump_remap(&mut self, kind: SyntaxKind) { if self.nth(0) == SyntaxKind::EOF { // FIXME: panic!? diff --git a/crates/squawk_parser/src/plpgsql_grammar.rs b/crates/squawk_parser/src/plpgsql_grammar.rs index 1c029b1c..13e33980 100644 --- a/crates/squawk_parser/src/plpgsql_grammar.rs +++ b/crates/squawk_parser/src/plpgsql_grammar.rs @@ -27,7 +27,8 @@ fn block(p: &mut Parser) { opt_block_label(p); opt_declare_section(p); p.expect(BEGIN_KW); - body(p); + body(p, BodyKind::Block); + opt_exception_section(p); p.expect(END_KW); opt_label_name_ref(p); // TODO: add validation, sometimes this is required @@ -72,9 +73,15 @@ fn opt_declare_section(p: &mut Parser) { m.complete(p, PLPGSQL_DECLARE_SECTION); } -fn body(p: &mut Parser) { +#[derive(Clone, Copy, PartialEq)] +enum BodyKind { + Block, + ExceptionHandler, +} + +fn body(p: &mut Parser, kind: BodyKind) { let m = p.start(); - while !p.at(EOF) && !at_block_end(p) { + while !p.at(EOF) && !at_body_end(p, kind) { stmt(p); } m.complete(p, PLPGSQL_BODY); @@ -115,6 +122,47 @@ fn temp_unknown(p: &mut Parser, message: &str) { m.complete(p, ERROR); } +fn opt_exception_section(p: &mut Parser) { + if !p.nth_at_contextual_kw(0, EXCEPTION_KW) { + return; + } + let m = p.start(); + p.bump_remap(EXCEPTION_KW); + while p.at(WHEN_KW) { + exception_handler(p); + } + m.complete(p, PLPGSQL_EXCEPTION_SECTION); +} + +fn exception_handler(p: &mut Parser) { + assert!(p.at(WHEN_KW)); + let m = p.start(); + // TODO: use delimited + p.bump(WHEN_KW); + condition(p); + while p.eat(OR_KW) { + condition(p); + } + p.expect(THEN_KW); + body(p, BodyKind::ExceptionHandler); + m.complete(p, PLPGSQL_EXCEPTION_HANDLER); +} + +fn condition(p: &mut Parser) { + let m = p.start(); + if at_name(p, 0) { + let sqlstate = p.nth_at_contextual_kw(0, SQLSTATE_KW); + p.bump_any(); + if sqlstate { + p.expect(STRING); + } + } else { + let kind = p.current(); + p.error(format!("expected a condition name, found {kind:?}")); + } + m.complete(p, PLPGSQL_CONDITION); +} + fn at_block_start(p: &Parser) -> bool { p.at_ts(BLOCK_FIRST) || at_block_label(p) } @@ -130,6 +178,13 @@ fn at_name(p: &Parser, n: usize) -> bool { p.nth_at_ts(n, ALL_KEYWORDS) && !p.nth_at_ts(n, PLPGSQL_RESERVED_KEYWORDS) } +fn at_body_end(p: &Parser, kind: BodyKind) -> bool { + at_block_end(p) + || p.nth_at_contextual_kw(0, EXCEPTION_KW) + // TODO: do we need this kind param? + || (kind == BodyKind::ExceptionHandler && p.at(WHEN_KW)) +} + fn at_block_end(p: &Parser) -> bool { if !p.at(END_KW) { return false; diff --git a/crates/squawk_parser/tests/data/plpgsql/ok/exception.sql b/crates/squawk_parser/tests/data/plpgsql/ok/exception.sql new file mode 100644 index 00000000..e323ec06 --- /dev/null +++ b/crates/squawk_parser/tests/data/plpgsql/ok/exception.sql @@ -0,0 +1,22 @@ +do $$ +begin + null; +exception + when others then + null; + when division_by_zero or sqlstate '22012' then + null; +end +$$; + +do $$ +<> +begin + begin + null; + exception + when no_data_found then + null; + end; +end lbl; +$$; diff --git a/crates/squawk_syntax/src/ast/generated/nodes.rs b/crates/squawk_syntax/src/ast/generated/nodes.rs index 0357499f..01a2b2c1 100644 --- a/crates/squawk_syntax/src/ast/generated/nodes.rs +++ b/crates/squawk_syntax/src/ast/generated/nodes.rs @@ -19418,6 +19418,10 @@ impl PlpgsqlBlock { support::child(&self.syntax) } #[inline] + pub fn plpgsql_exception_section(&self) -> Option { + support::child(&self.syntax) + } + #[inline] pub fn plpgsql_label(&self) -> Option { support::child(&self.syntax) } @@ -19450,6 +19454,21 @@ impl PlpgsqlBody { } } +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct PlpgsqlCondition { + pub(crate) syntax: SyntaxNode, +} +impl PlpgsqlCondition { + #[inline] + pub fn string_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::STRING) + } + #[inline] + pub fn ident_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::IDENT) + } +} + #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct PlpgsqlDeclareSection { pub(crate) syntax: SyntaxNode, @@ -19461,6 +19480,44 @@ impl PlpgsqlDeclareSection { } } +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct PlpgsqlExceptionHandler { + pub(crate) syntax: SyntaxNode, +} +impl PlpgsqlExceptionHandler { + #[inline] + pub fn conditions(&self) -> AstChildren { + support::children(&self.syntax) + } + #[inline] + pub fn plpgsql_body(&self) -> Option { + support::child(&self.syntax) + } + #[inline] + pub fn then_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::THEN_KW) + } + #[inline] + pub fn when_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::WHEN_KW) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct PlpgsqlExceptionSection { + pub(crate) syntax: SyntaxNode, +} +impl PlpgsqlExceptionSection { + #[inline] + pub fn handlers(&self) -> AstChildren { + support::children(&self.syntax) + } + #[inline] + pub fn exception_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::EXCEPTION_KW) + } +} + #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct PlpgsqlLabel { pub(crate) syntax: SyntaxNode, @@ -28625,6 +28682,7 @@ pub enum AnyName { ParamNameRef(ParamNameRef), PathSegment(PathSegment), PathSegmentRef(PathSegmentRef), + PlpgsqlCondition(PlpgsqlCondition), PlpgsqlLabelName(PlpgsqlLabelName), PlpgsqlLabelNameRef(PlpgsqlLabelNameRef), Policy(Policy), @@ -46197,6 +46255,24 @@ impl AstNode for PlpgsqlBody { &self.syntax } } +impl AstNode for PlpgsqlCondition { + #[inline] + fn can_cast(kind: SyntaxKind) -> bool { + kind == SyntaxKind::PLPGSQL_CONDITION + } + #[inline] + fn cast(syntax: SyntaxNode) -> Option { + if Self::can_cast(syntax.kind()) { + Some(Self { syntax }) + } else { + None + } + } + #[inline] + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} impl AstNode for PlpgsqlDeclareSection { #[inline] fn can_cast(kind: SyntaxKind) -> bool { @@ -46215,6 +46291,42 @@ impl AstNode for PlpgsqlDeclareSection { &self.syntax } } +impl AstNode for PlpgsqlExceptionHandler { + #[inline] + fn can_cast(kind: SyntaxKind) -> bool { + kind == SyntaxKind::PLPGSQL_EXCEPTION_HANDLER + } + #[inline] + fn cast(syntax: SyntaxNode) -> Option { + if Self::can_cast(syntax.kind()) { + Some(Self { syntax }) + } else { + None + } + } + #[inline] + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +impl AstNode for PlpgsqlExceptionSection { + #[inline] + fn can_cast(kind: SyntaxKind) -> bool { + kind == SyntaxKind::PLPGSQL_EXCEPTION_SECTION + } + #[inline] + fn cast(syntax: SyntaxNode) -> Option { + if Self::can_cast(syntax.kind()) { + Some(Self { syntax }) + } else { + None + } + } + #[inline] + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} impl AstNode for PlpgsqlLabel { #[inline] fn can_cast(kind: SyntaxKind) -> bool { @@ -58662,6 +58774,7 @@ impl AstNode for AnyName { | SyntaxKind::PARAM_NAME_REF | SyntaxKind::PATH_SEGMENT | SyntaxKind::PATH_SEGMENT_REF + | SyntaxKind::PLPGSQL_CONDITION | SyntaxKind::PLPGSQL_LABEL_NAME | SyntaxKind::PLPGSQL_LABEL_NAME_REF | SyntaxKind::POLICY @@ -58775,6 +58888,7 @@ impl AstNode for AnyName { SyntaxKind::PARAM_NAME_REF => AnyName::ParamNameRef(ParamNameRef { syntax }), SyntaxKind::PATH_SEGMENT => AnyName::PathSegment(PathSegment { syntax }), SyntaxKind::PATH_SEGMENT_REF => AnyName::PathSegmentRef(PathSegmentRef { syntax }), + SyntaxKind::PLPGSQL_CONDITION => AnyName::PlpgsqlCondition(PlpgsqlCondition { syntax }), SyntaxKind::PLPGSQL_LABEL_NAME => { AnyName::PlpgsqlLabelName(PlpgsqlLabelName { syntax }) } @@ -58886,6 +59000,7 @@ impl AstNode for AnyName { AnyName::ParamNameRef(it) => &it.syntax, AnyName::PathSegment(it) => &it.syntax, AnyName::PathSegmentRef(it) => &it.syntax, + AnyName::PlpgsqlCondition(it) => &it.syntax, AnyName::PlpgsqlLabelName(it) => &it.syntax, AnyName::PlpgsqlLabelNameRef(it) => &it.syntax, AnyName::Policy(it) => &it.syntax, @@ -59208,6 +59323,12 @@ impl From for AnyName { AnyName::PathSegmentRef(node) } } +impl From for AnyName { + #[inline] + fn from(node: PlpgsqlCondition) -> AnyName { + AnyName::PlpgsqlCondition(node) + } +} impl From for AnyName { #[inline] fn from(node: PlpgsqlLabelName) -> AnyName { diff --git a/crates/squawk_syntax/src/postgresql.ungram b/crates/squawk_syntax/src/postgresql.ungram index fd905e5f..a99e7971 100644 --- a/crates/squawk_syntax/src/postgresql.ungram +++ b/crates/squawk_syntax/src/postgresql.ungram @@ -2044,10 +2044,25 @@ PlpgsqlBlock = PlpgsqlDeclareSection? 'begin' PlpgsqlBody + PlpgsqlExceptionSection? 'end' PlpgsqlLabelNameRef? ';'? +PlpgsqlExceptionSection = + 'exception' + handlers:(PlpgsqlExceptionHandler*) + +PlpgsqlExceptionHandler = + 'when' + conditions:(PlpgsqlCondition*) + 'then' + PlpgsqlBody + +PlpgsqlCondition = + '#name'? + '@string'? + PlpgsqlLabel = '<<' PlpgsqlLabelName '>>' diff --git a/crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_exception_ok.snap b/crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_exception_ok.snap new file mode 100644 index 00000000..6870b64f --- /dev/null +++ b/crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_exception_ok.snap @@ -0,0 +1,98 @@ +--- +source: crates/squawk_syntax/src/test.rs +input_file: crates/squawk_parser/tests/data/plpgsql/ok/exception.sql +--- +PLPGSQL@0..117 + WHITESPACE@0..1 "\n" + PLPGSQL_BLOCK@1..116 + BEGIN_KW@1..6 "begin" + WHITESPACE@6..9 "\n " + PLPGSQL_BODY@9..14 + PLPGSQL_NULL_STMT@9..14 + NULL_KW@9..13 "null" + SEMICOLON@13..14 ";" + WHITESPACE@14..15 "\n" + PLPGSQL_EXCEPTION_SECTION@15..112 + EXCEPTION_KW@15..24 "exception" + WHITESPACE@24..27 "\n " + PLPGSQL_EXCEPTION_HANDLER@27..53 + WHEN_KW@27..31 "when" + WHITESPACE@31..32 " " + PLPGSQL_CONDITION@32..38 + OTHERS_KW@32..38 "others" + WHITESPACE@38..39 " " + THEN_KW@39..43 "then" + WHITESPACE@43..48 "\n " + PLPGSQL_BODY@48..53 + PLPGSQL_NULL_STMT@48..53 + NULL_KW@48..52 "null" + SEMICOLON@52..53 ";" + WHITESPACE@53..56 "\n " + PLPGSQL_EXCEPTION_HANDLER@56..112 + WHEN_KW@56..60 "when" + WHITESPACE@60..61 " " + PLPGSQL_CONDITION@61..77 + IDENT@61..77 "division_by_zero" + WHITESPACE@77..78 " " + OR_KW@78..80 "or" + WHITESPACE@80..81 " " + PLPGSQL_CONDITION@81..97 + IDENT@81..89 "sqlstate" + WHITESPACE@89..90 " " + STRING@90..97 "'22012'" + WHITESPACE@97..98 " " + THEN_KW@98..102 "then" + WHITESPACE@102..107 "\n " + PLPGSQL_BODY@107..112 + PLPGSQL_NULL_STMT@107..112 + NULL_KW@107..111 "null" + SEMICOLON@111..112 ";" + WHITESPACE@112..113 "\n" + END_KW@113..116 "end" + WHITESPACE@116..117 "\n" +--- +PLPGSQL@0..101 + WHITESPACE@0..1 "\n" + PLPGSQL_BLOCK@1..100 + PLPGSQL_LABEL@1..8 + LESS_LESS@1..3 "<<" + PLPGSQL_LABEL_NAME@3..6 + IDENT@3..6 "lbl" + GREATER_GREATER@6..8 ">>" + WHITESPACE@8..9 "\n" + BEGIN_KW@9..14 "begin" + WHITESPACE@14..17 "\n " + PLPGSQL_BODY@17..91 + PLPGSQL_BLOCK@17..91 + BEGIN_KW@17..22 "begin" + WHITESPACE@22..27 "\n " + PLPGSQL_BODY@27..32 + PLPGSQL_NULL_STMT@27..32 + NULL_KW@27..31 "null" + SEMICOLON@31..32 ";" + WHITESPACE@32..35 "\n " + PLPGSQL_EXCEPTION_SECTION@35..84 + EXCEPTION_KW@35..44 "exception" + WHITESPACE@44..49 "\n " + PLPGSQL_EXCEPTION_HANDLER@49..84 + WHEN_KW@49..53 "when" + WHITESPACE@53..54 " " + PLPGSQL_CONDITION@54..67 + IDENT@54..67 "no_data_found" + WHITESPACE@67..68 " " + THEN_KW@68..72 "then" + WHITESPACE@72..79 "\n " + PLPGSQL_BODY@79..84 + PLPGSQL_NULL_STMT@79..84 + NULL_KW@79..83 "null" + SEMICOLON@83..84 ";" + WHITESPACE@84..87 "\n " + END_KW@87..90 "end" + SEMICOLON@90..91 ";" + WHITESPACE@91..92 "\n" + END_KW@92..95 "end" + WHITESPACE@95..96 " " + PLPGSQL_LABEL_NAME_REF@96..99 + IDENT@96..99 "lbl" + SEMICOLON@99..100 ";" + WHITESPACE@100..101 "\n" diff --git a/crates/squawk_syntax/src/test.rs b/crates/squawk_syntax/src/test.rs index 36940fcf..0d276cde 100644 --- a/crates/squawk_syntax/src/test.rs +++ b/crates/squawk_syntax/src/test.rs @@ -271,21 +271,21 @@ fn plpgsql_suite_score() { assert_snapshot!(table, @" file bodies tokens unparsed err - plpgsql.sql 254 9959 9081 1181 + plpgsql.sql 254 9959 8995 1182 plpgsql_array.sql 26 949 867 110 plpgsql_cache.sql 2 60 53 8 - plpgsql_call.sql 45 1698 1557 224 - plpgsql_control.sql 27 1402 1263 175 + plpgsql_call.sql 45 1698 1553 224 + plpgsql_control.sql 27 1402 1259 175 plpgsql_copy.sql 4 28 16 4 plpgsql_domain.sql 23 307 252 39 plpgsql_misc.sql 16 261 213 29 plpgsql_record.sql 65 1994 1794 252 plpgsql_simple.sql 9 217 191 31 - plpgsql_transaction.sql 37 1203 1084 153 - plpgsql_trap.sql 7 354 315 52 + plpgsql_transaction.sql 37 1203 1076 153 + plpgsql_trap.sql 7 354 276 52 plpgsql_trigger.sql 1 55 53 6 plpgsql_varprops.sql 33 736 635 92 - total 549 19223 17374 2356 + total 549 19223 17233 2357 "); } From ac1a65b1ee56dc4e8c2eea5f73136c89b0679b84 Mon Sep 17 00:00:00 2001 From: Steve Dignam Date: Thu, 17 Sep 2026 09:38:13 -0400 Subject: [PATCH 07/11] var decl --- crates/squawk_ide/src/hover.rs | 1 + .../src/generated/syntax_kind.rs | 4 + crates/squawk_parser/src/grammar.rs | 8 +- crates/squawk_parser/src/plpgsql_grammar.rs | 53 ++++- .../tests/data/plpgsql/ok/var_decl.sql | 11 + .../squawk_syntax/src/ast/generated/nodes.rs | 192 +++++++++++++++++- crates/squawk_syntax/src/postgresql.ungram | 36 +++- ...awk_syntax__test__plpgsql_var_decl_ok.snap | 120 +++++++++++ crates/squawk_syntax/src/test.rs | 26 +-- 9 files changed, 414 insertions(+), 37 deletions(-) create mode 100644 crates/squawk_parser/tests/data/plpgsql/ok/var_decl.sql create mode 100644 crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_var_decl_ok.snap diff --git a/crates/squawk_ide/src/hover.rs b/crates/squawk_ide/src/hover.rs index 026694b1..11029293 100644 --- a/crates/squawk_ide/src/hover.rs +++ b/crates/squawk_ide/src/hover.rs @@ -230,6 +230,7 @@ pub fn hover(db: &dyn Db, position: InFile) -> Option { | ast::AnyName::PlpgsqlCondition(_) | ast::AnyName::PlpgsqlLabelName(_) | ast::AnyName::PlpgsqlLabelNameRef(_) + | ast::AnyName::PlpgsqlVarName(_) | ast::AnyName::PolicyRef(_) | ast::AnyName::PreparedStatementRef(_) | ast::AnyName::PropertyName(_) diff --git a/crates/squawk_parser/src/generated/syntax_kind.rs b/crates/squawk_parser/src/generated/syntax_kind.rs index 37b077ed..bb606006 100644 --- a/crates/squawk_parser/src/generated/syntax_kind.rs +++ b/crates/squawk_parser/src/generated/syntax_kind.rs @@ -1513,7 +1513,11 @@ pub enum SyntaxKind { PLPGSQL_LABEL, PLPGSQL_LABEL_NAME, PLPGSQL_LABEL_NAME_REF, + PLPGSQL_NOT_NULL, PLPGSQL_NULL_STMT, + PLPGSQL_VAR_DECL, + PLPGSQL_VAR_INIT, + PLPGSQL_VAR_NAME, POLICY, POLICY_COMMAND, POLICY_COMMAND_ALL, diff --git a/crates/squawk_parser/src/grammar.rs b/crates/squawk_parser/src/grammar.rs index a90ede43..ebdeeb5c 100644 --- a/crates/squawk_parser/src/grammar.rs +++ b/crates/squawk_parser/src/grammar.rs @@ -166,7 +166,7 @@ fn is_select_marker(cm: &CompletedMarker) -> bool { } #[derive(Clone, Copy, PartialEq, Eq)] -enum ExprKind { +pub(crate) enum ExprKind { Other, Select, } @@ -2547,7 +2547,7 @@ fn type_name(p: &mut Parser<'_>) { } } -fn func_type(p: &mut Parser<'_>) { +pub(crate) fn func_type(p: &mut Parser<'_>) { if opt_type_name_with(p, true, true).is_none() { p.error("expected type name"); } @@ -2997,7 +2997,7 @@ fn field_expr(p: &mut Parser<'_>, lhs: CompletedMarker) -> CompletedMarker { m.complete(p, FIELD_EXPR) } -fn expr(p: &mut Parser<'_>) -> Option<(CompletedMarker, ExprKind)> { +pub(crate) fn expr(p: &mut Parser<'_>) -> Option<(CompletedMarker, ExprKind)> { expr_bp(p, 1, &Restrictions::default()) } @@ -19522,7 +19522,7 @@ fn attribute_list(p: &mut Parser<'_>) { m.complete(p, ATTRIBUTE_LIST); } -fn opt_collate(p: &mut Parser<'_>) -> Option { +pub(crate) fn opt_collate(p: &mut Parser<'_>) -> Option { let m = p.start(); if p.eat(COLLATE_KW) { collation_ref(p); diff --git a/crates/squawk_parser/src/plpgsql_grammar.rs b/crates/squawk_parser/src/plpgsql_grammar.rs index 13e33980..95a17bd8 100644 --- a/crates/squawk_parser/src/plpgsql_grammar.rs +++ b/crates/squawk_parser/src/plpgsql_grammar.rs @@ -3,6 +3,7 @@ use crate::{ generated::token_sets::{ ALL_KEYWORDS, PLPGSQL_RESERVED_CONTEXTUAL_KEYWORDS, PLPGSQL_RESERVED_KEYWORDS, }, + grammar, syntax_kind::SyntaxKind::*, token_set::TokenSet, }; @@ -68,11 +69,53 @@ fn opt_declare_section(p: &mut Parser) { let m = p.start(); p.bump(DECLARE_KW); while !p.at(EOF) && !p.at(BEGIN_KW) { - temp_unknown(p, "expected a declaration"); + if at_var_decl(p) { + var_decl(p); + } else { + temp_unknown(p, "expected a declaration"); + } } m.complete(p, PLPGSQL_DECLARE_SECTION); } +fn var_decl(p: &mut Parser) { + let m = p.start(); + name(p, PLPGSQL_VAR_NAME); + if p.nth_at_contextual_kw(0, CONSTANT_KW) { + p.bump_remap(CONSTANT_KW); + } + grammar::func_type(p); + grammar::opt_collate(p); + opt_not_null(p); + opt_var_init(p); + p.expect(SEMICOLON); + m.complete(p, PLPGSQL_VAR_DECL); +} + +fn opt_not_null(p: &mut Parser) { + if !p.at(NOT_KW) { + return; + } + let m = p.start(); + p.bump(NOT_KW); + p.expect(NULL_KW); + m.complete(p, PLPGSQL_NOT_NULL); +} + +fn opt_var_init(p: &mut Parser) { + if !p.at(COLON_EQ) && !p.at(EQ) && !p.at(DEFAULT_KW) { + return; + } + let m = p.start(); + if !p.eat(COLON_EQ) && !p.eat(EQ) { + p.bump(DEFAULT_KW); + } + if grammar::expr(p).is_none() { + p.error("expected an expression"); + } + m.complete(p, PLPGSQL_VAR_INIT); +} + #[derive(Clone, Copy, PartialEq)] enum BodyKind { Block, @@ -163,6 +206,14 @@ fn condition(p: &mut Parser) { m.complete(p, PLPGSQL_CONDITION); } +fn at_var_decl(p: &Parser) -> bool { + at_name(p, 0) + && !p.nth_at_contextual_kw(1, ALIAS_KW) + && !p.nth_at(1, CURSOR_KW) + && !p.nth_at(1, SCROLL_KW) + && !p.nth_at(1, NO_KW) +} + fn at_block_start(p: &Parser) -> bool { p.at_ts(BLOCK_FIRST) || at_block_label(p) } diff --git a/crates/squawk_parser/tests/data/plpgsql/ok/var_decl.sql b/crates/squawk_parser/tests/data/plpgsql/ok/var_decl.sql new file mode 100644 index 00000000..172604d6 --- /dev/null +++ b/crates/squawk_parser/tests/data/plpgsql/ok/var_decl.sql @@ -0,0 +1,11 @@ +do $$ +declare + a int; + b constant text := 'x'; + c numeric(10, 2) default 0; + d text collate "C" not null = 'y'; + e some_table.some_col%type; +begin + null; +end +$$; diff --git a/crates/squawk_syntax/src/ast/generated/nodes.rs b/crates/squawk_syntax/src/ast/generated/nodes.rs index 01a2b2c1..74da4109 100644 --- a/crates/squawk_syntax/src/ast/generated/nodes.rs +++ b/crates/squawk_syntax/src/ast/generated/nodes.rs @@ -19399,7 +19399,7 @@ pub struct Plpgsql { } impl Plpgsql { #[inline] - pub fn plpgsql_block(&self) -> Option { + pub fn block(&self) -> Option { support::child(&self.syntax) } } @@ -19410,23 +19410,23 @@ pub struct PlpgsqlBlock { } impl PlpgsqlBlock { #[inline] - pub fn plpgsql_body(&self) -> Option { + pub fn body(&self) -> Option { support::child(&self.syntax) } #[inline] - pub fn plpgsql_declare_section(&self) -> Option { + pub fn declare_section(&self) -> Option { support::child(&self.syntax) } #[inline] - pub fn plpgsql_exception_section(&self) -> Option { + pub fn end_label(&self) -> Option { support::child(&self.syntax) } #[inline] - pub fn plpgsql_label(&self) -> Option { + pub fn exception(&self) -> Option { support::child(&self.syntax) } #[inline] - pub fn plpgsql_label_name_ref(&self) -> Option { + pub fn label(&self) -> Option { support::child(&self.syntax) } #[inline] @@ -19474,6 +19474,10 @@ pub struct PlpgsqlDeclareSection { pub(crate) syntax: SyntaxNode, } impl PlpgsqlDeclareSection { + #[inline] + pub fn decls(&self) -> AstChildren { + support::children(&self.syntax) + } #[inline] pub fn declare_token(&self) -> Option { support::token(&self.syntax, SyntaxKind::DECLARE_KW) @@ -19486,12 +19490,12 @@ pub struct PlpgsqlExceptionHandler { } impl PlpgsqlExceptionHandler { #[inline] - pub fn conditions(&self) -> AstChildren { - support::children(&self.syntax) + pub fn body(&self) -> Option { + support::child(&self.syntax) } #[inline] - pub fn plpgsql_body(&self) -> Option { - support::child(&self.syntax) + pub fn conditions(&self) -> AstChildren { + support::children(&self.syntax) } #[inline] pub fn then_token(&self) -> Option { @@ -19524,7 +19528,7 @@ pub struct PlpgsqlLabel { } impl PlpgsqlLabel { #[inline] - pub fn plpgsql_label_name(&self) -> Option { + pub fn name(&self) -> Option { support::child(&self.syntax) } #[inline] @@ -19559,6 +19563,21 @@ impl PlpgsqlLabelNameRef { } } +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct PlpgsqlNotNull { + pub(crate) syntax: SyntaxNode, +} +impl PlpgsqlNotNull { + #[inline] + pub fn not_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::NOT_KW) + } + #[inline] + pub fn null_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::NULL_KW) + } +} + #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct PlpgsqlNullStmt { pub(crate) syntax: SyntaxNode, @@ -19574,6 +19593,75 @@ impl PlpgsqlNullStmt { } } +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct PlpgsqlVarDecl { + pub(crate) syntax: SyntaxNode, +} +impl PlpgsqlVarDecl { + #[inline] + pub fn collate(&self) -> Option { + support::child(&self.syntax) + } + #[inline] + pub fn func_type(&self) -> Option { + support::child(&self.syntax) + } + #[inline] + pub fn init(&self) -> Option { + support::child(&self.syntax) + } + #[inline] + pub fn name(&self) -> Option { + support::child(&self.syntax) + } + #[inline] + pub fn not_null(&self) -> Option { + support::child(&self.syntax) + } + #[inline] + pub fn semicolon_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::SEMICOLON) + } + #[inline] + pub fn constant_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::CONSTANT_KW) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct PlpgsqlVarInit { + pub(crate) syntax: SyntaxNode, +} +impl PlpgsqlVarInit { + #[inline] + pub fn expr(&self) -> Option { + support::child(&self.syntax) + } + #[inline] + pub fn colon_eq_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::COLON_EQ) + } + #[inline] + pub fn eq_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::EQ) + } + #[inline] + pub fn default_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::DEFAULT_KW) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct PlpgsqlVarName { + pub(crate) syntax: SyntaxNode, +} +impl PlpgsqlVarName { + #[inline] + pub fn ident_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::IDENT) + } +} + #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct Policy { pub(crate) syntax: SyntaxNode, @@ -28685,6 +28773,7 @@ pub enum AnyName { PlpgsqlCondition(PlpgsqlCondition), PlpgsqlLabelName(PlpgsqlLabelName), PlpgsqlLabelNameRef(PlpgsqlLabelNameRef), + PlpgsqlVarName(PlpgsqlVarName), Policy(Policy), PolicyRef(PolicyRef), PreparedStatement(PreparedStatement), @@ -46381,6 +46470,24 @@ impl AstNode for PlpgsqlLabelNameRef { &self.syntax } } +impl AstNode for PlpgsqlNotNull { + #[inline] + fn can_cast(kind: SyntaxKind) -> bool { + kind == SyntaxKind::PLPGSQL_NOT_NULL + } + #[inline] + fn cast(syntax: SyntaxNode) -> Option { + if Self::can_cast(syntax.kind()) { + Some(Self { syntax }) + } else { + None + } + } + #[inline] + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} impl AstNode for PlpgsqlNullStmt { #[inline] fn can_cast(kind: SyntaxKind) -> bool { @@ -46399,6 +46506,60 @@ impl AstNode for PlpgsqlNullStmt { &self.syntax } } +impl AstNode for PlpgsqlVarDecl { + #[inline] + fn can_cast(kind: SyntaxKind) -> bool { + kind == SyntaxKind::PLPGSQL_VAR_DECL + } + #[inline] + fn cast(syntax: SyntaxNode) -> Option { + if Self::can_cast(syntax.kind()) { + Some(Self { syntax }) + } else { + None + } + } + #[inline] + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +impl AstNode for PlpgsqlVarInit { + #[inline] + fn can_cast(kind: SyntaxKind) -> bool { + kind == SyntaxKind::PLPGSQL_VAR_INIT + } + #[inline] + fn cast(syntax: SyntaxNode) -> Option { + if Self::can_cast(syntax.kind()) { + Some(Self { syntax }) + } else { + None + } + } + #[inline] + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +impl AstNode for PlpgsqlVarName { + #[inline] + fn can_cast(kind: SyntaxKind) -> bool { + kind == SyntaxKind::PLPGSQL_VAR_NAME + } + #[inline] + fn cast(syntax: SyntaxNode) -> Option { + if Self::can_cast(syntax.kind()) { + Some(Self { syntax }) + } else { + None + } + } + #[inline] + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} impl AstNode for Policy { #[inline] fn can_cast(kind: SyntaxKind) -> bool { @@ -58777,6 +58938,7 @@ impl AstNode for AnyName { | SyntaxKind::PLPGSQL_CONDITION | SyntaxKind::PLPGSQL_LABEL_NAME | SyntaxKind::PLPGSQL_LABEL_NAME_REF + | SyntaxKind::PLPGSQL_VAR_NAME | SyntaxKind::POLICY | SyntaxKind::POLICY_REF | SyntaxKind::PREPARED_STATEMENT @@ -58895,6 +59057,7 @@ impl AstNode for AnyName { SyntaxKind::PLPGSQL_LABEL_NAME_REF => { AnyName::PlpgsqlLabelNameRef(PlpgsqlLabelNameRef { syntax }) } + SyntaxKind::PLPGSQL_VAR_NAME => AnyName::PlpgsqlVarName(PlpgsqlVarName { syntax }), SyntaxKind::POLICY => AnyName::Policy(Policy { syntax }), SyntaxKind::POLICY_REF => AnyName::PolicyRef(PolicyRef { syntax }), SyntaxKind::PREPARED_STATEMENT => { @@ -59003,6 +59166,7 @@ impl AstNode for AnyName { AnyName::PlpgsqlCondition(it) => &it.syntax, AnyName::PlpgsqlLabelName(it) => &it.syntax, AnyName::PlpgsqlLabelNameRef(it) => &it.syntax, + AnyName::PlpgsqlVarName(it) => &it.syntax, AnyName::Policy(it) => &it.syntax, AnyName::PolicyRef(it) => &it.syntax, AnyName::PreparedStatement(it) => &it.syntax, @@ -59341,6 +59505,12 @@ impl From for AnyName { AnyName::PlpgsqlLabelNameRef(node) } } +impl From for AnyName { + #[inline] + fn from(node: PlpgsqlVarName) -> AnyName { + AnyName::PlpgsqlVarName(node) + } +} impl From for AnyName { #[inline] fn from(node: Policy) -> AnyName { diff --git a/crates/squawk_syntax/src/postgresql.ungram b/crates/squawk_syntax/src/postgresql.ungram index a99e7971..58fc4b14 100644 --- a/crates/squawk_syntax/src/postgresql.ungram +++ b/crates/squawk_syntax/src/postgresql.ungram @@ -2037,16 +2037,16 @@ RoutineBody = | AtomicBody Plpgsql = - PlpgsqlBlock? + block:PlpgsqlBlock? PlpgsqlBlock = - PlpgsqlLabel? - PlpgsqlDeclareSection? + label:PlpgsqlLabel? + declare_section:PlpgsqlDeclareSection? 'begin' - PlpgsqlBody - PlpgsqlExceptionSection? + body:PlpgsqlBody + exception:PlpgsqlExceptionSection? 'end' - PlpgsqlLabelNameRef? + end_label:PlpgsqlLabelNameRef? ';'? PlpgsqlExceptionSection = @@ -2057,14 +2057,14 @@ PlpgsqlExceptionHandler = 'when' conditions:(PlpgsqlCondition*) 'then' - PlpgsqlBody + body:PlpgsqlBody PlpgsqlCondition = '#name'? '@string'? PlpgsqlLabel = - '<<' PlpgsqlLabelName '>>' + '<<' name:PlpgsqlLabelName '>>' PlpgsqlLabelName = '#name'? @@ -2074,6 +2074,26 @@ PlpgsqlLabelNameRef = PlpgsqlDeclareSection = 'declare' + decls:(PlpgsqlVarDecl*) + +PlpgsqlVarDecl = + name:PlpgsqlVarName + 'constant'? + FuncType + Collate? + not_null:PlpgsqlNotNull? + init:PlpgsqlVarInit? + ';' + +PlpgsqlNotNull = + 'not' 'null' + +PlpgsqlVarName = + '#name'? + +PlpgsqlVarInit = + (':=' | '=' | 'default') + Expr PlpgsqlBody = stmts:(PlpgsqlStmt*) diff --git a/crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_var_decl_ok.snap b/crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_var_decl_ok.snap new file mode 100644 index 00000000..9cadcc79 --- /dev/null +++ b/crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_var_decl_ok.snap @@ -0,0 +1,120 @@ +--- +source: crates/squawk_syntax/src/test.rs +input_file: crates/squawk_parser/tests/data/plpgsql/ok/var_decl.sql +--- +PLPGSQL@0..159 + WHITESPACE@0..1 "\n" + PLPGSQL_BLOCK@1..158 + PLPGSQL_DECLARE_SECTION@1..140 + DECLARE_KW@1..8 "declare" + WHITESPACE@8..11 "\n " + PLPGSQL_VAR_DECL@11..17 + PLPGSQL_VAR_NAME@11..12 + IDENT@11..12 "a" + WHITESPACE@12..13 " " + PATH_TYPE@13..16 + PATH_REF@13..16 + PATH_SEGMENT_REF@13..16 + INT_KW@13..16 "int" + SEMICOLON@16..17 ";" + WHITESPACE@17..20 "\n " + PLPGSQL_VAR_DECL@20..43 + PLPGSQL_VAR_NAME@20..21 + IDENT@20..21 "b" + WHITESPACE@21..22 " " + CONSTANT_KW@22..30 "constant" + WHITESPACE@30..31 " " + PATH_TYPE@31..35 + PATH_REF@31..35 + PATH_SEGMENT_REF@31..35 + TEXT_KW@31..35 "text" + WHITESPACE@35..36 " " + PLPGSQL_VAR_INIT@36..42 + COLON_EQ@36..38 ":=" + WHITESPACE@38..39 " " + LITERAL@39..42 + STRING@39..42 "'x'" + SEMICOLON@42..43 ";" + WHITESPACE@43..46 "\n " + PLPGSQL_VAR_DECL@46..73 + PLPGSQL_VAR_NAME@46..47 + IDENT@46..47 "c" + WHITESPACE@47..48 " " + PATH_TYPE@48..62 + PATH_REF@48..55 + PATH_SEGMENT_REF@48..55 + NUMERIC_KW@48..55 "numeric" + ARG_LIST@55..62 + L_PAREN@55..56 "(" + ARG@56..58 + LITERAL@56..58 + INT_NUMBER@56..58 "10" + COMMA@58..59 "," + WHITESPACE@59..60 " " + ARG@60..61 + LITERAL@60..61 + INT_NUMBER@60..61 "2" + R_PAREN@61..62 ")" + WHITESPACE@62..63 " " + PLPGSQL_VAR_INIT@63..72 + DEFAULT_KW@63..70 "default" + WHITESPACE@70..71 " " + LITERAL@71..72 + INT_NUMBER@71..72 "0" + SEMICOLON@72..73 ";" + WHITESPACE@73..76 "\n " + PLPGSQL_VAR_DECL@76..110 + PLPGSQL_VAR_NAME@76..77 + IDENT@76..77 "d" + WHITESPACE@77..78 " " + PATH_TYPE@78..82 + PATH_REF@78..82 + PATH_SEGMENT_REF@78..82 + TEXT_KW@78..82 "text" + WHITESPACE@82..83 " " + COLLATE@83..94 + COLLATE_KW@83..90 "collate" + WHITESPACE@90..91 " " + COLLATION_REF@91..94 + PATH_REF@91..94 + PATH_SEGMENT_REF@91..94 + IDENT@91..94 "\"C\"" + WHITESPACE@94..95 " " + PLPGSQL_NOT_NULL@95..103 + NOT_KW@95..98 "not" + WHITESPACE@98..99 " " + NULL_KW@99..103 "null" + WHITESPACE@103..104 " " + PLPGSQL_VAR_INIT@104..109 + EQ@104..105 "=" + WHITESPACE@105..106 " " + LITERAL@106..109 + STRING@106..109 "'y'" + SEMICOLON@109..110 ";" + WHITESPACE@110..113 "\n " + PLPGSQL_VAR_DECL@113..140 + PLPGSQL_VAR_NAME@113..114 + IDENT@113..114 "e" + WHITESPACE@114..115 " " + PERCENT_TYPE@115..139 + PATH_REF@115..134 + PATH_REF@115..125 + PATH_SEGMENT_REF@115..125 + IDENT@115..125 "some_table" + DOT@125..126 "." + PATH_SEGMENT_REF@126..134 + IDENT@126..134 "some_col" + PERCENT_TYPE_CLAUSE@134..139 + PERCENT@134..135 "%" + TYPE_KW@135..139 "type" + SEMICOLON@139..140 ";" + WHITESPACE@140..141 "\n" + BEGIN_KW@141..146 "begin" + WHITESPACE@146..149 "\n " + PLPGSQL_BODY@149..154 + PLPGSQL_NULL_STMT@149..154 + NULL_KW@149..153 "null" + SEMICOLON@153..154 ";" + WHITESPACE@154..155 "\n" + END_KW@155..158 "end" + WHITESPACE@158..159 "\n" diff --git a/crates/squawk_syntax/src/test.rs b/crates/squawk_syntax/src/test.rs index 0d276cde..ffb01595 100644 --- a/crates/squawk_syntax/src/test.rs +++ b/crates/squawk_syntax/src/test.rs @@ -271,21 +271,21 @@ fn plpgsql_suite_score() { assert_snapshot!(table, @" file bodies tokens unparsed err - plpgsql.sql 254 9959 8995 1182 - plpgsql_array.sql 26 949 867 110 - plpgsql_cache.sql 2 60 53 8 - plpgsql_call.sql 45 1698 1553 224 - plpgsql_control.sql 27 1402 1259 175 + plpgsql.sql 254 9926 8183 984 + plpgsql_array.sql 26 948 681 102 + plpgsql_cache.sql 2 60 44 5 + plpgsql_call.sql 45 1687 1374 177 + plpgsql_control.sql 27 1400 1212 164 plpgsql_copy.sql 4 28 16 4 - plpgsql_domain.sql 23 307 252 39 - plpgsql_misc.sql 16 261 213 29 - plpgsql_record.sql 65 1994 1794 252 - plpgsql_simple.sql 9 217 191 31 - plpgsql_transaction.sql 37 1203 1076 153 - plpgsql_trap.sql 7 354 276 52 + plpgsql_domain.sql 23 300 179 30 + plpgsql_misc.sql 16 261 132 24 + plpgsql_record.sql 65 1994 1576 187 + plpgsql_simple.sql 9 213 145 21 + plpgsql_transaction.sql 37 1202 999 136 + plpgsql_trap.sql 7 354 255 45 plpgsql_trigger.sql 1 55 53 6 - plpgsql_varprops.sql 33 736 635 92 - total 549 19223 17233 2357 + plpgsql_varprops.sql 33 716 387 57 + total 549 19144 15236 1942 "); } From a9ee2ed9a316492a9a7d162354aef7d8127aca17 Mon Sep 17 00:00:00 2001 From: Steve Dignam Date: Thu, 17 Sep 2026 19:12:13 -0400 Subject: [PATCH 08/11] if, case, aliases, cursor --- crates/squawk_ide/src/expand_selection.rs | 1 + crates/squawk_ide/src/folding_ranges.rs | 3 +- .../src/generated/syntax_kind.rs | 11 + crates/squawk_parser/src/grammar.rs | 40 +- crates/squawk_parser/src/plpgsql_grammar.rs | 273 +++++++- .../tests/data/plpgsql/err/alias_decl.sql | 9 + .../tests/data/plpgsql/err/case_stmt.sql | 27 + .../tests/data/plpgsql/err/cursor_decl.sql | 10 + .../tests/data/plpgsql/err/if_stmt.sql | 24 + .../tests/data/plpgsql/ok/alias_decl.sql | 13 + .../tests/data/plpgsql/ok/case_stmt.sql | 39 ++ .../tests/data/plpgsql/ok/cursor_decl.sql | 12 + .../tests/data/plpgsql/ok/if_stmt.sql | 40 ++ .../tests/data/plpgsql/ok/percent_type.sql | 19 + .../squawk_syntax/src/ast/generated/nodes.rs | 630 +++++++++++++++++- crates/squawk_syntax/src/plpgsql.rs | 14 +- crates/squawk_syntax/src/postgresql.ungram | 85 ++- ..._syntax__test__plpgsql_alias_decl_err.snap | 82 +++ ...k_syntax__test__plpgsql_alias_decl_ok.snap | 121 ++++ ...k_syntax__test__plpgsql_case_stmt_err.snap | 222 ++++++ ...wk_syntax__test__plpgsql_case_stmt_ok.snap | 273 ++++++++ ...syntax__test__plpgsql_cursor_decl_err.snap | 101 +++ ..._syntax__test__plpgsql_cursor_decl_ok.snap | 216 ++++++ ...awk_syntax__test__plpgsql_if_stmt_err.snap | 174 +++++ ...uawk_syntax__test__plpgsql_if_stmt_ok.snap | 351 ++++++++++ ...syntax__test__plpgsql_percent_type_ok.snap | 217 ++++++ crates/squawk_syntax/src/test.rs | 30 +- crates/squawk_syntax/src/validation.rs | 35 + 28 files changed, 3010 insertions(+), 62 deletions(-) create mode 100644 crates/squawk_parser/tests/data/plpgsql/err/alias_decl.sql create mode 100644 crates/squawk_parser/tests/data/plpgsql/err/case_stmt.sql create mode 100644 crates/squawk_parser/tests/data/plpgsql/err/cursor_decl.sql create mode 100644 crates/squawk_parser/tests/data/plpgsql/err/if_stmt.sql create mode 100644 crates/squawk_parser/tests/data/plpgsql/ok/alias_decl.sql create mode 100644 crates/squawk_parser/tests/data/plpgsql/ok/case_stmt.sql create mode 100644 crates/squawk_parser/tests/data/plpgsql/ok/cursor_decl.sql create mode 100644 crates/squawk_parser/tests/data/plpgsql/ok/if_stmt.sql create mode 100644 crates/squawk_parser/tests/data/plpgsql/ok/percent_type.sql create mode 100644 crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_alias_decl_err.snap create mode 100644 crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_alias_decl_ok.snap create mode 100644 crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_case_stmt_err.snap create mode 100644 crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_case_stmt_ok.snap create mode 100644 crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_cursor_decl_err.snap create mode 100644 crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_cursor_decl_ok.snap create mode 100644 crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_if_stmt_err.snap create mode 100644 crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_if_stmt_ok.snap create mode 100644 crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_percent_type_ok.snap diff --git a/crates/squawk_ide/src/expand_selection.rs b/crates/squawk_ide/src/expand_selection.rs index a0eb72fe..6bf1f8e5 100644 --- a/crates/squawk_ide/src/expand_selection.rs +++ b/crates/squawk_ide/src/expand_selection.rs @@ -71,6 +71,7 @@ const DELIMITED_LIST_KINDS: &[SyntaxKind] = &[ SyntaxKind::PARAM_LIST, SyntaxKind::PARTITION_ITEM_LIST, SyntaxKind::PARTITION_LIST, + SyntaxKind::PLPGSQL_CURSOR_ARG_LIST, SyntaxKind::TABLE_NAME_REF_LIST, SyntaxKind::REINDEX_OPTION_LIST, SyntaxKind::RELATION_LIST, diff --git a/crates/squawk_ide/src/folding_ranges.rs b/crates/squawk_ide/src/folding_ranges.rs index 21bd6f75..f10e0485 100644 --- a/crates/squawk_ide/src/folding_ranges.rs +++ b/crates/squawk_ide/src/folding_ranges.rs @@ -128,7 +128,8 @@ fn fold_kind(kind: SyntaxKind) -> Option { SyntaxKind::ARG_LIST | SyntaxKind::TABLE_ARG_LIST | SyntaxKind::RETURN_TABLE_ARG_LIST - | SyntaxKind::PARAM_LIST => Some(FoldKind::ArgList), + | SyntaxKind::PARAM_LIST + | SyntaxKind::PLPGSQL_CURSOR_ARG_LIST => Some(FoldKind::ArgList), SyntaxKind::ARRAY_EXPR => Some(FoldKind::Array), SyntaxKind::CALL_EXPR => Some(FoldKind::FunctionCall), SyntaxKind::JOIN => Some(FoldKind::Join), diff --git a/crates/squawk_parser/src/generated/syntax_kind.rs b/crates/squawk_parser/src/generated/syntax_kind.rs index bb606006..306a5f6e 100644 --- a/crates/squawk_parser/src/generated/syntax_kind.rs +++ b/crates/squawk_parser/src/generated/syntax_kind.rs @@ -1504,17 +1504,28 @@ pub enum SyntaxKind { PERCENT_TYPE_CLAUSE, PERIOD_COLUMN, PLPGSQL, + PLPGSQL_ALIAS_DECL, + PLPGSQL_ALIAS_TARGET, PLPGSQL_BLOCK, PLPGSQL_BODY, + PLPGSQL_CASE_STMT, + PLPGSQL_CASE_WHEN, PLPGSQL_CONDITION, + PLPGSQL_CURSOR_ARG, + PLPGSQL_CURSOR_ARG_LIST, + PLPGSQL_CURSOR_DECL, PLPGSQL_DECLARE_SECTION, + PLPGSQL_ELSE_CLAUSE, + PLPGSQL_ELSIF_CLAUSE, PLPGSQL_EXCEPTION_HANDLER, PLPGSQL_EXCEPTION_SECTION, + PLPGSQL_IF_STMT, PLPGSQL_LABEL, PLPGSQL_LABEL_NAME, PLPGSQL_LABEL_NAME_REF, PLPGSQL_NOT_NULL, PLPGSQL_NULL_STMT, + PLPGSQL_PERCENT_ROWTYPE, PLPGSQL_VAR_DECL, PLPGSQL_VAR_INIT, PLPGSQL_VAR_NAME, diff --git a/crates/squawk_parser/src/grammar.rs b/crates/squawk_parser/src/grammar.rs index ebdeeb5c..57de1622 100644 --- a/crates/squawk_parser/src/grammar.rs +++ b/crates/squawk_parser/src/grammar.rs @@ -2323,7 +2323,7 @@ fn path_for_qualifier( } } -fn opt_percent_type(p: &mut Parser<'_>) -> Option { +pub(crate) fn opt_percent_type(p: &mut Parser<'_>) -> Option { if p.at(PERCENT) && p.nth_at(1, TYPE_KW) { let m = p.start(); p.bump(PERCENT); @@ -2334,7 +2334,7 @@ fn opt_percent_type(p: &mut Parser<'_>) -> Option { } } -fn opt_array_bound(p: &mut Parser<'_>) -> bool { +pub(crate) fn opt_array_bound(p: &mut Parser<'_>) -> bool { if !p.at(L_BRACK) { return false; } @@ -7960,12 +7960,12 @@ fn rollback(p: &mut Parser<'_>) -> CompletedMarker { } #[derive(Default)] -struct StmtRestrictions { +pub(crate) struct StmtRestrictions { begin_end_allowed: bool, semi_allowed: bool, } -fn stmt(p: &mut Parser, r: &StmtRestrictions) -> Option { +pub(crate) fn stmt(p: &mut Parser, r: &StmtRestrictions) -> Option { match (p.current(), p.nth(1)) { (SEMICOLON, _) => Some(empty_stmt(p)), (ABORT_KW, _) => Some(rollback(p)), @@ -15998,6 +15998,23 @@ fn do_(p: &mut Parser<'_>) -> CompletedMarker { m.complete(p, DO) } +pub(crate) fn opt_cursor_scroll(p: &mut Parser<'_>) { + match p.current() { + NO_KW => { + let m = p.start(); + p.bump(NO_KW); + p.expect(SCROLL_KW); + m.complete(p, NO_SCROLL); + } + SCROLL_KW => { + let m = p.start(); + p.bump(SCROLL_KW); + m.complete(p, SCROLL); + } + _ => (), + } +} + // DECLARE name [ BINARY ] [ ASENSITIVE | INSENSITIVE ] [ [ NO ] SCROLL ] // CURSOR [ { WITH | WITHOUT } HOLD ] FOR query fn declare(p: &mut Parser<'_>) -> CompletedMarker { @@ -16022,20 +16039,7 @@ fn declare(p: &mut Parser<'_>) -> CompletedMarker { _ => (), } // [ [ NO ] SCROLL ] - match p.current() { - NO_KW => { - let m = p.start(); - p.bump(NO_KW); - p.expect(SCROLL_KW); - m.complete(p, NO_SCROLL); - } - SCROLL_KW => { - let m = p.start(); - p.bump(SCROLL_KW); - m.complete(p, SCROLL); - } - _ => (), - } + opt_cursor_scroll(p); p.expect(CURSOR_KW); // [ { WITH | WITHOUT } HOLD ] match p.current() { diff --git a/crates/squawk_parser/src/plpgsql_grammar.rs b/crates/squawk_parser/src/plpgsql_grammar.rs index 95a17bd8..1aa89fcd 100644 --- a/crates/squawk_parser/src/plpgsql_grammar.rs +++ b/crates/squawk_parser/src/plpgsql_grammar.rs @@ -12,7 +12,7 @@ pub(crate) fn plpgsql_entry_point(p: &mut Parser) { let m = p.start(); while !p.at(EOF) { if at_block_start(p) { - block(p); + opt_block(p); } else { temp_unknown(p, "expected a block"); } @@ -22,8 +22,10 @@ pub(crate) fn plpgsql_entry_point(p: &mut Parser) { const BLOCK_FIRST: TokenSet = TokenSet::new(&[BEGIN_KW, DECLARE_KW]); -fn block(p: &mut Parser) { - assert!(at_block_start(p)); +fn opt_block(p: &mut Parser) { + if !at_block_start(p) { + return; + } let m = p.start(); opt_block_label(p); opt_declare_section(p); @@ -69,7 +71,11 @@ fn opt_declare_section(p: &mut Parser) { let m = p.start(); p.bump(DECLARE_KW); while !p.at(EOF) && !p.at(BEGIN_KW) { - if at_var_decl(p) { + if at_alias_decl(p) { + alias_decl(p); + } else if at_cursor_decl(p) { + cursor_decl(p); + } else if at_var_decl(p) { var_decl(p); } else { temp_unknown(p, "expected a declaration"); @@ -84,7 +90,7 @@ fn var_decl(p: &mut Parser) { if p.nth_at_contextual_kw(0, CONSTANT_KW) { p.bump_remap(CONSTANT_KW); } - grammar::func_type(p); + decl_datatype(p); grammar::opt_collate(p); opt_not_null(p); opt_var_init(p); @@ -92,6 +98,126 @@ fn var_decl(p: &mut Parser) { m.complete(p, PLPGSQL_VAR_DECL); } +fn alias_decl(p: &mut Parser) { + let m = p.start(); + name(p, PLPGSQL_VAR_NAME); + p.bump_remap(ALIAS_KW); + p.expect(FOR_KW); + alias_target(p); + p.expect(SEMICOLON); + m.complete(p, PLPGSQL_ALIAS_DECL); +} + +fn alias_target(p: &mut Parser) { + let m = p.start(); + if at_path(p).is_some() { + path_name_ref(p); + } else { + let kind = p.current(); + p.error(format!("expected an alias target, found {kind:?}")); + } + m.complete(p, PLPGSQL_ALIAS_TARGET); +} + +fn cursor_decl(p: &mut Parser) { + let m = p.start(); + name(p, PLPGSQL_VAR_NAME); + grammar::opt_cursor_scroll(p); + p.expect(CURSOR_KW); + if p.at(L_PAREN) { + cursor_arg_list(p); + } + if !p.eat(IS_KW) { + p.expect(FOR_KW); + } + if grammar::stmt(p, &grammar::StmtRestrictions::default()).is_none() { + p.error("expected a query"); + } + p.expect(SEMICOLON); + m.complete(p, PLPGSQL_CURSOR_DECL); +} + +fn cursor_arg_list(p: &mut Parser) { + assert!(p.at(L_PAREN)); + let m = p.start(); + // TODO: use delimited + p.bump(L_PAREN); + if p.at(R_PAREN) { + p.error("expected at least one cursor argument"); + } + while !p.at(EOF) && !p.at(R_PAREN) { + cursor_arg(p); + if !p.eat(COMMA) { + break; + } + } + p.expect(R_PAREN); + m.complete(p, PLPGSQL_CURSOR_ARG_LIST); +} + +fn cursor_arg(p: &mut Parser) { + let m = p.start(); + if at_name(p, 0) { + name(p, PLPGSQL_VAR_NAME); + decl_datatype(p); + } else { + let kind = p.current(); + p.error(format!("expected a cursor argument name, found {kind:?}")); + } + m.complete(p, PLPGSQL_CURSOR_ARG); +} + +fn decl_datatype(p: &mut Parser) { + if !at_percent_datatype(p) { + grammar::func_type(p); + return; + } + let m = p.start(); + path_name_ref(p); + let kind = if grammar::opt_percent_type(p).is_some() { + PERCENT_TYPE + } else { + p.bump(PERCENT); + p.bump_remap(ROWTYPE_KW); + PLPGSQL_PERCENT_ROWTYPE + }; + p.eat(ARRAY_KW); + while !p.at(EOF) && grammar::opt_array_bound(p) {} + m.complete(p, kind); +} + +fn path_name_ref(p: &mut Parser) { + assert!(at_path(p).is_some()); + let m = p.start(); + name(p, PATH_SEGMENT_REF); + let mut path = m.complete(p, PATH_REF); + while !p.at(EOF) && p.at(DOT) { + let m = path.precede(p); + p.bump(DOT); + name(p, PATH_SEGMENT_REF); + path = m.complete(p, PATH_REF); + } +} + +fn at_percent_datatype(p: &Parser) -> bool { + let Some(n) = at_path(p) else { + return false; + }; + p.nth_at(n, PERCENT) && (p.nth_at(n + 1, TYPE_KW) || p.nth_at_contextual_kw(n + 1, ROWTYPE_KW)) +} + +fn at_path(p: &Parser) -> Option { + let mut n = 0; + while !p.nth_at(n, EOF) && at_name(p, n) { + n += 1; + if !p.nth_at(n, DOT) { + return Some(n); + } + n += 1; + } + None +} + fn opt_not_null(p: &mut Parser) { if !p.at(NOT_KW) { return; @@ -110,16 +236,24 @@ fn opt_var_init(p: &mut Parser) { if !p.eat(COLON_EQ) && !p.eat(EQ) { p.bump(DEFAULT_KW); } + expr(p); + m.complete(p, PLPGSQL_VAR_INIT); +} + +fn expr(p: &mut Parser) { if grammar::expr(p).is_none() { p.error("expected an expression"); } - m.complete(p, PLPGSQL_VAR_INIT); } #[derive(Clone, Copy, PartialEq)] enum BodyKind { Block, ExceptionHandler, + IfThen, + IfElse, + CaseWhen, + CaseElse, } fn body(p: &mut Parser, kind: BodyKind) { @@ -132,7 +266,11 @@ fn body(p: &mut Parser, kind: BodyKind) { fn stmt(p: &mut Parser) { if at_block_start(p) { - block(p); + opt_block(p); + } else if p.at(CASE_KW) { + case_stmt(p); + } else if p.at(IF_KW) { + if_stmt(p); } else if p.at(NULL_KW) && p.nth_at(1, SEMICOLON) { let m = p.start(); p.bump(NULL_KW); @@ -143,6 +281,81 @@ fn stmt(p: &mut Parser) { } } +fn if_stmt(p: &mut Parser) { + assert!(p.at(IF_KW)); + let m = p.start(); + p.bump(IF_KW); + expr(p); + p.expect(THEN_KW); + body(p, BodyKind::IfThen); + while !p.at(EOF) && opt_elsif_clause(p) {} + opt_else_clause(p, BodyKind::IfElse); + p.expect(END_KW); + p.expect(IF_KW); + p.expect(SEMICOLON); + m.complete(p, PLPGSQL_IF_STMT); +} + +fn case_stmt(p: &mut Parser) { + assert!(p.at(CASE_KW)); + let m = p.start(); + p.bump(CASE_KW); + if !p.at(WHEN_KW) { + expr(p); + if !p.at(WHEN_KW) { + p.error("expected `when`"); + } + } + while !p.at(EOF) && p.at(WHEN_KW) { + case_when(p); + } + opt_else_clause(p, BodyKind::CaseElse); + p.expect(END_KW); + p.expect(CASE_KW); + p.expect(SEMICOLON); + m.complete(p, PLPGSQL_CASE_STMT); +} + +fn case_when(p: &mut Parser) { + assert!(p.at(WHEN_KW)); + let m = p.start(); + p.bump(WHEN_KW); + expr(p); + while !p.at(EOF) && p.eat(COMMA) { + expr(p); + } + p.expect(THEN_KW); + body(p, BodyKind::CaseWhen); + m.complete(p, PLPGSQL_CASE_WHEN); +} + +fn opt_elsif_clause(p: &mut Parser) -> bool { + if !at_elsif(p) { + return false; + } + let m = p.start(); + if p.nth_at_contextual_kw(0, ELSEIF_KW) { + p.bump_remap(ELSEIF_KW); + } else { + p.bump_remap(ELSIF_KW); + } + expr(p); + p.expect(THEN_KW); + body(p, BodyKind::IfThen); + m.complete(p, PLPGSQL_ELSIF_CLAUSE); + true +} + +fn opt_else_clause(p: &mut Parser, kind: BodyKind) { + if !p.at(ELSE_KW) { + return; + } + let m = p.start(); + p.bump(ELSE_KW); + body(p, kind); + m.complete(p, PLPGSQL_ELSE_CLAUSE); +} + // TODO: remove this once we get all the ast nodes working fn temp_unknown(p: &mut Parser, message: &str) { let m = p.start(); @@ -171,7 +384,7 @@ fn opt_exception_section(p: &mut Parser) { } let m = p.start(); p.bump_remap(EXCEPTION_KW); - while p.at(WHEN_KW) { + while !p.at(EOF) && p.at(WHEN_KW) { exception_handler(p); } m.complete(p, PLPGSQL_EXCEPTION_SECTION); @@ -183,7 +396,7 @@ fn exception_handler(p: &mut Parser) { // TODO: use delimited p.bump(WHEN_KW); condition(p); - while p.eat(OR_KW) { + while !p.at(EOF) && p.eat(OR_KW) { condition(p); } p.expect(THEN_KW); @@ -206,12 +419,16 @@ fn condition(p: &mut Parser) { m.complete(p, PLPGSQL_CONDITION); } +fn at_alias_decl(p: &Parser) -> bool { + at_name(p, 0) && p.nth_at_contextual_kw(1, ALIAS_KW) +} + +fn at_cursor_decl(p: &Parser) -> bool { + at_name(p, 0) && (p.nth_at(1, CURSOR_KW) || p.nth_at(1, SCROLL_KW) || p.nth_at(1, NO_KW)) +} + fn at_var_decl(p: &Parser) -> bool { at_name(p, 0) - && !p.nth_at_contextual_kw(1, ALIAS_KW) - && !p.nth_at(1, CURSOR_KW) - && !p.nth_at(1, SCROLL_KW) - && !p.nth_at(1, NO_KW) } fn at_block_start(p: &Parser) -> bool { @@ -226,14 +443,36 @@ fn at_name(p: &Parser, n: usize) -> bool { if p.nth_at(n, IDENT) { return !p.nth_at_contextual_ts(n, PLPGSQL_RESERVED_CONTEXTUAL_KEYWORDS); } + if p.nth_at(n, POSITIONAL_PARAM) { + return true; + } p.nth_at_ts(n, ALL_KEYWORDS) && !p.nth_at_ts(n, PLPGSQL_RESERVED_KEYWORDS) } +fn at_elsif(p: &Parser) -> bool { + p.nth_at_contextual_kw(0, ELSIF_KW) || p.nth_at_contextual_kw(0, ELSEIF_KW) +} + fn at_body_end(p: &Parser, kind: BodyKind) -> bool { - at_block_end(p) - || p.nth_at_contextual_kw(0, EXCEPTION_KW) - // TODO: do we need this kind param? - || (kind == BodyKind::ExceptionHandler && p.at(WHEN_KW)) + if at_block_end(p) { + return true; + } + match kind { + BodyKind::Block => p.nth_at_contextual_kw(0, EXCEPTION_KW), + BodyKind::ExceptionHandler => p.nth_at_contextual_kw(0, EXCEPTION_KW) || p.at(WHEN_KW), + BodyKind::IfThen => at_end_if(p) || p.at(ELSE_KW) || at_elsif(p), + BodyKind::IfElse => at_end_if(p), + BodyKind::CaseWhen => at_end_case(p) || p.at(WHEN_KW) || p.at(ELSE_KW), + BodyKind::CaseElse => at_end_case(p), + } +} + +fn at_end_if(p: &Parser) -> bool { + p.at(END_KW) && p.nth_at(1, IF_KW) +} + +fn at_end_case(p: &Parser) -> bool { + p.at(END_KW) && p.nth_at(1, CASE_KW) } fn at_block_end(p: &Parser) -> bool { diff --git a/crates/squawk_parser/tests/data/plpgsql/err/alias_decl.sql b/crates/squawk_parser/tests/data/plpgsql/err/alias_decl.sql new file mode 100644 index 00000000..2f142753 --- /dev/null +++ b/crates/squawk_parser/tests/data/plpgsql/err/alias_decl.sql @@ -0,0 +1,9 @@ +do $$ +declare + a alias for 5; + b alias 1; + c int; +begin + null; +end +$$; diff --git a/crates/squawk_parser/tests/data/plpgsql/err/case_stmt.sql b/crates/squawk_parser/tests/data/plpgsql/err/case_stmt.sql new file mode 100644 index 00000000..15736778 --- /dev/null +++ b/crates/squawk_parser/tests/data/plpgsql/err/case_stmt.sql @@ -0,0 +1,27 @@ +do $$ +declare + x int := 1; +begin + case x + else + null; + end case; + + case x + when 1 then + null; + end case + + case case when x = 1 then 1 end + when 1 then + null; + end case; + + case + when case when x = 1 then true end then + null; + when x = 2, case when x = 3 then true end then + null; + end case; +end +$$; diff --git a/crates/squawk_parser/tests/data/plpgsql/err/cursor_decl.sql b/crates/squawk_parser/tests/data/plpgsql/err/cursor_decl.sql new file mode 100644 index 00000000..2d715de6 --- /dev/null +++ b/crates/squawk_parser/tests/data/plpgsql/err/cursor_decl.sql @@ -0,0 +1,10 @@ +do $$ +declare + a cursor () for select 1; + b no cursor for select 1; + c cursor select 1; + d int; +begin + null; +end +$$; diff --git a/crates/squawk_parser/tests/data/plpgsql/err/if_stmt.sql b/crates/squawk_parser/tests/data/plpgsql/err/if_stmt.sql new file mode 100644 index 00000000..ee739201 --- /dev/null +++ b/crates/squawk_parser/tests/data/plpgsql/err/if_stmt.sql @@ -0,0 +1,24 @@ +do $$ +begin + if 1 = 1 + null; + end if; + + if 1 = 1 then + null; + else + null; + elsif 1 = 2 then + null; + end if; + + -- `expr_until_then` ends the condition at the case's own `then` + if case when true then 1 end = 1 then + null; + elsif 1 = case when true then 1 end then + null; + end if; + + null; +end +$$; diff --git a/crates/squawk_parser/tests/data/plpgsql/ok/alias_decl.sql b/crates/squawk_parser/tests/data/plpgsql/ok/alias_decl.sql new file mode 100644 index 00000000..130e6fd0 --- /dev/null +++ b/crates/squawk_parser/tests/data/plpgsql/ok/alias_decl.sql @@ -0,0 +1,13 @@ +do $$ +<> +declare + a alias for new; + b alias for $1; + c alias for blk.x; + d alias for pg_catalog.pg_class.relname; + e alias for "my var"; + f alias for value; +begin + null; +end +$$; diff --git a/crates/squawk_parser/tests/data/plpgsql/ok/case_stmt.sql b/crates/squawk_parser/tests/data/plpgsql/ok/case_stmt.sql new file mode 100644 index 00000000..08d4f44a --- /dev/null +++ b/crates/squawk_parser/tests/data/plpgsql/ok/case_stmt.sql @@ -0,0 +1,39 @@ +do $$ +declare + x int := 1; +begin + case x + when 1 then + null; + end case; + + case x + when 1, 2 then + null; + when 3 then + null; + else + null; + end case; + + case + when x = 1 then + case x + when 1 then + null; + end case; + when x = 2 then + null; + else + end case; + + case (case when x = 1 then 1 end) + when 1 then + end case; + + case coalesce(case when x = 1 then 1 end, 0) + when 1 then + null; + end case; +end +$$; diff --git a/crates/squawk_parser/tests/data/plpgsql/ok/cursor_decl.sql b/crates/squawk_parser/tests/data/plpgsql/ok/cursor_decl.sql new file mode 100644 index 00000000..77f91344 --- /dev/null +++ b/crates/squawk_parser/tests/data/plpgsql/ok/cursor_decl.sql @@ -0,0 +1,12 @@ +do $$ +declare + a cursor for select 1; + b scroll cursor for select f1 from int4_tbl; + c no scroll cursor for select f1 from int4_tbl; + d cursor (r1 integer, r2 integer) for select * from generate_series(r1, r2) i; + e cursor (p1 int4_tbl.f1%type) is select 1; + cursor cursor for table t; +begin + null; +end +$$; diff --git a/crates/squawk_parser/tests/data/plpgsql/ok/if_stmt.sql b/crates/squawk_parser/tests/data/plpgsql/ok/if_stmt.sql new file mode 100644 index 00000000..d7249c44 --- /dev/null +++ b/crates/squawk_parser/tests/data/plpgsql/ok/if_stmt.sql @@ -0,0 +1,40 @@ +do $$ +declare + x int := 1; +begin + if x > 0 then + null; + end if; + + if x = 1 then + null; + else + null; + end if; + + if x = 1 then + if x = 2 then + null; + end if; + elsif x = 2 then + null; + elseif x = 3 then + null; + elsif (case when x = 4 then true else false end) then + null; + else + null; + end if; + + if x is null then + end if; + + if coalesce(case when x = 1 then 1 end, 0) = 1 then + null; + end if; + + if (array[case when x = 1 then 1 end])[1] = 1 then + null; + end if; +end +$$; diff --git a/crates/squawk_parser/tests/data/plpgsql/ok/percent_type.sql b/crates/squawk_parser/tests/data/plpgsql/ok/percent_type.sql new file mode 100644 index 00000000..7d60d1fa --- /dev/null +++ b/crates/squawk_parser/tests/data/plpgsql/ok/percent_type.sql @@ -0,0 +1,19 @@ +do $$ +declare + v int; + a v%type; + b v%type[]; + c v%type[1][3]; + d v%type array; + e v%type array[1]; + f foo%rowtype; + g notice%rowtype; + h foo.bar.baz%rowtype; + i pg_catalog.pg_class%rowtype[]; + j table%rowtype; + k select%type; + l $1%type; +begin + null; +end +$$; diff --git a/crates/squawk_syntax/src/ast/generated/nodes.rs b/crates/squawk_syntax/src/ast/generated/nodes.rs index 74da4109..055d40e4 100644 --- a/crates/squawk_syntax/src/ast/generated/nodes.rs +++ b/crates/squawk_syntax/src/ast/generated/nodes.rs @@ -19319,6 +19319,10 @@ pub struct PathSegmentRef { pub(crate) syntax: SyntaxNode, } impl PathSegmentRef { + #[inline] + pub fn positional_param_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::POSITIONAL_PARAM) + } #[inline] pub fn ident_token(&self) -> Option { support::token(&self.syntax, SyntaxKind::IDENT) @@ -19349,6 +19353,10 @@ pub struct PercentType { pub(crate) syntax: SyntaxNode, } impl PercentType { + #[inline] + pub fn array_bounds(&self) -> AstChildren { + support::children(&self.syntax) + } #[inline] pub fn path_ref(&self) -> Option { support::child(&self.syntax) @@ -19358,6 +19366,10 @@ impl PercentType { support::child(&self.syntax) } #[inline] + pub fn array_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::ARRAY_KW) + } + #[inline] pub fn setof_token(&self) -> Option { support::token(&self.syntax, SyntaxKind::SETOF_KW) } @@ -19404,6 +19416,40 @@ impl Plpgsql { } } +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct PlpgsqlAliasDecl { + pub(crate) syntax: SyntaxNode, +} +impl PlpgsqlAliasDecl { + #[inline] + pub fn name(&self) -> Option { + support::child(&self.syntax) + } + #[inline] + pub fn semicolon_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::SEMICOLON) + } + #[inline] + pub fn alias_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::ALIAS_KW) + } + #[inline] + pub fn for_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::FOR_KW) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct PlpgsqlAliasTarget { + pub(crate) syntax: SyntaxNode, +} +impl PlpgsqlAliasTarget { + #[inline] + pub fn path_ref(&self) -> Option { + support::child(&self.syntax) + } +} + #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct PlpgsqlBlock { pub(crate) syntax: SyntaxNode, @@ -19454,11 +19500,69 @@ impl PlpgsqlBody { } } +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct PlpgsqlCaseStmt { + pub(crate) syntax: SyntaxNode, +} +impl PlpgsqlCaseStmt { + #[inline] + pub fn else_clause(&self) -> Option { + support::child(&self.syntax) + } + #[inline] + pub fn subject(&self) -> Option { + support::child(&self.syntax) + } + #[inline] + pub fn whens(&self) -> AstChildren { + support::children(&self.syntax) + } + #[inline] + pub fn semicolon_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::SEMICOLON) + } + #[inline] + pub fn case_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::CASE_KW) + } + #[inline] + pub fn end_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::END_KW) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct PlpgsqlCaseWhen { + pub(crate) syntax: SyntaxNode, +} +impl PlpgsqlCaseWhen { + #[inline] + pub fn body(&self) -> Option { + support::child(&self.syntax) + } + #[inline] + pub fn conds(&self) -> AstChildren { + support::children(&self.syntax) + } + #[inline] + pub fn then_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::THEN_KW) + } + #[inline] + pub fn when_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::WHEN_KW) + } +} + #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct PlpgsqlCondition { pub(crate) syntax: SyntaxNode, } impl PlpgsqlCondition { + #[inline] + pub fn positional_param_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::POSITIONAL_PARAM) + } #[inline] pub fn string_token(&self) -> Option { support::token(&self.syntax, SyntaxKind::STRING) @@ -19469,13 +19573,86 @@ impl PlpgsqlCondition { } } +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct PlpgsqlCursorArg { + pub(crate) syntax: SyntaxNode, +} +impl PlpgsqlCursorArg { + #[inline] + pub fn name(&self) -> Option { + support::child(&self.syntax) + } + #[inline] + pub fn ty(&self) -> Option { + support::child(&self.syntax) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct PlpgsqlCursorArgList { + pub(crate) syntax: SyntaxNode, +} +impl PlpgsqlCursorArgList { + #[inline] + pub fn plpgsql_cursor_args(&self) -> AstChildren { + support::children(&self.syntax) + } + #[inline] + pub fn l_paren_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::L_PAREN) + } + #[inline] + pub fn r_paren_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::R_PAREN) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct PlpgsqlCursorDecl { + pub(crate) syntax: SyntaxNode, +} +impl PlpgsqlCursorDecl { + #[inline] + pub fn args(&self) -> Option { + support::child(&self.syntax) + } + #[inline] + pub fn name(&self) -> Option { + support::child(&self.syntax) + } + #[inline] + pub fn query(&self) -> Option { + support::child(&self.syntax) + } + #[inline] + pub fn scroll(&self) -> Option { + support::child(&self.syntax) + } + #[inline] + pub fn semicolon_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::SEMICOLON) + } + #[inline] + pub fn cursor_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::CURSOR_KW) + } + #[inline] + pub fn for_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::FOR_KW) + } + #[inline] + pub fn is_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::IS_KW) + } +} + #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct PlpgsqlDeclareSection { pub(crate) syntax: SyntaxNode, } impl PlpgsqlDeclareSection { #[inline] - pub fn decls(&self) -> AstChildren { + pub fn decls(&self) -> AstChildren { support::children(&self.syntax) } #[inline] @@ -19484,6 +19661,48 @@ impl PlpgsqlDeclareSection { } } +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct PlpgsqlElseClause { + pub(crate) syntax: SyntaxNode, +} +impl PlpgsqlElseClause { + #[inline] + pub fn body(&self) -> Option { + support::child(&self.syntax) + } + #[inline] + pub fn else_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::ELSE_KW) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct PlpgsqlElsifClause { + pub(crate) syntax: SyntaxNode, +} +impl PlpgsqlElsifClause { + #[inline] + pub fn body(&self) -> Option { + support::child(&self.syntax) + } + #[inline] + pub fn cond(&self) -> Option { + support::child(&self.syntax) + } + #[inline] + pub fn elseif_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::ELSEIF_KW) + } + #[inline] + pub fn elsif_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::ELSIF_KW) + } + #[inline] + pub fn then_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::THEN_KW) + } +} + #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct PlpgsqlExceptionHandler { pub(crate) syntax: SyntaxNode, @@ -19522,6 +19741,45 @@ impl PlpgsqlExceptionSection { } } +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct PlpgsqlIfStmt { + pub(crate) syntax: SyntaxNode, +} +impl PlpgsqlIfStmt { + #[inline] + pub fn cond(&self) -> Option { + support::child(&self.syntax) + } + #[inline] + pub fn else_clause(&self) -> Option { + support::child(&self.syntax) + } + #[inline] + pub fn elsifs(&self) -> AstChildren { + support::children(&self.syntax) + } + #[inline] + pub fn then_body(&self) -> Option { + support::child(&self.syntax) + } + #[inline] + pub fn semicolon_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::SEMICOLON) + } + #[inline] + pub fn end_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::END_KW) + } + #[inline] + pub fn if_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::IF_KW) + } + #[inline] + pub fn then_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::THEN_KW) + } +} + #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct PlpgsqlLabel { pub(crate) syntax: SyntaxNode, @@ -19546,6 +19804,10 @@ pub struct PlpgsqlLabelName { pub(crate) syntax: SyntaxNode, } impl PlpgsqlLabelName { + #[inline] + pub fn positional_param_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::POSITIONAL_PARAM) + } #[inline] pub fn ident_token(&self) -> Option { support::token(&self.syntax, SyntaxKind::IDENT) @@ -19557,6 +19819,10 @@ pub struct PlpgsqlLabelNameRef { pub(crate) syntax: SyntaxNode, } impl PlpgsqlLabelNameRef { + #[inline] + pub fn positional_param_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::POSITIONAL_PARAM) + } #[inline] pub fn ident_token(&self) -> Option { support::token(&self.syntax, SyntaxKind::IDENT) @@ -19594,16 +19860,39 @@ impl PlpgsqlNullStmt { } #[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct PlpgsqlVarDecl { +pub struct PlpgsqlPercentRowtype { pub(crate) syntax: SyntaxNode, } -impl PlpgsqlVarDecl { +impl PlpgsqlPercentRowtype { #[inline] - pub fn collate(&self) -> Option { + pub fn array_bounds(&self) -> AstChildren { + support::children(&self.syntax) + } + #[inline] + pub fn path_ref(&self) -> Option { support::child(&self.syntax) } #[inline] - pub fn func_type(&self) -> Option { + pub fn percent_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::PERCENT) + } + #[inline] + pub fn array_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::ARRAY_KW) + } + #[inline] + pub fn rowtype_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::ROWTYPE_KW) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct PlpgsqlVarDecl { + pub(crate) syntax: SyntaxNode, +} +impl PlpgsqlVarDecl { + #[inline] + pub fn collate(&self) -> Option { support::child(&self.syntax) } #[inline] @@ -19619,6 +19908,10 @@ impl PlpgsqlVarDecl { support::child(&self.syntax) } #[inline] + pub fn ty(&self) -> Option { + support::child(&self.syntax) + } + #[inline] pub fn semicolon_token(&self) -> Option { support::token(&self.syntax, SyntaxKind::SEMICOLON) } @@ -19656,6 +19949,10 @@ pub struct PlpgsqlVarName { pub(crate) syntax: SyntaxNode, } impl PlpgsqlVarName { + #[inline] + pub fn positional_param_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::POSITIONAL_PARAM) + } #[inline] pub fn ident_token(&self) -> Option { support::token(&self.syntax, SyntaxKind::IDENT) @@ -29674,9 +29971,24 @@ pub enum Persistence { Unlogged(Unlogged), } +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum PlpgsqlDecl { + PlpgsqlAliasDecl(PlpgsqlAliasDecl), + PlpgsqlCursorDecl(PlpgsqlCursorDecl), + PlpgsqlVarDecl(PlpgsqlVarDecl), +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum PlpgsqlDeclType { + PlpgsqlPercentRowtype(PlpgsqlPercentRowtype), + FuncType(FuncType), +} + #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum PlpgsqlStmt { PlpgsqlBlock(PlpgsqlBlock), + PlpgsqlCaseStmt(PlpgsqlCaseStmt), + PlpgsqlIfStmt(PlpgsqlIfStmt), PlpgsqlNullStmt(PlpgsqlNullStmt), } @@ -46308,6 +46620,42 @@ impl AstNode for Plpgsql { &self.syntax } } +impl AstNode for PlpgsqlAliasDecl { + #[inline] + fn can_cast(kind: SyntaxKind) -> bool { + kind == SyntaxKind::PLPGSQL_ALIAS_DECL + } + #[inline] + fn cast(syntax: SyntaxNode) -> Option { + if Self::can_cast(syntax.kind()) { + Some(Self { syntax }) + } else { + None + } + } + #[inline] + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +impl AstNode for PlpgsqlAliasTarget { + #[inline] + fn can_cast(kind: SyntaxKind) -> bool { + kind == SyntaxKind::PLPGSQL_ALIAS_TARGET + } + #[inline] + fn cast(syntax: SyntaxNode) -> Option { + if Self::can_cast(syntax.kind()) { + Some(Self { syntax }) + } else { + None + } + } + #[inline] + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} impl AstNode for PlpgsqlBlock { #[inline] fn can_cast(kind: SyntaxKind) -> bool { @@ -46344,6 +46692,42 @@ impl AstNode for PlpgsqlBody { &self.syntax } } +impl AstNode for PlpgsqlCaseStmt { + #[inline] + fn can_cast(kind: SyntaxKind) -> bool { + kind == SyntaxKind::PLPGSQL_CASE_STMT + } + #[inline] + fn cast(syntax: SyntaxNode) -> Option { + if Self::can_cast(syntax.kind()) { + Some(Self { syntax }) + } else { + None + } + } + #[inline] + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +impl AstNode for PlpgsqlCaseWhen { + #[inline] + fn can_cast(kind: SyntaxKind) -> bool { + kind == SyntaxKind::PLPGSQL_CASE_WHEN + } + #[inline] + fn cast(syntax: SyntaxNode) -> Option { + if Self::can_cast(syntax.kind()) { + Some(Self { syntax }) + } else { + None + } + } + #[inline] + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} impl AstNode for PlpgsqlCondition { #[inline] fn can_cast(kind: SyntaxKind) -> bool { @@ -46362,6 +46746,60 @@ impl AstNode for PlpgsqlCondition { &self.syntax } } +impl AstNode for PlpgsqlCursorArg { + #[inline] + fn can_cast(kind: SyntaxKind) -> bool { + kind == SyntaxKind::PLPGSQL_CURSOR_ARG + } + #[inline] + fn cast(syntax: SyntaxNode) -> Option { + if Self::can_cast(syntax.kind()) { + Some(Self { syntax }) + } else { + None + } + } + #[inline] + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +impl AstNode for PlpgsqlCursorArgList { + #[inline] + fn can_cast(kind: SyntaxKind) -> bool { + kind == SyntaxKind::PLPGSQL_CURSOR_ARG_LIST + } + #[inline] + fn cast(syntax: SyntaxNode) -> Option { + if Self::can_cast(syntax.kind()) { + Some(Self { syntax }) + } else { + None + } + } + #[inline] + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +impl AstNode for PlpgsqlCursorDecl { + #[inline] + fn can_cast(kind: SyntaxKind) -> bool { + kind == SyntaxKind::PLPGSQL_CURSOR_DECL + } + #[inline] + fn cast(syntax: SyntaxNode) -> Option { + if Self::can_cast(syntax.kind()) { + Some(Self { syntax }) + } else { + None + } + } + #[inline] + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} impl AstNode for PlpgsqlDeclareSection { #[inline] fn can_cast(kind: SyntaxKind) -> bool { @@ -46380,6 +46818,42 @@ impl AstNode for PlpgsqlDeclareSection { &self.syntax } } +impl AstNode for PlpgsqlElseClause { + #[inline] + fn can_cast(kind: SyntaxKind) -> bool { + kind == SyntaxKind::PLPGSQL_ELSE_CLAUSE + } + #[inline] + fn cast(syntax: SyntaxNode) -> Option { + if Self::can_cast(syntax.kind()) { + Some(Self { syntax }) + } else { + None + } + } + #[inline] + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +impl AstNode for PlpgsqlElsifClause { + #[inline] + fn can_cast(kind: SyntaxKind) -> bool { + kind == SyntaxKind::PLPGSQL_ELSIF_CLAUSE + } + #[inline] + fn cast(syntax: SyntaxNode) -> Option { + if Self::can_cast(syntax.kind()) { + Some(Self { syntax }) + } else { + None + } + } + #[inline] + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} impl AstNode for PlpgsqlExceptionHandler { #[inline] fn can_cast(kind: SyntaxKind) -> bool { @@ -46416,6 +46890,24 @@ impl AstNode for PlpgsqlExceptionSection { &self.syntax } } +impl AstNode for PlpgsqlIfStmt { + #[inline] + fn can_cast(kind: SyntaxKind) -> bool { + kind == SyntaxKind::PLPGSQL_IF_STMT + } + #[inline] + fn cast(syntax: SyntaxNode) -> Option { + if Self::can_cast(syntax.kind()) { + Some(Self { syntax }) + } else { + None + } + } + #[inline] + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} impl AstNode for PlpgsqlLabel { #[inline] fn can_cast(kind: SyntaxKind) -> bool { @@ -46506,6 +46998,24 @@ impl AstNode for PlpgsqlNullStmt { &self.syntax } } +impl AstNode for PlpgsqlPercentRowtype { + #[inline] + fn can_cast(kind: SyntaxKind) -> bool { + kind == SyntaxKind::PLPGSQL_PERCENT_ROWTYPE + } + #[inline] + fn cast(syntax: SyntaxNode) -> Option { + if Self::can_cast(syntax.kind()) { + Some(Self { syntax }) + } else { + None + } + } + #[inline] + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} impl AstNode for PlpgsqlVarDecl { #[inline] fn can_cast(kind: SyntaxKind) -> bool { @@ -66294,18 +66804,112 @@ impl From for Persistence { Persistence::Unlogged(node) } } +impl AstNode for PlpgsqlDecl { + #[inline] + fn can_cast(kind: SyntaxKind) -> bool { + matches!( + kind, + SyntaxKind::PLPGSQL_ALIAS_DECL + | SyntaxKind::PLPGSQL_CURSOR_DECL + | SyntaxKind::PLPGSQL_VAR_DECL + ) + } + #[inline] + fn cast(syntax: SyntaxNode) -> Option { + let res = match syntax.kind() { + SyntaxKind::PLPGSQL_ALIAS_DECL => { + PlpgsqlDecl::PlpgsqlAliasDecl(PlpgsqlAliasDecl { syntax }) + } + SyntaxKind::PLPGSQL_CURSOR_DECL => { + PlpgsqlDecl::PlpgsqlCursorDecl(PlpgsqlCursorDecl { syntax }) + } + SyntaxKind::PLPGSQL_VAR_DECL => PlpgsqlDecl::PlpgsqlVarDecl(PlpgsqlVarDecl { syntax }), + _ => { + return None; + } + }; + Some(res) + } + #[inline] + fn syntax(&self) -> &SyntaxNode { + match self { + PlpgsqlDecl::PlpgsqlAliasDecl(it) => &it.syntax, + PlpgsqlDecl::PlpgsqlCursorDecl(it) => &it.syntax, + PlpgsqlDecl::PlpgsqlVarDecl(it) => &it.syntax, + } + } +} +impl From for PlpgsqlDecl { + #[inline] + fn from(node: PlpgsqlAliasDecl) -> PlpgsqlDecl { + PlpgsqlDecl::PlpgsqlAliasDecl(node) + } +} +impl From for PlpgsqlDecl { + #[inline] + fn from(node: PlpgsqlCursorDecl) -> PlpgsqlDecl { + PlpgsqlDecl::PlpgsqlCursorDecl(node) + } +} +impl From for PlpgsqlDecl { + #[inline] + fn from(node: PlpgsqlVarDecl) -> PlpgsqlDecl { + PlpgsqlDecl::PlpgsqlVarDecl(node) + } +} +impl AstNode for PlpgsqlDeclType { + #[inline] + fn can_cast(kind: SyntaxKind) -> bool { + matches!(kind, SyntaxKind::PLPGSQL_PERCENT_ROWTYPE) || FuncType::can_cast(kind) + } + #[inline] + fn cast(syntax: SyntaxNode) -> Option { + let res = match syntax.kind() { + SyntaxKind::PLPGSQL_PERCENT_ROWTYPE => { + PlpgsqlDeclType::PlpgsqlPercentRowtype(PlpgsqlPercentRowtype { syntax }) + } + _ => { + if let Some(result) = FuncType::cast(syntax.clone()) { + return Some(PlpgsqlDeclType::FuncType(result)); + } + return None; + } + }; + Some(res) + } + #[inline] + fn syntax(&self) -> &SyntaxNode { + match self { + PlpgsqlDeclType::PlpgsqlPercentRowtype(it) => &it.syntax, + PlpgsqlDeclType::FuncType(it) => it.syntax(), + } + } +} +impl From for PlpgsqlDeclType { + #[inline] + fn from(node: PlpgsqlPercentRowtype) -> PlpgsqlDeclType { + PlpgsqlDeclType::PlpgsqlPercentRowtype(node) + } +} impl AstNode for PlpgsqlStmt { #[inline] fn can_cast(kind: SyntaxKind) -> bool { matches!( kind, - SyntaxKind::PLPGSQL_BLOCK | SyntaxKind::PLPGSQL_NULL_STMT + SyntaxKind::PLPGSQL_BLOCK + | SyntaxKind::PLPGSQL_CASE_STMT + | SyntaxKind::PLPGSQL_IF_STMT + | SyntaxKind::PLPGSQL_NULL_STMT ) } #[inline] fn cast(syntax: SyntaxNode) -> Option { let res = match syntax.kind() { SyntaxKind::PLPGSQL_BLOCK => PlpgsqlStmt::PlpgsqlBlock(PlpgsqlBlock { syntax }), + SyntaxKind::PLPGSQL_CASE_STMT => { + PlpgsqlStmt::PlpgsqlCaseStmt(PlpgsqlCaseStmt { syntax }) + } + SyntaxKind::PLPGSQL_IF_STMT => PlpgsqlStmt::PlpgsqlIfStmt(PlpgsqlIfStmt { syntax }), SyntaxKind::PLPGSQL_NULL_STMT => { PlpgsqlStmt::PlpgsqlNullStmt(PlpgsqlNullStmt { syntax }) } @@ -66319,6 +66923,8 @@ impl AstNode for PlpgsqlStmt { fn syntax(&self) -> &SyntaxNode { match self { PlpgsqlStmt::PlpgsqlBlock(it) => &it.syntax, + PlpgsqlStmt::PlpgsqlCaseStmt(it) => &it.syntax, + PlpgsqlStmt::PlpgsqlIfStmt(it) => &it.syntax, PlpgsqlStmt::PlpgsqlNullStmt(it) => &it.syntax, } } @@ -66329,6 +66935,18 @@ impl From for PlpgsqlStmt { PlpgsqlStmt::PlpgsqlBlock(node) } } +impl From for PlpgsqlStmt { + #[inline] + fn from(node: PlpgsqlCaseStmt) -> PlpgsqlStmt { + PlpgsqlStmt::PlpgsqlCaseStmt(node) + } +} +impl From for PlpgsqlStmt { + #[inline] + fn from(node: PlpgsqlIfStmt) -> PlpgsqlStmt { + PlpgsqlStmt::PlpgsqlIfStmt(node) + } +} impl From for PlpgsqlStmt { #[inline] fn from(node: PlpgsqlNullStmt) -> PlpgsqlStmt { diff --git a/crates/squawk_syntax/src/plpgsql.rs b/crates/squawk_syntax/src/plpgsql.rs index 6c71b209..3b876e63 100644 --- a/crates/squawk_syntax/src/plpgsql.rs +++ b/crates/squawk_syntax/src/plpgsql.rs @@ -2,6 +2,7 @@ use rowan::{GreenNode, TextRange}; use crate::{ SyntaxNode, ast, ast::AstNode, decoded_text::DecodedText, parsing, syntax_error::SyntaxError, + validation, }; pub struct Plpgsql { @@ -44,8 +45,17 @@ impl Plpgsql { self.decoded.source_range(range) } - pub fn errors(&self) -> &[SyntaxError] { - &self.errors + pub fn errors(self) -> Vec { + let mut validation_errors = vec![]; + validation::validate(&self.syntax(), &mut validation_errors); + + let mut errors = self.errors; + errors.extend(validation_errors.into_iter().map(|error| { + let range = self.decoded.source_range(error.range()); + error.with_range(range) + })); + errors.sort_by_key(|error| error.range().start()); + errors } } diff --git a/crates/squawk_syntax/src/postgresql.ungram b/crates/squawk_syntax/src/postgresql.ungram index 58fc4b14..2471d48a 100644 --- a/crates/squawk_syntax/src/postgresql.ungram +++ b/crates/squawk_syntax/src/postgresql.ungram @@ -35,6 +35,8 @@ PathSegment = PathSegmentRef = '#name'? + // '@positional_param' is only used in pl/pgsql, in an alias target + '@positional_param'? Path = qualifier:PathRef? @@ -767,7 +769,8 @@ ArrayBound = '[' Expr? ']' PercentType = - 'setof'? PathRef PercentTypeClause + // 'array' and ArrayBound are only used in pl/pgsql + 'setof'? PathRef PercentTypeClause 'array'? ArrayBound* PathType = 'setof'? PathRef ArgList? @@ -2061,6 +2064,7 @@ PlpgsqlExceptionHandler = PlpgsqlCondition = '#name'? + '@positional_param'? '@string'? PlpgsqlLabel = @@ -2068,43 +2072,118 @@ PlpgsqlLabel = PlpgsqlLabelName = '#name'? + '@positional_param'? PlpgsqlLabelNameRef = '#name'? + '@positional_param'? PlpgsqlDeclareSection = 'declare' - decls:(PlpgsqlVarDecl*) + decls:(PlpgsqlDecl*) + +PlpgsqlDecl = + PlpgsqlVarDecl +| PlpgsqlAliasDecl +| PlpgsqlCursorDecl PlpgsqlVarDecl = name:PlpgsqlVarName 'constant'? - FuncType + ty:PlpgsqlDeclType Collate? not_null:PlpgsqlNotNull? init:PlpgsqlVarInit? ';' +PlpgsqlDeclType = + FuncType +| PlpgsqlPercentRowtype + +PlpgsqlPercentRowtype = + PathRef '%' 'rowtype' 'array'? ArrayBound* + PlpgsqlNotNull = 'not' 'null' PlpgsqlVarName = '#name'? + '@positional_param'? PlpgsqlVarInit = (':=' | '=' | 'default') Expr +PlpgsqlAliasDecl = + name:PlpgsqlVarName + 'alias' + 'for' + target:PlpgsqlAliasTarget + ';' + +PlpgsqlAliasTarget = + PathRef + +PlpgsqlCursorDecl = + name:PlpgsqlVarName + scroll:CursorScroll? + 'cursor' + args:PlpgsqlCursorArgList? + ('is' | 'for') + query:Stmt + ';' + +PlpgsqlCursorArgList = + '(' (PlpgsqlCursorArg (',' PlpgsqlCursorArg)*) ')' + +PlpgsqlCursorArg = + name:PlpgsqlVarName + ty:PlpgsqlDeclType + PlpgsqlBody = stmts:(PlpgsqlStmt*) PlpgsqlStmt = PlpgsqlBlock +| PlpgsqlCaseStmt +| PlpgsqlIfStmt | PlpgsqlNullStmt PlpgsqlNullStmt = 'null' ';' +PlpgsqlIfStmt = + 'if' + cond:Expr + 'then' + then_body:PlpgsqlBody + elsifs:(PlpgsqlElsifClause*) + else_clause:PlpgsqlElseClause? + 'end' 'if' ';' + +PlpgsqlElsifClause = + ('elsif' | 'elseif') + cond:Expr + 'then' + body:PlpgsqlBody + +PlpgsqlElseClause = + 'else' + body:PlpgsqlBody + +PlpgsqlCaseStmt = + 'case' + subject:Expr? + whens:(PlpgsqlCaseWhen*) + else_clause:PlpgsqlElseClause? + 'end' 'case' ';' + +PlpgsqlCaseWhen = + 'when' + conds:(Expr (',' Expr)*) + 'then' + body:PlpgsqlBody + FuncOptionList = options:(FuncOption*) diff --git a/crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_alias_decl_err.snap b/crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_alias_decl_err.snap new file mode 100644 index 00000000..5b1f6872 --- /dev/null +++ b/crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_alias_decl_err.snap @@ -0,0 +1,82 @@ +--- +source: crates/squawk_syntax/src/test.rs +input_file: crates/squawk_parser/tests/data/plpgsql/err/alias_decl.sql +--- +PLPGSQL@0..66 + WHITESPACE@0..1 "\n" + PLPGSQL_BLOCK@1..65 + PLPGSQL_DECLARE_SECTION@1..47 + DECLARE_KW@1..8 "declare" + WHITESPACE@8..11 "\n " + PLPGSQL_ALIAS_DECL@11..23 + PLPGSQL_VAR_NAME@11..12 + IDENT@11..12 "a" + WHITESPACE@12..13 " " + ALIAS_KW@13..18 "alias" + WHITESPACE@18..19 " " + FOR_KW@19..22 "for" + WHITESPACE@22..23 " " + PLPGSQL_ALIAS_TARGET@23..23 + ERROR@23..25 + INT_NUMBER@23..24 "5" + SEMICOLON@24..25 ";" + WHITESPACE@25..28 "\n " + PLPGSQL_ALIAS_DECL@28..36 + PLPGSQL_VAR_NAME@28..29 + IDENT@28..29 "b" + WHITESPACE@29..30 " " + ALIAS_KW@30..35 "alias" + WHITESPACE@35..36 " " + PLPGSQL_ALIAS_TARGET@36..36 + ERROR@36..38 + INT_NUMBER@36..37 "1" + SEMICOLON@37..38 ";" + WHITESPACE@38..41 "\n " + PLPGSQL_VAR_DECL@41..47 + PLPGSQL_VAR_NAME@41..42 + IDENT@41..42 "c" + WHITESPACE@42..43 " " + PATH_TYPE@43..46 + PATH_REF@43..46 + PATH_SEGMENT_REF@43..46 + INT_KW@43..46 "int" + SEMICOLON@46..47 ";" + WHITESPACE@47..48 "\n" + BEGIN_KW@48..53 "begin" + WHITESPACE@53..56 "\n " + PLPGSQL_BODY@56..61 + PLPGSQL_NULL_STMT@56..61 + NULL_KW@56..60 "null" + SEMICOLON@60..61 ";" + WHITESPACE@61..62 "\n" + END_KW@62..65 "end" + WHITESPACE@65..66 "\n" + +error[syntax-error]: expected an alias target, found INT_NUMBER + ╭▸ +3 │ a alias for 5; + ╰╴ ━ +error[syntax-error]: expected SEMICOLON + ╭▸ +3 │ a alias for 5; + ╰╴ ━ +error[syntax-error]: expected a declaration, found INT_NUMBER + ╭▸ +3 │ a alias for 5; + ╰╴ ━ +error[syntax-error]: expected FOR_KW + ╭▸ +4 │ b alias 1; + ╰╴ ━ +error[syntax-error]: expected an alias target, found INT_NUMBER + ╭▸ +4 │ b alias 1; + ╰╴ ━ +error[syntax-error]: expected SEMICOLON + ╭▸ +4 │ b alias 1; + ╰╴ ━ +error[syntax-error]: expected a declaration, found INT_NUMBER + ╭▸ +4 │ b alias 1; + ╰╴ ━ diff --git a/crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_alias_decl_ok.snap b/crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_alias_decl_ok.snap new file mode 100644 index 00000000..f1fbd287 --- /dev/null +++ b/crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_alias_decl_ok.snap @@ -0,0 +1,121 @@ +--- +source: crates/squawk_syntax/src/test.rs +input_file: crates/squawk_parser/tests/data/plpgsql/ok/alias_decl.sql +--- +PLPGSQL@0..181 + WHITESPACE@0..1 "\n" + PLPGSQL_BLOCK@1..180 + PLPGSQL_LABEL@1..8 + LESS_LESS@1..3 "<<" + PLPGSQL_LABEL_NAME@3..6 + IDENT@3..6 "blk" + GREATER_GREATER@6..8 ">>" + WHITESPACE@8..9 "\n" + PLPGSQL_DECLARE_SECTION@9..162 + DECLARE_KW@9..16 "declare" + WHITESPACE@16..19 "\n " + PLPGSQL_ALIAS_DECL@19..35 + PLPGSQL_VAR_NAME@19..20 + IDENT@19..20 "a" + WHITESPACE@20..21 " " + ALIAS_KW@21..26 "alias" + WHITESPACE@26..27 " " + FOR_KW@27..30 "for" + WHITESPACE@30..31 " " + PLPGSQL_ALIAS_TARGET@31..34 + PATH_REF@31..34 + PATH_SEGMENT_REF@31..34 + NEW_KW@31..34 "new" + SEMICOLON@34..35 ";" + WHITESPACE@35..38 "\n " + PLPGSQL_ALIAS_DECL@38..53 + PLPGSQL_VAR_NAME@38..39 + IDENT@38..39 "b" + WHITESPACE@39..40 " " + ALIAS_KW@40..45 "alias" + WHITESPACE@45..46 " " + FOR_KW@46..49 "for" + WHITESPACE@49..50 " " + PLPGSQL_ALIAS_TARGET@50..52 + PATH_REF@50..52 + PATH_SEGMENT_REF@50..52 + POSITIONAL_PARAM@50..52 "$1" + SEMICOLON@52..53 ";" + WHITESPACE@53..56 "\n " + PLPGSQL_ALIAS_DECL@56..74 + PLPGSQL_VAR_NAME@56..57 + IDENT@56..57 "c" + WHITESPACE@57..58 " " + ALIAS_KW@58..63 "alias" + WHITESPACE@63..64 " " + FOR_KW@64..67 "for" + WHITESPACE@67..68 " " + PLPGSQL_ALIAS_TARGET@68..73 + PATH_REF@68..73 + PATH_REF@68..71 + PATH_SEGMENT_REF@68..71 + IDENT@68..71 "blk" + DOT@71..72 "." + PATH_SEGMENT_REF@72..73 + IDENT@72..73 "x" + SEMICOLON@73..74 ";" + WHITESPACE@74..77 "\n " + PLPGSQL_ALIAS_DECL@77..117 + PLPGSQL_VAR_NAME@77..78 + IDENT@77..78 "d" + WHITESPACE@78..79 " " + ALIAS_KW@79..84 "alias" + WHITESPACE@84..85 " " + FOR_KW@85..88 "for" + WHITESPACE@88..89 " " + PLPGSQL_ALIAS_TARGET@89..116 + PATH_REF@89..116 + PATH_REF@89..108 + PATH_REF@89..99 + PATH_SEGMENT_REF@89..99 + IDENT@89..99 "pg_catalog" + DOT@99..100 "." + PATH_SEGMENT_REF@100..108 + IDENT@100..108 "pg_class" + DOT@108..109 "." + PATH_SEGMENT_REF@109..116 + IDENT@109..116 "relname" + SEMICOLON@116..117 ";" + WHITESPACE@117..120 "\n " + PLPGSQL_ALIAS_DECL@120..141 + PLPGSQL_VAR_NAME@120..121 + IDENT@120..121 "e" + WHITESPACE@121..122 " " + ALIAS_KW@122..127 "alias" + WHITESPACE@127..128 " " + FOR_KW@128..131 "for" + WHITESPACE@131..132 " " + PLPGSQL_ALIAS_TARGET@132..140 + PATH_REF@132..140 + PATH_SEGMENT_REF@132..140 + IDENT@132..140 "\"my var\"" + SEMICOLON@140..141 ";" + WHITESPACE@141..144 "\n " + PLPGSQL_ALIAS_DECL@144..162 + PLPGSQL_VAR_NAME@144..145 + IDENT@144..145 "f" + WHITESPACE@145..146 " " + ALIAS_KW@146..151 "alias" + WHITESPACE@151..152 " " + FOR_KW@152..155 "for" + WHITESPACE@155..156 " " + PLPGSQL_ALIAS_TARGET@156..161 + PATH_REF@156..161 + PATH_SEGMENT_REF@156..161 + VALUE_KW@156..161 "value" + SEMICOLON@161..162 ";" + WHITESPACE@162..163 "\n" + BEGIN_KW@163..168 "begin" + WHITESPACE@168..171 "\n " + PLPGSQL_BODY@171..176 + PLPGSQL_NULL_STMT@171..176 + NULL_KW@171..175 "null" + SEMICOLON@175..176 ";" + WHITESPACE@176..177 "\n" + END_KW@177..180 "end" + WHITESPACE@180..181 "\n" diff --git a/crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_case_stmt_err.snap b/crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_case_stmt_err.snap new file mode 100644 index 00000000..4d3c0ace --- /dev/null +++ b/crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_case_stmt_err.snap @@ -0,0 +1,222 @@ +--- +source: crates/squawk_syntax/src/test.rs +input_file: crates/squawk_parser/tests/data/plpgsql/err/case_stmt.sql +--- +PLPGSQL@0..338 + WHITESPACE@0..1 "\n" + PLPGSQL_BLOCK@1..337 + PLPGSQL_DECLARE_SECTION@1..22 + DECLARE_KW@1..8 "declare" + WHITESPACE@8..11 "\n " + PLPGSQL_VAR_DECL@11..22 + PLPGSQL_VAR_NAME@11..12 + IDENT@11..12 "x" + WHITESPACE@12..13 " " + PATH_TYPE@13..16 + PATH_REF@13..16 + PATH_SEGMENT_REF@13..16 + INT_KW@13..16 "int" + WHITESPACE@16..17 " " + PLPGSQL_VAR_INIT@17..21 + COLON_EQ@17..19 ":=" + WHITESPACE@19..20 " " + LITERAL@20..21 + INT_NUMBER@20..21 "1" + SEMICOLON@21..22 ";" + WHITESPACE@22..23 "\n" + BEGIN_KW@23..28 "begin" + WHITESPACE@28..31 "\n " + PLPGSQL_BODY@31..333 + PLPGSQL_CASE_STMT@31..70 + CASE_KW@31..35 "case" + WHITESPACE@35..36 " " + NAME_REF@36..37 + IDENT@36..37 "x" + WHITESPACE@37..42 "\n " + PLPGSQL_ELSE_CLAUSE@42..58 + ELSE_KW@42..46 "else" + WHITESPACE@46..53 "\n " + PLPGSQL_BODY@53..58 + PLPGSQL_NULL_STMT@53..58 + NULL_KW@53..57 "null" + SEMICOLON@57..58 ";" + WHITESPACE@58..61 "\n " + END_KW@61..64 "end" + WHITESPACE@64..65 " " + CASE_KW@65..69 "case" + SEMICOLON@69..70 ";" + WHITESPACE@70..74 "\n\n " + PLPGSQL_CASE_STMT@74..119 + CASE_KW@74..78 "case" + WHITESPACE@78..79 " " + NAME_REF@79..80 + IDENT@79..80 "x" + WHITESPACE@80..85 "\n " + PLPGSQL_CASE_WHEN@85..108 + WHEN_KW@85..89 "when" + WHITESPACE@89..90 " " + LITERAL@90..91 + INT_NUMBER@90..91 "1" + WHITESPACE@91..92 " " + THEN_KW@92..96 "then" + WHITESPACE@96..103 "\n " + PLPGSQL_BODY@103..108 + PLPGSQL_NULL_STMT@103..108 + NULL_KW@103..107 "null" + SEMICOLON@107..108 ";" + WHITESPACE@108..111 "\n " + END_KW@111..114 "end" + WHITESPACE@114..115 " " + CASE_KW@115..119 "case" + WHITESPACE@119..123 "\n\n " + PLPGSQL_CASE_STMT@123..194 + CASE_KW@123..127 "case" + WHITESPACE@127..128 " " + CASE_EXPR@128..154 + CASE_KW@128..132 "case" + WHITESPACE@132..133 " " + WHEN_CLAUSE_LIST@133..150 + WHEN_CLAUSE@133..150 + WHEN_KW@133..137 "when" + WHITESPACE@137..138 " " + BIN_EXPR@138..143 + NAME_REF@138..139 + IDENT@138..139 "x" + WHITESPACE@139..140 " " + EQ@140..141 "=" + WHITESPACE@141..142 " " + LITERAL@142..143 + INT_NUMBER@142..143 "1" + WHITESPACE@143..144 " " + THEN_KW@144..148 "then" + WHITESPACE@148..149 " " + LITERAL@149..150 + INT_NUMBER@149..150 "1" + WHITESPACE@150..151 " " + END_KW@151..154 "end" + WHITESPACE@154..159 "\n " + PLPGSQL_CASE_WHEN@159..182 + WHEN_KW@159..163 "when" + WHITESPACE@163..164 " " + LITERAL@164..165 + INT_NUMBER@164..165 "1" + WHITESPACE@165..166 " " + THEN_KW@166..170 "then" + WHITESPACE@170..177 "\n " + PLPGSQL_BODY@177..182 + PLPGSQL_NULL_STMT@177..182 + NULL_KW@177..181 "null" + SEMICOLON@181..182 ";" + WHITESPACE@182..185 "\n " + END_KW@185..188 "end" + WHITESPACE@188..189 " " + CASE_KW@189..193 "case" + SEMICOLON@193..194 ";" + WHITESPACE@194..198 "\n\n " + PLPGSQL_CASE_STMT@198..333 + CASE_KW@198..202 "case" + WHITESPACE@202..207 "\n " + PLPGSQL_CASE_WHEN@207..258 + WHEN_KW@207..211 "when" + WHITESPACE@211..212 " " + CASE_EXPR@212..241 + CASE_KW@212..216 "case" + WHITESPACE@216..217 " " + WHEN_CLAUSE_LIST@217..237 + WHEN_CLAUSE@217..237 + WHEN_KW@217..221 "when" + WHITESPACE@221..222 " " + BIN_EXPR@222..227 + NAME_REF@222..223 + IDENT@222..223 "x" + WHITESPACE@223..224 " " + EQ@224..225 "=" + WHITESPACE@225..226 " " + LITERAL@226..227 + INT_NUMBER@226..227 "1" + WHITESPACE@227..228 " " + THEN_KW@228..232 "then" + WHITESPACE@232..233 " " + LITERAL@233..237 + TRUE_KW@233..237 "true" + WHITESPACE@237..238 " " + END_KW@238..241 "end" + WHITESPACE@241..242 " " + THEN_KW@242..246 "then" + WHITESPACE@246..253 "\n " + PLPGSQL_BODY@253..258 + PLPGSQL_NULL_STMT@253..258 + NULL_KW@253..257 "null" + SEMICOLON@257..258 ";" + WHITESPACE@258..263 "\n " + PLPGSQL_CASE_WHEN@263..321 + WHEN_KW@263..267 "when" + WHITESPACE@267..268 " " + BIN_EXPR@268..273 + NAME_REF@268..269 + IDENT@268..269 "x" + WHITESPACE@269..270 " " + EQ@270..271 "=" + WHITESPACE@271..272 " " + LITERAL@272..273 + INT_NUMBER@272..273 "2" + COMMA@273..274 "," + WHITESPACE@274..275 " " + CASE_EXPR@275..304 + CASE_KW@275..279 "case" + WHITESPACE@279..280 " " + WHEN_CLAUSE_LIST@280..300 + WHEN_CLAUSE@280..300 + WHEN_KW@280..284 "when" + WHITESPACE@284..285 " " + BIN_EXPR@285..290 + NAME_REF@285..286 + IDENT@285..286 "x" + WHITESPACE@286..287 " " + EQ@287..288 "=" + WHITESPACE@288..289 " " + LITERAL@289..290 + INT_NUMBER@289..290 "3" + WHITESPACE@290..291 " " + THEN_KW@291..295 "then" + WHITESPACE@295..296 " " + LITERAL@296..300 + TRUE_KW@296..300 "true" + WHITESPACE@300..301 " " + END_KW@301..304 "end" + WHITESPACE@304..305 " " + THEN_KW@305..309 "then" + WHITESPACE@309..316 "\n " + PLPGSQL_BODY@316..321 + PLPGSQL_NULL_STMT@316..321 + NULL_KW@316..320 "null" + SEMICOLON@320..321 ";" + WHITESPACE@321..324 "\n " + END_KW@324..327 "end" + WHITESPACE@327..328 " " + CASE_KW@328..332 "case" + SEMICOLON@332..333 ";" + WHITESPACE@333..334 "\n" + END_KW@334..337 "end" + WHITESPACE@337..338 "\n" + +error[syntax-error]: expected `when` + ╭▸ +5 │ case x + ╰╴ ━ +error[syntax-error]: expected SEMICOLON + ╭▸ +13 │ end case + ╰╴ ━ +error[syntax-error]: CASE expression must be parenthesized + ╭▸ +15 │ case case when x = 1 then 1 end + ╰╴ ━━━━━━━━━━━━━━━━━━━━━━━━━━ +error[syntax-error]: CASE expression must be parenthesized + ╭▸ +21 │ when case when x = 1 then true end then + ╰╴ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +error[syntax-error]: CASE expression must be parenthesized + ╭▸ +23 │ when x = 2, case when x = 3 then true end then + ╰╴ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ diff --git a/crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_case_stmt_ok.snap b/crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_case_stmt_ok.snap new file mode 100644 index 00000000..95513fb6 --- /dev/null +++ b/crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_case_stmt_ok.snap @@ -0,0 +1,273 @@ +--- +source: crates/squawk_syntax/src/test.rs +input_file: crates/squawk_parser/tests/data/plpgsql/ok/case_stmt.sql +--- +PLPGSQL@0..483 + WHITESPACE@0..1 "\n" + PLPGSQL_BLOCK@1..482 + PLPGSQL_DECLARE_SECTION@1..22 + DECLARE_KW@1..8 "declare" + WHITESPACE@8..11 "\n " + PLPGSQL_VAR_DECL@11..22 + PLPGSQL_VAR_NAME@11..12 + IDENT@11..12 "x" + WHITESPACE@12..13 " " + PATH_TYPE@13..16 + PATH_REF@13..16 + PATH_SEGMENT_REF@13..16 + INT_KW@13..16 "int" + WHITESPACE@16..17 " " + PLPGSQL_VAR_INIT@17..21 + COLON_EQ@17..19 ":=" + WHITESPACE@19..20 " " + LITERAL@20..21 + INT_NUMBER@20..21 "1" + SEMICOLON@21..22 ";" + WHITESPACE@22..23 "\n" + BEGIN_KW@23..28 "begin" + WHITESPACE@28..31 "\n " + PLPGSQL_BODY@31..478 + PLPGSQL_CASE_STMT@31..77 + CASE_KW@31..35 "case" + WHITESPACE@35..36 " " + NAME_REF@36..37 + IDENT@36..37 "x" + WHITESPACE@37..42 "\n " + PLPGSQL_CASE_WHEN@42..65 + WHEN_KW@42..46 "when" + WHITESPACE@46..47 " " + LITERAL@47..48 + INT_NUMBER@47..48 "1" + WHITESPACE@48..49 " " + THEN_KW@49..53 "then" + WHITESPACE@53..60 "\n " + PLPGSQL_BODY@60..65 + PLPGSQL_NULL_STMT@60..65 + NULL_KW@60..64 "null" + SEMICOLON@64..65 ";" + WHITESPACE@65..68 "\n " + END_KW@68..71 "end" + WHITESPACE@71..72 " " + CASE_KW@72..76 "case" + SEMICOLON@76..77 ";" + WHITESPACE@77..81 "\n\n " + PLPGSQL_CASE_STMT@81..179 + CASE_KW@81..85 "case" + WHITESPACE@85..86 " " + NAME_REF@86..87 + IDENT@86..87 "x" + WHITESPACE@87..92 "\n " + PLPGSQL_CASE_WHEN@92..118 + WHEN_KW@92..96 "when" + WHITESPACE@96..97 " " + LITERAL@97..98 + INT_NUMBER@97..98 "1" + COMMA@98..99 "," + WHITESPACE@99..100 " " + LITERAL@100..101 + INT_NUMBER@100..101 "2" + WHITESPACE@101..102 " " + THEN_KW@102..106 "then" + WHITESPACE@106..113 "\n " + PLPGSQL_BODY@113..118 + PLPGSQL_NULL_STMT@113..118 + NULL_KW@113..117 "null" + SEMICOLON@117..118 ";" + WHITESPACE@118..123 "\n " + PLPGSQL_CASE_WHEN@123..146 + WHEN_KW@123..127 "when" + WHITESPACE@127..128 " " + LITERAL@128..129 + INT_NUMBER@128..129 "3" + WHITESPACE@129..130 " " + THEN_KW@130..134 "then" + WHITESPACE@134..141 "\n " + PLPGSQL_BODY@141..146 + PLPGSQL_NULL_STMT@141..146 + NULL_KW@141..145 "null" + SEMICOLON@145..146 ";" + WHITESPACE@146..151 "\n " + PLPGSQL_ELSE_CLAUSE@151..167 + ELSE_KW@151..155 "else" + WHITESPACE@155..162 "\n " + PLPGSQL_BODY@162..167 + PLPGSQL_NULL_STMT@162..167 + NULL_KW@162..166 "null" + SEMICOLON@166..167 ";" + WHITESPACE@167..170 "\n " + END_KW@170..173 "end" + WHITESPACE@173..174 " " + CASE_KW@174..178 "case" + SEMICOLON@178..179 ";" + WHITESPACE@179..183 "\n\n " + PLPGSQL_CASE_STMT@183..325 + CASE_KW@183..187 "case" + WHITESPACE@187..192 "\n " + PLPGSQL_CASE_WHEN@192..272 + WHEN_KW@192..196 "when" + WHITESPACE@196..197 " " + BIN_EXPR@197..202 + NAME_REF@197..198 + IDENT@197..198 "x" + WHITESPACE@198..199 " " + EQ@199..200 "=" + WHITESPACE@200..201 " " + LITERAL@201..202 + INT_NUMBER@201..202 "1" + WHITESPACE@202..203 " " + THEN_KW@203..207 "then" + WHITESPACE@207..214 "\n " + PLPGSQL_BODY@214..272 + PLPGSQL_CASE_STMT@214..272 + CASE_KW@214..218 "case" + WHITESPACE@218..219 " " + NAME_REF@219..220 + IDENT@219..220 "x" + WHITESPACE@220..229 "\n " + PLPGSQL_CASE_WHEN@229..256 + WHEN_KW@229..233 "when" + WHITESPACE@233..234 " " + LITERAL@234..235 + INT_NUMBER@234..235 "1" + WHITESPACE@235..236 " " + THEN_KW@236..240 "then" + WHITESPACE@240..251 "\n " + PLPGSQL_BODY@251..256 + PLPGSQL_NULL_STMT@251..256 + NULL_KW@251..255 "null" + SEMICOLON@255..256 ";" + WHITESPACE@256..263 "\n " + END_KW@263..266 "end" + WHITESPACE@266..267 " " + CASE_KW@267..271 "case" + SEMICOLON@271..272 ";" + WHITESPACE@272..277 "\n " + PLPGSQL_CASE_WHEN@277..304 + WHEN_KW@277..281 "when" + WHITESPACE@281..282 " " + BIN_EXPR@282..287 + NAME_REF@282..283 + IDENT@282..283 "x" + WHITESPACE@283..284 " " + EQ@284..285 "=" + WHITESPACE@285..286 " " + LITERAL@286..287 + INT_NUMBER@286..287 "2" + WHITESPACE@287..288 " " + THEN_KW@288..292 "then" + WHITESPACE@292..299 "\n " + PLPGSQL_BODY@299..304 + PLPGSQL_NULL_STMT@299..304 + NULL_KW@299..303 "null" + SEMICOLON@303..304 ";" + WHITESPACE@304..309 "\n " + PLPGSQL_ELSE_CLAUSE@309..316 + ELSE_KW@309..313 "else" + WHITESPACE@313..316 "\n " + PLPGSQL_BODY@316..316 + END_KW@316..319 "end" + WHITESPACE@319..320 " " + CASE_KW@320..324 "case" + SEMICOLON@324..325 ";" + WHITESPACE@325..329 "\n\n " + PLPGSQL_CASE_STMT@329..390 + CASE_KW@329..333 "case" + WHITESPACE@333..334 " " + PAREN_EXPR@334..362 + L_PAREN@334..335 "(" + CASE_EXPR@335..361 + CASE_KW@335..339 "case" + WHITESPACE@339..340 " " + WHEN_CLAUSE_LIST@340..357 + WHEN_CLAUSE@340..357 + WHEN_KW@340..344 "when" + WHITESPACE@344..345 " " + BIN_EXPR@345..350 + NAME_REF@345..346 + IDENT@345..346 "x" + WHITESPACE@346..347 " " + EQ@347..348 "=" + WHITESPACE@348..349 " " + LITERAL@349..350 + INT_NUMBER@349..350 "1" + WHITESPACE@350..351 " " + THEN_KW@351..355 "then" + WHITESPACE@355..356 " " + LITERAL@356..357 + INT_NUMBER@356..357 "1" + WHITESPACE@357..358 " " + END_KW@358..361 "end" + R_PAREN@361..362 ")" + WHITESPACE@362..367 "\n " + PLPGSQL_CASE_WHEN@367..381 + WHEN_KW@367..371 "when" + WHITESPACE@371..372 " " + LITERAL@372..373 + INT_NUMBER@372..373 "1" + WHITESPACE@373..374 " " + THEN_KW@374..378 "then" + WHITESPACE@378..381 "\n " + PLPGSQL_BODY@381..381 + END_KW@381..384 "end" + WHITESPACE@384..385 " " + CASE_KW@385..389 "case" + SEMICOLON@389..390 ";" + WHITESPACE@390..394 "\n\n " + PLPGSQL_CASE_STMT@394..478 + CASE_KW@394..398 "case" + WHITESPACE@398..399 " " + CALL_EXPR@399..438 + NAME_REF@399..407 + COALESCE_KW@399..407 "coalesce" + ARG_LIST@407..438 + L_PAREN@407..408 "(" + ARG@408..434 + CASE_EXPR@408..434 + CASE_KW@408..412 "case" + WHITESPACE@412..413 " " + WHEN_CLAUSE_LIST@413..430 + WHEN_CLAUSE@413..430 + WHEN_KW@413..417 "when" + WHITESPACE@417..418 " " + BIN_EXPR@418..423 + NAME_REF@418..419 + IDENT@418..419 "x" + WHITESPACE@419..420 " " + EQ@420..421 "=" + WHITESPACE@421..422 " " + LITERAL@422..423 + INT_NUMBER@422..423 "1" + WHITESPACE@423..424 " " + THEN_KW@424..428 "then" + WHITESPACE@428..429 " " + LITERAL@429..430 + INT_NUMBER@429..430 "1" + WHITESPACE@430..431 " " + END_KW@431..434 "end" + COMMA@434..435 "," + WHITESPACE@435..436 " " + ARG@436..437 + LITERAL@436..437 + INT_NUMBER@436..437 "0" + R_PAREN@437..438 ")" + WHITESPACE@438..443 "\n " + PLPGSQL_CASE_WHEN@443..466 + WHEN_KW@443..447 "when" + WHITESPACE@447..448 " " + LITERAL@448..449 + INT_NUMBER@448..449 "1" + WHITESPACE@449..450 " " + THEN_KW@450..454 "then" + WHITESPACE@454..461 "\n " + PLPGSQL_BODY@461..466 + PLPGSQL_NULL_STMT@461..466 + NULL_KW@461..465 "null" + SEMICOLON@465..466 ";" + WHITESPACE@466..469 "\n " + END_KW@469..472 "end" + WHITESPACE@472..473 " " + CASE_KW@473..477 "case" + SEMICOLON@477..478 ";" + WHITESPACE@478..479 "\n" + END_KW@479..482 "end" + WHITESPACE@482..483 "\n" diff --git a/crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_cursor_decl_err.snap b/crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_cursor_decl_err.snap new file mode 100644 index 00000000..55cdda3e --- /dev/null +++ b/crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_cursor_decl_err.snap @@ -0,0 +1,101 @@ +--- +source: crates/squawk_syntax/src/test.rs +input_file: crates/squawk_parser/tests/data/plpgsql/err/cursor_decl.sql +--- +PLPGSQL@0..113 + WHITESPACE@0..1 "\n" + PLPGSQL_BLOCK@1..112 + PLPGSQL_DECLARE_SECTION@1..94 + DECLARE_KW@1..8 "declare" + WHITESPACE@8..11 "\n " + PLPGSQL_CURSOR_DECL@11..36 + PLPGSQL_VAR_NAME@11..12 + IDENT@11..12 "a" + WHITESPACE@12..13 " " + CURSOR_KW@13..19 "cursor" + WHITESPACE@19..20 " " + PLPGSQL_CURSOR_ARG_LIST@20..22 + L_PAREN@20..21 "(" + R_PAREN@21..22 ")" + WHITESPACE@22..23 " " + FOR_KW@23..26 "for" + WHITESPACE@26..27 " " + SELECT@27..35 + SELECT_CLAUSE@27..35 + SELECT_KW@27..33 "select" + WHITESPACE@33..34 " " + TARGET_LIST@34..35 + TARGET@34..35 + LITERAL@34..35 + INT_NUMBER@34..35 "1" + SEMICOLON@35..36 ";" + WHITESPACE@36..39 "\n " + PLPGSQL_CURSOR_DECL@39..64 + PLPGSQL_VAR_NAME@39..40 + IDENT@39..40 "b" + WHITESPACE@40..41 " " + NO_SCROLL@41..43 + NO_KW@41..43 "no" + WHITESPACE@43..44 " " + CURSOR_KW@44..50 "cursor" + WHITESPACE@50..51 " " + FOR_KW@51..54 "for" + WHITESPACE@54..55 " " + SELECT@55..63 + SELECT_CLAUSE@55..63 + SELECT_KW@55..61 "select" + WHITESPACE@61..62 " " + TARGET_LIST@62..63 + TARGET@62..63 + LITERAL@62..63 + INT_NUMBER@62..63 "1" + SEMICOLON@63..64 ";" + WHITESPACE@64..67 "\n " + PLPGSQL_CURSOR_DECL@67..85 + PLPGSQL_VAR_NAME@67..68 + IDENT@67..68 "c" + WHITESPACE@68..69 " " + CURSOR_KW@69..75 "cursor" + WHITESPACE@75..76 " " + SELECT@76..84 + SELECT_CLAUSE@76..84 + SELECT_KW@76..82 "select" + WHITESPACE@82..83 " " + TARGET_LIST@83..84 + TARGET@83..84 + LITERAL@83..84 + INT_NUMBER@83..84 "1" + SEMICOLON@84..85 ";" + WHITESPACE@85..88 "\n " + PLPGSQL_VAR_DECL@88..94 + PLPGSQL_VAR_NAME@88..89 + IDENT@88..89 "d" + WHITESPACE@89..90 " " + PATH_TYPE@90..93 + PATH_REF@90..93 + PATH_SEGMENT_REF@90..93 + INT_KW@90..93 "int" + SEMICOLON@93..94 ";" + WHITESPACE@94..95 "\n" + BEGIN_KW@95..100 "begin" + WHITESPACE@100..103 "\n " + PLPGSQL_BODY@103..108 + PLPGSQL_NULL_STMT@103..108 + NULL_KW@103..107 "null" + SEMICOLON@107..108 ";" + WHITESPACE@108..109 "\n" + END_KW@109..112 "end" + WHITESPACE@112..113 "\n" + +error[syntax-error]: expected at least one cursor argument + ╭▸ +3 │ a cursor () for select 1; + ╰╴ ━ +error[syntax-error]: expected SCROLL_KW + ╭▸ +4 │ b no cursor for select 1; + ╰╴ ━ +error[syntax-error]: expected FOR_KW + ╭▸ +5 │ c cursor select 1; + ╰╴ ━ diff --git a/crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_cursor_decl_ok.snap b/crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_cursor_decl_ok.snap new file mode 100644 index 00000000..594bdd19 --- /dev/null +++ b/crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_cursor_decl_ok.snap @@ -0,0 +1,216 @@ +--- +source: crates/squawk_syntax/src/test.rs +input_file: crates/squawk_parser/tests/data/plpgsql/ok/cursor_decl.sql +--- +PLPGSQL@0..305 + WHITESPACE@0..1 "\n" + PLPGSQL_BLOCK@1..304 + PLPGSQL_DECLARE_SECTION@1..286 + DECLARE_KW@1..8 "declare" + WHITESPACE@8..11 "\n " + PLPGSQL_CURSOR_DECL@11..33 + PLPGSQL_VAR_NAME@11..12 + IDENT@11..12 "a" + WHITESPACE@12..13 " " + CURSOR_KW@13..19 "cursor" + WHITESPACE@19..20 " " + FOR_KW@20..23 "for" + WHITESPACE@23..24 " " + SELECT@24..32 + SELECT_CLAUSE@24..32 + SELECT_KW@24..30 "select" + WHITESPACE@30..31 " " + TARGET_LIST@31..32 + TARGET@31..32 + LITERAL@31..32 + INT_NUMBER@31..32 "1" + SEMICOLON@32..33 ";" + WHITESPACE@33..36 "\n " + PLPGSQL_CURSOR_DECL@36..80 + PLPGSQL_VAR_NAME@36..37 + IDENT@36..37 "b" + WHITESPACE@37..38 " " + SCROLL@38..44 + SCROLL_KW@38..44 "scroll" + WHITESPACE@44..45 " " + CURSOR_KW@45..51 "cursor" + WHITESPACE@51..52 " " + FOR_KW@52..55 "for" + WHITESPACE@55..56 " " + SELECT@56..79 + SELECT_CLAUSE@56..65 + SELECT_KW@56..62 "select" + WHITESPACE@62..63 " " + TARGET_LIST@63..65 + TARGET@63..65 + NAME_REF@63..65 + IDENT@63..65 "f1" + WHITESPACE@65..66 " " + FROM_CLAUSE@66..79 + FROM_KW@66..70 "from" + WHITESPACE@70..71 " " + RELATION_FROM_ITEM@71..79 + RELATION_NAME_REF@71..79 + PATH_REF@71..79 + PATH_SEGMENT_REF@71..79 + IDENT@71..79 "int4_tbl" + SEMICOLON@79..80 ";" + WHITESPACE@80..83 "\n " + PLPGSQL_CURSOR_DECL@83..130 + PLPGSQL_VAR_NAME@83..84 + IDENT@83..84 "c" + WHITESPACE@84..85 " " + NO_SCROLL@85..94 + NO_KW@85..87 "no" + WHITESPACE@87..88 " " + SCROLL_KW@88..94 "scroll" + WHITESPACE@94..95 " " + CURSOR_KW@95..101 "cursor" + WHITESPACE@101..102 " " + FOR_KW@102..105 "for" + WHITESPACE@105..106 " " + SELECT@106..129 + SELECT_CLAUSE@106..115 + SELECT_KW@106..112 "select" + WHITESPACE@112..113 " " + TARGET_LIST@113..115 + TARGET@113..115 + NAME_REF@113..115 + IDENT@113..115 "f1" + WHITESPACE@115..116 " " + FROM_CLAUSE@116..129 + FROM_KW@116..120 "from" + WHITESPACE@120..121 " " + RELATION_FROM_ITEM@121..129 + RELATION_NAME_REF@121..129 + PATH_REF@121..129 + PATH_SEGMENT_REF@121..129 + IDENT@121..129 "int4_tbl" + SEMICOLON@129..130 ";" + WHITESPACE@130..133 "\n " + PLPGSQL_CURSOR_DECL@133..211 + PLPGSQL_VAR_NAME@133..134 + IDENT@133..134 "d" + WHITESPACE@134..135 " " + CURSOR_KW@135..141 "cursor" + WHITESPACE@141..142 " " + PLPGSQL_CURSOR_ARG_LIST@142..166 + L_PAREN@142..143 "(" + PLPGSQL_CURSOR_ARG@143..153 + PLPGSQL_VAR_NAME@143..145 + IDENT@143..145 "r1" + WHITESPACE@145..146 " " + PATH_TYPE@146..153 + PATH_REF@146..153 + PATH_SEGMENT_REF@146..153 + INTEGER_KW@146..153 "integer" + COMMA@153..154 "," + WHITESPACE@154..155 " " + PLPGSQL_CURSOR_ARG@155..165 + PLPGSQL_VAR_NAME@155..157 + IDENT@155..157 "r2" + WHITESPACE@157..158 " " + PATH_TYPE@158..165 + PATH_REF@158..165 + PATH_SEGMENT_REF@158..165 + INTEGER_KW@158..165 "integer" + R_PAREN@165..166 ")" + WHITESPACE@166..167 " " + FOR_KW@167..170 "for" + WHITESPACE@170..171 " " + SELECT@171..210 + SELECT_CLAUSE@171..179 + SELECT_KW@171..177 "select" + WHITESPACE@177..178 " " + TARGET_LIST@178..179 + TARGET@178..179 + STAR@178..179 "*" + WHITESPACE@179..180 " " + FROM_CLAUSE@180..210 + FROM_KW@180..184 "from" + WHITESPACE@184..185 " " + FUNCTION_FROM_ITEM@185..210 + CALL_EXPR@185..208 + NAME_REF@185..200 + IDENT@185..200 "generate_series" + ARG_LIST@200..208 + L_PAREN@200..201 "(" + ARG@201..203 + NAME_REF@201..203 + IDENT@201..203 "r1" + COMMA@203..204 "," + WHITESPACE@204..205 " " + ARG@205..207 + NAME_REF@205..207 + IDENT@205..207 "r2" + R_PAREN@207..208 ")" + WHITESPACE@208..209 " " + FROM_ALIAS@209..210 + TABLE_ALIAS@209..210 + IDENT@209..210 "i" + SEMICOLON@210..211 ";" + WHITESPACE@211..214 "\n " + PLPGSQL_CURSOR_DECL@214..257 + PLPGSQL_VAR_NAME@214..215 + IDENT@214..215 "e" + WHITESPACE@215..216 " " + CURSOR_KW@216..222 "cursor" + WHITESPACE@222..223 " " + PLPGSQL_CURSOR_ARG_LIST@223..244 + L_PAREN@223..224 "(" + PLPGSQL_CURSOR_ARG@224..243 + PLPGSQL_VAR_NAME@224..226 + IDENT@224..226 "p1" + WHITESPACE@226..227 " " + PERCENT_TYPE@227..243 + PATH_REF@227..238 + PATH_REF@227..235 + PATH_SEGMENT_REF@227..235 + IDENT@227..235 "int4_tbl" + DOT@235..236 "." + PATH_SEGMENT_REF@236..238 + IDENT@236..238 "f1" + PERCENT_TYPE_CLAUSE@238..243 + PERCENT@238..239 "%" + TYPE_KW@239..243 "type" + R_PAREN@243..244 ")" + WHITESPACE@244..245 " " + IS_KW@245..247 "is" + WHITESPACE@247..248 " " + SELECT@248..256 + SELECT_CLAUSE@248..256 + SELECT_KW@248..254 "select" + WHITESPACE@254..255 " " + TARGET_LIST@255..256 + TARGET@255..256 + LITERAL@255..256 + INT_NUMBER@255..256 "1" + SEMICOLON@256..257 ";" + WHITESPACE@257..260 "\n " + PLPGSQL_CURSOR_DECL@260..286 + PLPGSQL_VAR_NAME@260..266 + CURSOR_KW@260..266 "cursor" + WHITESPACE@266..267 " " + CURSOR_KW@267..273 "cursor" + WHITESPACE@273..274 " " + FOR_KW@274..277 "for" + WHITESPACE@277..278 " " + TABLE@278..285 + TABLE_KW@278..283 "table" + WHITESPACE@283..284 " " + RELATION_NAME@284..285 + RELATION_NAME_REF@284..285 + PATH_REF@284..285 + PATH_SEGMENT_REF@284..285 + IDENT@284..285 "t" + SEMICOLON@285..286 ";" + WHITESPACE@286..287 "\n" + BEGIN_KW@287..292 "begin" + WHITESPACE@292..295 "\n " + PLPGSQL_BODY@295..300 + PLPGSQL_NULL_STMT@295..300 + NULL_KW@295..299 "null" + SEMICOLON@299..300 ";" + WHITESPACE@300..301 "\n" + END_KW@301..304 "end" + WHITESPACE@304..305 "\n" diff --git a/crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_if_stmt_err.snap b/crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_if_stmt_err.snap new file mode 100644 index 00000000..9e0646d5 --- /dev/null +++ b/crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_if_stmt_err.snap @@ -0,0 +1,174 @@ +--- +source: crates/squawk_syntax/src/test.rs +input_file: crates/squawk_parser/tests/data/plpgsql/err/if_stmt.sql +--- +PLPGSQL@0..315 + WHITESPACE@0..1 "\n" + PLPGSQL_BLOCK@1..314 + BEGIN_KW@1..6 "begin" + WHITESPACE@6..9 "\n " + PLPGSQL_BODY@9..310 + PLPGSQL_IF_STMT@9..37 + IF_KW@9..11 "if" + WHITESPACE@11..12 " " + BIN_EXPR@12..17 + LITERAL@12..13 + INT_NUMBER@12..13 "1" + WHITESPACE@13..14 " " + EQ@14..15 "=" + WHITESPACE@15..16 " " + LITERAL@16..17 + INT_NUMBER@16..17 "1" + WHITESPACE@17..22 "\n " + PLPGSQL_BODY@22..27 + PLPGSQL_NULL_STMT@22..27 + NULL_KW@22..26 "null" + SEMICOLON@26..27 ";" + WHITESPACE@27..30 "\n " + END_KW@30..33 "end" + WHITESPACE@33..34 " " + IF_KW@34..36 "if" + SEMICOLON@36..37 ";" + WHITESPACE@37..41 "\n\n " + PLPGSQL_IF_STMT@41..120 + IF_KW@41..43 "if" + WHITESPACE@43..44 " " + BIN_EXPR@44..49 + LITERAL@44..45 + INT_NUMBER@44..45 "1" + WHITESPACE@45..46 " " + EQ@46..47 "=" + WHITESPACE@47..48 " " + LITERAL@48..49 + INT_NUMBER@48..49 "1" + WHITESPACE@49..50 " " + THEN_KW@50..54 "then" + WHITESPACE@54..59 "\n " + PLPGSQL_BODY@59..64 + PLPGSQL_NULL_STMT@59..64 + NULL_KW@59..63 "null" + SEMICOLON@63..64 ";" + WHITESPACE@64..67 "\n " + PLPGSQL_ELSE_CLAUSE@67..110 + ELSE_KW@67..71 "else" + WHITESPACE@71..76 "\n " + PLPGSQL_BODY@76..110 + PLPGSQL_NULL_STMT@76..81 + NULL_KW@76..80 "null" + SEMICOLON@80..81 ";" + WHITESPACE@81..84 "\n " + ERROR@84..110 + IDENT@84..89 "elsif" + WHITESPACE@89..90 " " + INT_NUMBER@90..91 "1" + WHITESPACE@91..92 " " + EQ@92..93 "=" + WHITESPACE@93..94 " " + INT_NUMBER@94..95 "2" + WHITESPACE@95..96 " " + THEN_KW@96..100 "then" + WHITESPACE@100..105 "\n " + NULL_KW@105..109 "null" + SEMICOLON@109..110 ";" + WHITESPACE@110..113 "\n " + END_KW@113..116 "end" + WHITESPACE@116..117 " " + IF_KW@117..119 "if" + SEMICOLON@119..120 ";" + WHITESPACE@120..124 "\n\n " + COMMENT@124..188 "-- `expr_until_then` ..." + WHITESPACE@188..191 "\n " + PLPGSQL_IF_STMT@191..301 + IF_KW@191..193 "if" + WHITESPACE@193..194 " " + BIN_EXPR@194..223 + CASE_EXPR@194..219 + CASE_KW@194..198 "case" + WHITESPACE@198..199 " " + WHEN_CLAUSE_LIST@199..215 + WHEN_CLAUSE@199..215 + WHEN_KW@199..203 "when" + WHITESPACE@203..204 " " + LITERAL@204..208 + TRUE_KW@204..208 "true" + WHITESPACE@208..209 " " + THEN_KW@209..213 "then" + WHITESPACE@213..214 " " + LITERAL@214..215 + INT_NUMBER@214..215 "1" + WHITESPACE@215..216 " " + END_KW@216..219 "end" + WHITESPACE@219..220 " " + EQ@220..221 "=" + WHITESPACE@221..222 " " + LITERAL@222..223 + INT_NUMBER@222..223 "1" + WHITESPACE@223..224 " " + THEN_KW@224..228 "then" + WHITESPACE@228..233 "\n " + PLPGSQL_BODY@233..238 + PLPGSQL_NULL_STMT@233..238 + NULL_KW@233..237 "null" + SEMICOLON@237..238 ";" + WHITESPACE@238..241 "\n " + PLPGSQL_ELSIF_CLAUSE@241..291 + ELSIF_KW@241..246 "elsif" + WHITESPACE@246..247 " " + BIN_EXPR@247..276 + LITERAL@247..248 + INT_NUMBER@247..248 "1" + WHITESPACE@248..249 " " + EQ@249..250 "=" + WHITESPACE@250..251 " " + CASE_EXPR@251..276 + CASE_KW@251..255 "case" + WHITESPACE@255..256 " " + WHEN_CLAUSE_LIST@256..272 + WHEN_CLAUSE@256..272 + WHEN_KW@256..260 "when" + WHITESPACE@260..261 " " + LITERAL@261..265 + TRUE_KW@261..265 "true" + WHITESPACE@265..266 " " + THEN_KW@266..270 "then" + WHITESPACE@270..271 " " + LITERAL@271..272 + INT_NUMBER@271..272 "1" + WHITESPACE@272..273 " " + END_KW@273..276 "end" + WHITESPACE@276..277 " " + THEN_KW@277..281 "then" + WHITESPACE@281..286 "\n " + PLPGSQL_BODY@286..291 + PLPGSQL_NULL_STMT@286..291 + NULL_KW@286..290 "null" + SEMICOLON@290..291 ";" + WHITESPACE@291..294 "\n " + END_KW@294..297 "end" + WHITESPACE@297..298 " " + IF_KW@298..300 "if" + SEMICOLON@300..301 ";" + WHITESPACE@301..305 "\n\n " + PLPGSQL_NULL_STMT@305..310 + NULL_KW@305..309 "null" + SEMICOLON@309..310 ";" + WHITESPACE@310..311 "\n" + END_KW@311..314 "end" + WHITESPACE@314..315 "\n" + +error[syntax-error]: expected THEN_KW + ╭▸ +3 │ if 1 = 1 + ╰╴ ━ +error[syntax-error]: expected a statement, found IDENT + ╭▸ +11 │ elsif 1 = 2 then + ╰╴ ━ +error[syntax-error]: CASE expression must be parenthesized + ╭▸ +16 │ if case when true then 1 end = 1 then + ╰╴ ━━━━━━━━━━━━━━━━━━━━━━━━━ +error[syntax-error]: CASE expression must be parenthesized + ╭▸ +18 │ elsif 1 = case when true then 1 end then + ╰╴ ━━━━━━━━━━━━━━━━━━━━━━━━━ diff --git a/crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_if_stmt_ok.snap b/crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_if_stmt_ok.snap new file mode 100644 index 00000000..71c0b080 --- /dev/null +++ b/crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_if_stmt_ok.snap @@ -0,0 +1,351 @@ +--- +source: crates/squawk_syntax/src/test.rs +input_file: crates/squawk_parser/tests/data/plpgsql/ok/if_stmt.sql +--- +PLPGSQL@0..514 + WHITESPACE@0..1 "\n" + PLPGSQL_BLOCK@1..513 + PLPGSQL_DECLARE_SECTION@1..22 + DECLARE_KW@1..8 "declare" + WHITESPACE@8..11 "\n " + PLPGSQL_VAR_DECL@11..22 + PLPGSQL_VAR_NAME@11..12 + IDENT@11..12 "x" + WHITESPACE@12..13 " " + PATH_TYPE@13..16 + PATH_REF@13..16 + PATH_SEGMENT_REF@13..16 + INT_KW@13..16 "int" + WHITESPACE@16..17 " " + PLPGSQL_VAR_INIT@17..21 + COLON_EQ@17..19 ":=" + WHITESPACE@19..20 " " + LITERAL@20..21 + INT_NUMBER@20..21 "1" + SEMICOLON@21..22 ";" + WHITESPACE@22..23 "\n" + BEGIN_KW@23..28 "begin" + WHITESPACE@28..31 "\n " + PLPGSQL_BODY@31..509 + PLPGSQL_IF_STMT@31..64 + IF_KW@31..33 "if" + WHITESPACE@33..34 " " + BIN_EXPR@34..39 + NAME_REF@34..35 + IDENT@34..35 "x" + WHITESPACE@35..36 " " + R_ANGLE@36..37 ">" + WHITESPACE@37..38 " " + LITERAL@38..39 + INT_NUMBER@38..39 "0" + WHITESPACE@39..40 " " + THEN_KW@40..44 "then" + WHITESPACE@44..49 "\n " + PLPGSQL_BODY@49..54 + PLPGSQL_NULL_STMT@49..54 + NULL_KW@49..53 "null" + SEMICOLON@53..54 ";" + WHITESPACE@54..57 "\n " + END_KW@57..60 "end" + WHITESPACE@60..61 " " + IF_KW@61..63 "if" + SEMICOLON@63..64 ";" + WHITESPACE@64..68 "\n\n " + PLPGSQL_IF_STMT@68..118 + IF_KW@68..70 "if" + WHITESPACE@70..71 " " + BIN_EXPR@71..76 + NAME_REF@71..72 + IDENT@71..72 "x" + WHITESPACE@72..73 " " + EQ@73..74 "=" + WHITESPACE@74..75 " " + LITERAL@75..76 + INT_NUMBER@75..76 "1" + WHITESPACE@76..77 " " + THEN_KW@77..81 "then" + WHITESPACE@81..86 "\n " + PLPGSQL_BODY@86..91 + PLPGSQL_NULL_STMT@86..91 + NULL_KW@86..90 "null" + SEMICOLON@90..91 ";" + WHITESPACE@91..94 "\n " + PLPGSQL_ELSE_CLAUSE@94..108 + ELSE_KW@94..98 "else" + WHITESPACE@98..103 "\n " + PLPGSQL_BODY@103..108 + PLPGSQL_NULL_STMT@103..108 + NULL_KW@103..107 "null" + SEMICOLON@107..108 ";" + WHITESPACE@108..111 "\n " + END_KW@111..114 "end" + WHITESPACE@114..115 " " + IF_KW@115..117 "if" + SEMICOLON@117..118 ";" + WHITESPACE@118..122 "\n\n " + PLPGSQL_IF_STMT@122..329 + IF_KW@122..124 "if" + WHITESPACE@124..125 " " + BIN_EXPR@125..130 + NAME_REF@125..126 + IDENT@125..126 "x" + WHITESPACE@126..127 " " + EQ@127..128 "=" + WHITESPACE@128..129 " " + LITERAL@129..130 + INT_NUMBER@129..130 "1" + WHITESPACE@130..131 " " + THEN_KW@131..135 "then" + WHITESPACE@135..140 "\n " + PLPGSQL_BODY@140..177 + PLPGSQL_IF_STMT@140..177 + IF_KW@140..142 "if" + WHITESPACE@142..143 " " + BIN_EXPR@143..148 + NAME_REF@143..144 + IDENT@143..144 "x" + WHITESPACE@144..145 " " + EQ@145..146 "=" + WHITESPACE@146..147 " " + LITERAL@147..148 + INT_NUMBER@147..148 "2" + WHITESPACE@148..149 " " + THEN_KW@149..153 "then" + WHITESPACE@153..160 "\n " + PLPGSQL_BODY@160..165 + PLPGSQL_NULL_STMT@160..165 + NULL_KW@160..164 "null" + SEMICOLON@164..165 ";" + WHITESPACE@165..170 "\n " + END_KW@170..173 "end" + WHITESPACE@173..174 " " + IF_KW@174..176 "if" + SEMICOLON@176..177 ";" + WHITESPACE@177..180 "\n " + PLPGSQL_ELSIF_CLAUSE@180..206 + ELSIF_KW@180..185 "elsif" + WHITESPACE@185..186 " " + BIN_EXPR@186..191 + NAME_REF@186..187 + IDENT@186..187 "x" + WHITESPACE@187..188 " " + EQ@188..189 "=" + WHITESPACE@189..190 " " + LITERAL@190..191 + INT_NUMBER@190..191 "2" + WHITESPACE@191..192 " " + THEN_KW@192..196 "then" + WHITESPACE@196..201 "\n " + PLPGSQL_BODY@201..206 + PLPGSQL_NULL_STMT@201..206 + NULL_KW@201..205 "null" + SEMICOLON@205..206 ";" + WHITESPACE@206..209 "\n " + PLPGSQL_ELSIF_CLAUSE@209..236 + ELSEIF_KW@209..215 "elseif" + WHITESPACE@215..216 " " + BIN_EXPR@216..221 + NAME_REF@216..217 + IDENT@216..217 "x" + WHITESPACE@217..218 " " + EQ@218..219 "=" + WHITESPACE@219..220 " " + LITERAL@220..221 + INT_NUMBER@220..221 "3" + WHITESPACE@221..222 " " + THEN_KW@222..226 "then" + WHITESPACE@226..231 "\n " + PLPGSQL_BODY@231..236 + PLPGSQL_NULL_STMT@231..236 + NULL_KW@231..235 "null" + SEMICOLON@235..236 ";" + WHITESPACE@236..239 "\n " + PLPGSQL_ELSIF_CLAUSE@239..302 + ELSIF_KW@239..244 "elsif" + WHITESPACE@244..245 " " + PAREN_EXPR@245..287 + L_PAREN@245..246 "(" + CASE_EXPR@246..286 + CASE_KW@246..250 "case" + WHITESPACE@250..251 " " + WHEN_CLAUSE_LIST@251..271 + WHEN_CLAUSE@251..271 + WHEN_KW@251..255 "when" + WHITESPACE@255..256 " " + BIN_EXPR@256..261 + NAME_REF@256..257 + IDENT@256..257 "x" + WHITESPACE@257..258 " " + EQ@258..259 "=" + WHITESPACE@259..260 " " + LITERAL@260..261 + INT_NUMBER@260..261 "4" + WHITESPACE@261..262 " " + THEN_KW@262..266 "then" + WHITESPACE@266..267 " " + LITERAL@267..271 + TRUE_KW@267..271 "true" + WHITESPACE@271..272 " " + ELSE_CLAUSE@272..282 + ELSE_KW@272..276 "else" + WHITESPACE@276..277 " " + LITERAL@277..282 + FALSE_KW@277..282 "false" + WHITESPACE@282..283 " " + END_KW@283..286 "end" + R_PAREN@286..287 ")" + WHITESPACE@287..288 " " + THEN_KW@288..292 "then" + WHITESPACE@292..297 "\n " + PLPGSQL_BODY@297..302 + PLPGSQL_NULL_STMT@297..302 + NULL_KW@297..301 "null" + SEMICOLON@301..302 ";" + WHITESPACE@302..305 "\n " + PLPGSQL_ELSE_CLAUSE@305..319 + ELSE_KW@305..309 "else" + WHITESPACE@309..314 "\n " + PLPGSQL_BODY@314..319 + PLPGSQL_NULL_STMT@314..319 + NULL_KW@314..318 "null" + SEMICOLON@318..319 ";" + WHITESPACE@319..322 "\n " + END_KW@322..325 "end" + WHITESPACE@325..326 " " + IF_KW@326..328 "if" + SEMICOLON@328..329 ";" + WHITESPACE@329..333 "\n\n " + PLPGSQL_IF_STMT@333..360 + IF_KW@333..335 "if" + WHITESPACE@335..336 " " + BIN_EXPR@336..345 + NAME_REF@336..337 + IDENT@336..337 "x" + WHITESPACE@337..338 " " + IS_KW@338..340 "is" + WHITESPACE@340..341 " " + LITERAL@341..345 + NULL_KW@341..345 "null" + WHITESPACE@345..346 " " + THEN_KW@346..350 "then" + WHITESPACE@350..353 "\n " + PLPGSQL_BODY@353..353 + END_KW@353..356 "end" + WHITESPACE@356..357 " " + IF_KW@357..359 "if" + SEMICOLON@359..360 ";" + WHITESPACE@360..364 "\n\n " + PLPGSQL_IF_STMT@364..435 + IF_KW@364..366 "if" + WHITESPACE@366..367 " " + BIN_EXPR@367..410 + CALL_EXPR@367..406 + NAME_REF@367..375 + COALESCE_KW@367..375 "coalesce" + ARG_LIST@375..406 + L_PAREN@375..376 "(" + ARG@376..402 + CASE_EXPR@376..402 + CASE_KW@376..380 "case" + WHITESPACE@380..381 " " + WHEN_CLAUSE_LIST@381..398 + WHEN_CLAUSE@381..398 + WHEN_KW@381..385 "when" + WHITESPACE@385..386 " " + BIN_EXPR@386..391 + NAME_REF@386..387 + IDENT@386..387 "x" + WHITESPACE@387..388 " " + EQ@388..389 "=" + WHITESPACE@389..390 " " + LITERAL@390..391 + INT_NUMBER@390..391 "1" + WHITESPACE@391..392 " " + THEN_KW@392..396 "then" + WHITESPACE@396..397 " " + LITERAL@397..398 + INT_NUMBER@397..398 "1" + WHITESPACE@398..399 " " + END_KW@399..402 "end" + COMMA@402..403 "," + WHITESPACE@403..404 " " + ARG@404..405 + LITERAL@404..405 + INT_NUMBER@404..405 "0" + R_PAREN@405..406 ")" + WHITESPACE@406..407 " " + EQ@407..408 "=" + WHITESPACE@408..409 " " + LITERAL@409..410 + INT_NUMBER@409..410 "1" + WHITESPACE@410..411 " " + THEN_KW@411..415 "then" + WHITESPACE@415..420 "\n " + PLPGSQL_BODY@420..425 + PLPGSQL_NULL_STMT@420..425 + NULL_KW@420..424 "null" + SEMICOLON@424..425 ";" + WHITESPACE@425..428 "\n " + END_KW@428..431 "end" + WHITESPACE@431..432 " " + IF_KW@432..434 "if" + SEMICOLON@434..435 ";" + WHITESPACE@435..439 "\n\n " + PLPGSQL_IF_STMT@439..509 + IF_KW@439..441 "if" + WHITESPACE@441..442 " " + BIN_EXPR@442..484 + INDEX_EXPR@442..480 + PAREN_EXPR@442..477 + L_PAREN@442..443 "(" + ARRAY_EXPR@443..476 + ARRAY_KW@443..448 "array" + L_BRACK@448..449 "[" + CASE_EXPR@449..475 + CASE_KW@449..453 "case" + WHITESPACE@453..454 " " + WHEN_CLAUSE_LIST@454..471 + WHEN_CLAUSE@454..471 + WHEN_KW@454..458 "when" + WHITESPACE@458..459 " " + BIN_EXPR@459..464 + NAME_REF@459..460 + IDENT@459..460 "x" + WHITESPACE@460..461 " " + EQ@461..462 "=" + WHITESPACE@462..463 " " + LITERAL@463..464 + INT_NUMBER@463..464 "1" + WHITESPACE@464..465 " " + THEN_KW@465..469 "then" + WHITESPACE@469..470 " " + LITERAL@470..471 + INT_NUMBER@470..471 "1" + WHITESPACE@471..472 " " + END_KW@472..475 "end" + R_BRACK@475..476 "]" + R_PAREN@476..477 ")" + L_BRACK@477..478 "[" + LITERAL@478..479 + INT_NUMBER@478..479 "1" + R_BRACK@479..480 "]" + WHITESPACE@480..481 " " + EQ@481..482 "=" + WHITESPACE@482..483 " " + LITERAL@483..484 + INT_NUMBER@483..484 "1" + WHITESPACE@484..485 " " + THEN_KW@485..489 "then" + WHITESPACE@489..494 "\n " + PLPGSQL_BODY@494..499 + PLPGSQL_NULL_STMT@494..499 + NULL_KW@494..498 "null" + SEMICOLON@498..499 ";" + WHITESPACE@499..502 "\n " + END_KW@502..505 "end" + WHITESPACE@505..506 " " + IF_KW@506..508 "if" + SEMICOLON@508..509 ";" + WHITESPACE@509..510 "\n" + END_KW@510..513 "end" + WHITESPACE@513..514 "\n" diff --git a/crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_percent_type_ok.snap b/crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_percent_type_ok.snap new file mode 100644 index 00000000..8e15c43b --- /dev/null +++ b/crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_percent_type_ok.snap @@ -0,0 +1,217 @@ +--- +source: crates/squawk_syntax/src/test.rs +input_file: crates/squawk_parser/tests/data/plpgsql/ok/percent_type.sql +--- +PLPGSQL@0..265 + WHITESPACE@0..1 "\n" + PLPGSQL_BLOCK@1..264 + PLPGSQL_DECLARE_SECTION@1..246 + DECLARE_KW@1..8 "declare" + WHITESPACE@8..11 "\n " + PLPGSQL_VAR_DECL@11..17 + PLPGSQL_VAR_NAME@11..12 + IDENT@11..12 "v" + WHITESPACE@12..13 " " + PATH_TYPE@13..16 + PATH_REF@13..16 + PATH_SEGMENT_REF@13..16 + INT_KW@13..16 "int" + SEMICOLON@16..17 ";" + WHITESPACE@17..20 "\n " + PLPGSQL_VAR_DECL@20..29 + PLPGSQL_VAR_NAME@20..21 + IDENT@20..21 "a" + WHITESPACE@21..22 " " + PERCENT_TYPE@22..28 + PATH_REF@22..23 + PATH_SEGMENT_REF@22..23 + IDENT@22..23 "v" + PERCENT_TYPE_CLAUSE@23..28 + PERCENT@23..24 "%" + TYPE_KW@24..28 "type" + SEMICOLON@28..29 ";" + WHITESPACE@29..32 "\n " + PLPGSQL_VAR_DECL@32..43 + PLPGSQL_VAR_NAME@32..33 + IDENT@32..33 "b" + WHITESPACE@33..34 " " + PERCENT_TYPE@34..42 + PATH_REF@34..35 + PATH_SEGMENT_REF@34..35 + IDENT@34..35 "v" + PERCENT_TYPE_CLAUSE@35..40 + PERCENT@35..36 "%" + TYPE_KW@36..40 "type" + ARRAY_BOUND@40..42 + L_BRACK@40..41 "[" + R_BRACK@41..42 "]" + SEMICOLON@42..43 ";" + WHITESPACE@43..46 "\n " + PLPGSQL_VAR_DECL@46..61 + PLPGSQL_VAR_NAME@46..47 + IDENT@46..47 "c" + WHITESPACE@47..48 " " + PERCENT_TYPE@48..60 + PATH_REF@48..49 + PATH_SEGMENT_REF@48..49 + IDENT@48..49 "v" + PERCENT_TYPE_CLAUSE@49..54 + PERCENT@49..50 "%" + TYPE_KW@50..54 "type" + ARRAY_BOUND@54..57 + L_BRACK@54..55 "[" + LITERAL@55..56 + INT_NUMBER@55..56 "1" + R_BRACK@56..57 "]" + ARRAY_BOUND@57..60 + L_BRACK@57..58 "[" + LITERAL@58..59 + INT_NUMBER@58..59 "3" + R_BRACK@59..60 "]" + SEMICOLON@60..61 ";" + WHITESPACE@61..64 "\n " + PLPGSQL_VAR_DECL@64..79 + PLPGSQL_VAR_NAME@64..65 + IDENT@64..65 "d" + WHITESPACE@65..66 " " + PERCENT_TYPE@66..78 + PATH_REF@66..67 + PATH_SEGMENT_REF@66..67 + IDENT@66..67 "v" + PERCENT_TYPE_CLAUSE@67..72 + PERCENT@67..68 "%" + TYPE_KW@68..72 "type" + WHITESPACE@72..73 " " + ARRAY_KW@73..78 "array" + SEMICOLON@78..79 ";" + WHITESPACE@79..82 "\n " + PLPGSQL_VAR_DECL@82..100 + PLPGSQL_VAR_NAME@82..83 + IDENT@82..83 "e" + WHITESPACE@83..84 " " + PERCENT_TYPE@84..99 + PATH_REF@84..85 + PATH_SEGMENT_REF@84..85 + IDENT@84..85 "v" + PERCENT_TYPE_CLAUSE@85..90 + PERCENT@85..86 "%" + TYPE_KW@86..90 "type" + WHITESPACE@90..91 " " + ARRAY_KW@91..96 "array" + ARRAY_BOUND@96..99 + L_BRACK@96..97 "[" + LITERAL@97..98 + INT_NUMBER@97..98 "1" + R_BRACK@98..99 "]" + SEMICOLON@99..100 ";" + WHITESPACE@100..103 "\n " + PLPGSQL_VAR_DECL@103..117 + PLPGSQL_VAR_NAME@103..104 + IDENT@103..104 "f" + WHITESPACE@104..105 " " + PLPGSQL_PERCENT_ROWTYPE@105..116 + PATH_REF@105..108 + PATH_SEGMENT_REF@105..108 + IDENT@105..108 "foo" + PERCENT@108..109 "%" + ROWTYPE_KW@109..116 "rowtype" + SEMICOLON@116..117 ";" + WHITESPACE@117..120 "\n " + PLPGSQL_VAR_DECL@120..137 + PLPGSQL_VAR_NAME@120..121 + IDENT@120..121 "g" + WHITESPACE@121..122 " " + PLPGSQL_PERCENT_ROWTYPE@122..136 + PATH_REF@122..128 + PATH_SEGMENT_REF@122..128 + IDENT@122..128 "notice" + PERCENT@128..129 "%" + ROWTYPE_KW@129..136 "rowtype" + SEMICOLON@136..137 ";" + WHITESPACE@137..140 "\n " + PLPGSQL_VAR_DECL@140..162 + PLPGSQL_VAR_NAME@140..141 + IDENT@140..141 "h" + WHITESPACE@141..142 " " + PLPGSQL_PERCENT_ROWTYPE@142..161 + PATH_REF@142..153 + PATH_REF@142..149 + PATH_REF@142..145 + PATH_SEGMENT_REF@142..145 + IDENT@142..145 "foo" + DOT@145..146 "." + PATH_SEGMENT_REF@146..149 + IDENT@146..149 "bar" + DOT@149..150 "." + PATH_SEGMENT_REF@150..153 + IDENT@150..153 "baz" + PERCENT@153..154 "%" + ROWTYPE_KW@154..161 "rowtype" + SEMICOLON@161..162 ";" + WHITESPACE@162..165 "\n " + PLPGSQL_VAR_DECL@165..197 + PLPGSQL_VAR_NAME@165..166 + IDENT@165..166 "i" + WHITESPACE@166..167 " " + PLPGSQL_PERCENT_ROWTYPE@167..196 + PATH_REF@167..186 + PATH_REF@167..177 + PATH_SEGMENT_REF@167..177 + IDENT@167..177 "pg_catalog" + DOT@177..178 "." + PATH_SEGMENT_REF@178..186 + IDENT@178..186 "pg_class" + PERCENT@186..187 "%" + ROWTYPE_KW@187..194 "rowtype" + ARRAY_BOUND@194..196 + L_BRACK@194..195 "[" + R_BRACK@195..196 "]" + SEMICOLON@196..197 ";" + WHITESPACE@197..200 "\n " + PLPGSQL_VAR_DECL@200..216 + PLPGSQL_VAR_NAME@200..201 + IDENT@200..201 "j" + WHITESPACE@201..202 " " + PLPGSQL_PERCENT_ROWTYPE@202..215 + PATH_REF@202..207 + PATH_SEGMENT_REF@202..207 + TABLE_KW@202..207 "table" + PERCENT@207..208 "%" + ROWTYPE_KW@208..215 "rowtype" + SEMICOLON@215..216 ";" + WHITESPACE@216..219 "\n " + PLPGSQL_VAR_DECL@219..233 + PLPGSQL_VAR_NAME@219..220 + IDENT@219..220 "k" + WHITESPACE@220..221 " " + PERCENT_TYPE@221..232 + PATH_REF@221..227 + PATH_SEGMENT_REF@221..227 + SELECT_KW@221..227 "select" + PERCENT_TYPE_CLAUSE@227..232 + PERCENT@227..228 "%" + TYPE_KW@228..232 "type" + SEMICOLON@232..233 ";" + WHITESPACE@233..236 "\n " + PLPGSQL_VAR_DECL@236..246 + PLPGSQL_VAR_NAME@236..237 + IDENT@236..237 "l" + WHITESPACE@237..238 " " + PERCENT_TYPE@238..245 + PATH_REF@238..240 + PATH_SEGMENT_REF@238..240 + POSITIONAL_PARAM@238..240 "$1" + PERCENT_TYPE_CLAUSE@240..245 + PERCENT@240..241 "%" + TYPE_KW@241..245 "type" + SEMICOLON@245..246 ";" + WHITESPACE@246..247 "\n" + BEGIN_KW@247..252 "begin" + WHITESPACE@252..255 "\n " + PLPGSQL_BODY@255..260 + PLPGSQL_NULL_STMT@255..260 + NULL_KW@255..259 "null" + SEMICOLON@259..260 ";" + WHITESPACE@260..261 "\n" + END_KW@261..264 "end" + WHITESPACE@264..265 "\n" diff --git a/crates/squawk_syntax/src/test.rs b/crates/squawk_syntax/src/test.rs index ffb01595..2c23b961 100644 --- a/crates/squawk_syntax/src/test.rs +++ b/crates/squawk_syntax/src/test.rs @@ -129,12 +129,12 @@ fn plpgsql_fixture(sql: &str) -> (String, Vec) { let mut buffer = String::new(); let mut errors = vec![]; - for body in &bodies { + for body in bodies { if !buffer.is_empty() { buffer.push_str("---\n"); } buffer.push_str(&format!("{:#?}", body.syntax())); - errors.extend_from_slice(body.errors()); + errors.extend(body.errors()); } if !errors.is_empty() { @@ -254,7 +254,7 @@ fn plpgsql_suite_score() { let bodies = plpgsql_bodies(&parse); let mut counts = [bodies.len(), 0, 0, 0]; - for body in &bodies { + for body in bodies { let (tokens, unparsed) = token_counts(&body.syntax()); counts[1] += tokens; counts[2] += unparsed; @@ -271,21 +271,21 @@ fn plpgsql_suite_score() { assert_snapshot!(table, @" file bodies tokens unparsed err - plpgsql.sql 254 9926 8183 984 - plpgsql_array.sql 26 948 681 102 + plpgsql.sql 254 9887 6445 807 + plpgsql_array.sql 26 948 629 70 plpgsql_cache.sql 2 60 44 5 - plpgsql_call.sql 45 1687 1374 177 - plpgsql_control.sql 27 1400 1212 164 + plpgsql_call.sql 45 1687 1366 176 + plpgsql_control.sql 27 1400 1152 161 plpgsql_copy.sql 4 28 16 4 plpgsql_domain.sql 23 300 179 30 - plpgsql_misc.sql 16 261 132 24 - plpgsql_record.sql 65 1994 1576 187 - plpgsql_simple.sql 9 213 145 21 - plpgsql_transaction.sql 37 1202 999 136 - plpgsql_trap.sql 7 354 255 45 - plpgsql_trigger.sql 1 55 53 6 - plpgsql_varprops.sql 33 716 387 57 - total 549 19144 15236 1942 + plpgsql_misc.sql 16 261 114 12 + plpgsql_record.sql 65 1994 1573 185 + plpgsql_simple.sql 9 213 129 19 + plpgsql_transaction.sql 37 1202 929 130 + plpgsql_trap.sql 7 354 247 44 + plpgsql_trigger.sql 1 55 42 5 + plpgsql_varprops.sql 33 716 379 56 + total 549 19105 13244 1704 "); } diff --git a/crates/squawk_syntax/src/validation.rs b/crates/squawk_syntax/src/validation.rs index f08aafd7..47e1ce64 100644 --- a/crates/squawk_syntax/src/validation.rs +++ b/crates/squawk_syntax/src/validation.rs @@ -49,6 +49,10 @@ pub(crate) fn validate(root: &SyntaxNode, errors: &mut Vec) { ast::NonStandardParam(it) => validate_non_standard_param(it, errors), ast::ParenFromItem(it) => validate_paren_from_item(it, errors), ast::PartitionForValuesWith(it) => validate_hash_partition_bounds(it, errors), + ast::PlpgsqlCaseStmt(it) => validate_no_bare_case(it.subject(), errors), + ast::PlpgsqlCaseWhen(it) => validate_no_bare_case_in_conds(it, errors), + ast::PlpgsqlElsifClause(it) => validate_no_bare_case(it.cond(), errors), + ast::PlpgsqlIfStmt(it) => validate_no_bare_case(it.cond(), errors), ast::RelationFromItem(it) => validate_relation_from_item(it, errors), ast::RuleStmtList(it) => validate_rule_stmt_list(it, errors), ast::Select(it) => validate_select(it, errors), @@ -343,6 +347,37 @@ fn validate_atomic_body(it: ast::AtomicBody, acc: &mut Vec) { } } +fn validate_no_bare_case_in_conds(it: ast::PlpgsqlCaseWhen, acc: &mut Vec) { + for cond in it.conds() { + validate_no_bare_case(Some(cond), acc); + } +} + +// -- err +// if case when a then 1 end then +// -- ok +// if (case when a then 1 end) then +fn validate_no_bare_case(cond: Option, acc: &mut Vec) { + let Some(cond) = cond else { + return; + }; + let mut depth = 0i32; + for element in cond.syntax().descendants_with_tokens() { + match element.kind() { + L_PAREN | L_BRACK => depth += 1, + R_PAREN | R_BRACK => depth -= 1, + CASE_EXPR if depth == 0 => { + acc.push(SyntaxError::new( + "CASE expression must be parenthesized", + element.text_range(), + )); + return; + } + _ => (), + } + } +} + fn validate_rule_stmt_list(it: ast::RuleStmtList, acc: &mut Vec) { let mut stmts = it.rule_stmts().peekable(); while let Some(stmt) = stmts.next() { From 1ca7e552b33870f762f4f366c041d21051dea201 Mon Sep 17 00:00:00 2001 From: Steve Dignam Date: Fri, 18 Sep 2026 00:16:42 -0400 Subject: [PATCH 09/11] continue, exit, loop, while --- .../src/generated/syntax_kind.rs | 5 + crates/squawk_parser/src/plpgsql_grammar.rs | 87 +++++- .../tests/data/plpgsql/err/loop_stmt.sql | 17 ++ .../tests/data/plpgsql/ok/loop_stmt.sql | 37 +++ .../squawk_syntax/src/ast/generated/nodes.rs | 269 ++++++++++++++++++ crates/squawk_syntax/src/postgresql.ungram | 38 +++ ...k_syntax__test__plpgsql_loop_stmt_err.snap | 84 ++++++ ...wk_syntax__test__plpgsql_loop_stmt_ok.snap | 253 ++++++++++++++++ crates/squawk_syntax/src/test.rs | 6 +- 9 files changed, 789 insertions(+), 7 deletions(-) create mode 100644 crates/squawk_parser/tests/data/plpgsql/err/loop_stmt.sql create mode 100644 crates/squawk_parser/tests/data/plpgsql/ok/loop_stmt.sql create mode 100644 crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_loop_stmt_err.snap create mode 100644 crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_loop_stmt_ok.snap diff --git a/crates/squawk_parser/src/generated/syntax_kind.rs b/crates/squawk_parser/src/generated/syntax_kind.rs index 306a5f6e..be8808ec 100644 --- a/crates/squawk_parser/src/generated/syntax_kind.rs +++ b/crates/squawk_parser/src/generated/syntax_kind.rs @@ -1511,6 +1511,7 @@ pub enum SyntaxKind { PLPGSQL_CASE_STMT, PLPGSQL_CASE_WHEN, PLPGSQL_CONDITION, + PLPGSQL_CONTINUE_STMT, PLPGSQL_CURSOR_ARG, PLPGSQL_CURSOR_ARG_LIST, PLPGSQL_CURSOR_DECL, @@ -1519,16 +1520,20 @@ pub enum SyntaxKind { PLPGSQL_ELSIF_CLAUSE, PLPGSQL_EXCEPTION_HANDLER, PLPGSQL_EXCEPTION_SECTION, + PLPGSQL_EXIT_STMT, + PLPGSQL_EXIT_WHEN, PLPGSQL_IF_STMT, PLPGSQL_LABEL, PLPGSQL_LABEL_NAME, PLPGSQL_LABEL_NAME_REF, + PLPGSQL_LOOP_STMT, PLPGSQL_NOT_NULL, PLPGSQL_NULL_STMT, PLPGSQL_PERCENT_ROWTYPE, PLPGSQL_VAR_DECL, PLPGSQL_VAR_INIT, PLPGSQL_VAR_NAME, + PLPGSQL_WHILE_STMT, POLICY, POLICY_COMMAND, POLICY_COMMAND_ALL, diff --git a/crates/squawk_parser/src/plpgsql_grammar.rs b/crates/squawk_parser/src/plpgsql_grammar.rs index 1aa89fcd..8f3d50d0 100644 --- a/crates/squawk_parser/src/plpgsql_grammar.rs +++ b/crates/squawk_parser/src/plpgsql_grammar.rs @@ -27,7 +27,7 @@ fn opt_block(p: &mut Parser) { return; } let m = p.start(); - opt_block_label(p); + opt_label(p); opt_declare_section(p); p.expect(BEGIN_KW); body(p, BodyKind::Block); @@ -40,8 +40,8 @@ fn opt_block(p: &mut Parser) { } // <> -fn opt_block_label(p: &mut Parser) { - if !at_block_label(p) { +fn opt_label(p: &mut Parser) { + if !at_label(p) { return; } let m = p.start(); @@ -254,6 +254,7 @@ enum BodyKind { IfElse, CaseWhen, CaseElse, + Loop, } fn body(p: &mut Parser, kind: BodyKind) { @@ -267,6 +268,10 @@ fn body(p: &mut Parser, kind: BodyKind) { fn stmt(p: &mut Parser) { if at_block_start(p) { opt_block(p); + } else if at_loop_start(p) { + loop_stmt(p); + } else if at_exit_stmt(p) { + exit_stmt(p); } else if p.at(CASE_KW) { case_stmt(p); } else if p.at(IF_KW) { @@ -329,6 +334,59 @@ fn case_when(p: &mut Parser) { m.complete(p, PLPGSQL_CASE_WHEN); } +fn loop_stmt(p: &mut Parser) { + assert!(at_loop_start(p)); + let m = p.start(); + opt_label(p); + let kind = if p.nth_at_contextual_kw(0, WHILE_KW) { + p.bump_remap(WHILE_KW); + expr(p); + PLPGSQL_WHILE_STMT + } else { + PLPGSQL_LOOP_STMT + }; + expect_contextual_kw(p, LOOP_KW); + body(p, BodyKind::Loop); + p.expect(END_KW); + expect_contextual_kw(p, LOOP_KW); + opt_label_name_ref(p); + p.expect(SEMICOLON); + m.complete(p, kind); +} + +fn exit_stmt(p: &mut Parser) { + assert!(at_exit_stmt(p)); + let m = p.start(); + let kind = if p.eat(CONTINUE_KW) { + PLPGSQL_CONTINUE_STMT + } else { + p.bump_remap(EXIT_KW); + PLPGSQL_EXIT_STMT + }; + opt_label_name_ref(p); + opt_exit_when(p); + p.expect(SEMICOLON); + m.complete(p, kind); +} + +fn opt_exit_when(p: &mut Parser) { + if !p.at(WHEN_KW) { + return; + } + let m = p.start(); + p.bump(WHEN_KW); + expr(p); + m.complete(p, PLPGSQL_EXIT_WHEN); +} + +fn expect_contextual_kw(p: &mut Parser, kw: SyntaxKind) { + if p.nth_at_contextual_kw(0, kw) { + p.bump_remap(kw); + } else { + p.error(format!("expected {kw:?}")); + } +} + fn opt_elsif_clause(p: &mut Parser) -> bool { if !at_elsif(p) { return false; @@ -436,7 +494,23 @@ fn at_block_start(p: &Parser) -> bool { } fn at_block_label(p: &Parser) -> bool { - p.at(LESS_LESS) && at_name(p, 2) && p.nth_at(3, GREATER_GREATER) && p.nth_at_ts(5, BLOCK_FIRST) + at_label(p) && p.nth_at_ts(5, BLOCK_FIRST) +} + +fn at_label(p: &Parser) -> bool { + p.at(LESS_LESS) && at_name(p, 2) && p.nth_at(3, GREATER_GREATER) +} + +fn at_loop_start(p: &Parser) -> bool { + at_loop_kw(p, 0) || (at_label(p) && at_loop_kw(p, 5)) +} + +fn at_loop_kw(p: &Parser, n: usize) -> bool { + p.nth_at_contextual_kw(n, LOOP_KW) || p.nth_at_contextual_kw(n, WHILE_KW) +} + +fn at_exit_stmt(p: &Parser) -> bool { + p.at(CONTINUE_KW) || p.nth_at_contextual_kw(0, EXIT_KW) } fn at_name(p: &Parser, n: usize) -> bool { @@ -464,6 +538,7 @@ fn at_body_end(p: &Parser, kind: BodyKind) -> bool { BodyKind::IfElse => at_end_if(p), BodyKind::CaseWhen => at_end_case(p) || p.at(WHEN_KW) || p.at(ELSE_KW), BodyKind::CaseElse => at_end_case(p), + BodyKind::Loop => at_end_loop(p), } } @@ -475,6 +550,10 @@ fn at_end_case(p: &Parser) -> bool { p.at(END_KW) && p.nth_at(1, CASE_KW) } +fn at_end_loop(p: &Parser) -> bool { + p.at(END_KW) && p.nth_at_contextual_kw(1, LOOP_KW) +} + fn at_block_end(p: &Parser) -> bool { if !p.at(END_KW) { return false; diff --git a/crates/squawk_parser/tests/data/plpgsql/err/loop_stmt.sql b/crates/squawk_parser/tests/data/plpgsql/err/loop_stmt.sql new file mode 100644 index 00000000..f73adf65 --- /dev/null +++ b/crates/squawk_parser/tests/data/plpgsql/err/loop_stmt.sql @@ -0,0 +1,17 @@ +do $$ +begin + loop + null; + end; + + while 1 = 1 + null; + end loop; + + loop + null; + end loop + + exit when +end +$$; diff --git a/crates/squawk_parser/tests/data/plpgsql/ok/loop_stmt.sql b/crates/squawk_parser/tests/data/plpgsql/ok/loop_stmt.sql new file mode 100644 index 00000000..db8fc3d5 --- /dev/null +++ b/crates/squawk_parser/tests/data/plpgsql/ok/loop_stmt.sql @@ -0,0 +1,37 @@ +do $$ +declare + i int := 0; +begin + loop + exit; + end loop; + + loop + exit when i > 10; + continue when i = 2; + continue; + end loop; + + while i < 10 loop + null; + end loop; + + while case when i = 1 then true else false end loop + null; + end loop; + + <> + loop + <> + while i < 10 loop + continue outer when i = 2; + exit inner; + end loop inner; + end loop outer; + + <> + begin + exit blk; + end blk; +end +$$; diff --git a/crates/squawk_syntax/src/ast/generated/nodes.rs b/crates/squawk_syntax/src/ast/generated/nodes.rs index 055d40e4..a1741fdd 100644 --- a/crates/squawk_syntax/src/ast/generated/nodes.rs +++ b/crates/squawk_syntax/src/ast/generated/nodes.rs @@ -19573,6 +19573,29 @@ impl PlpgsqlCondition { } } +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct PlpgsqlContinueStmt { + pub(crate) syntax: SyntaxNode, +} +impl PlpgsqlContinueStmt { + #[inline] + pub fn label(&self) -> Option { + support::child(&self.syntax) + } + #[inline] + pub fn when_clause(&self) -> Option { + support::child(&self.syntax) + } + #[inline] + pub fn semicolon_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::SEMICOLON) + } + #[inline] + pub fn continue_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::CONTINUE_KW) + } +} + #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct PlpgsqlCursorArg { pub(crate) syntax: SyntaxNode, @@ -19741,6 +19764,44 @@ impl PlpgsqlExceptionSection { } } +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct PlpgsqlExitStmt { + pub(crate) syntax: SyntaxNode, +} +impl PlpgsqlExitStmt { + #[inline] + pub fn label(&self) -> Option { + support::child(&self.syntax) + } + #[inline] + pub fn when_clause(&self) -> Option { + support::child(&self.syntax) + } + #[inline] + pub fn semicolon_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::SEMICOLON) + } + #[inline] + pub fn exit_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::EXIT_KW) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct PlpgsqlExitWhen { + pub(crate) syntax: SyntaxNode, +} +impl PlpgsqlExitWhen { + #[inline] + pub fn cond(&self) -> Option { + support::child(&self.syntax) + } + #[inline] + pub fn when_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::WHEN_KW) + } +} + #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct PlpgsqlIfStmt { pub(crate) syntax: SyntaxNode, @@ -19829,6 +19890,37 @@ impl PlpgsqlLabelNameRef { } } +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct PlpgsqlLoopStmt { + pub(crate) syntax: SyntaxNode, +} +impl PlpgsqlLoopStmt { + #[inline] + pub fn body(&self) -> Option { + support::child(&self.syntax) + } + #[inline] + pub fn end_label(&self) -> Option { + support::child(&self.syntax) + } + #[inline] + pub fn label(&self) -> Option { + support::child(&self.syntax) + } + #[inline] + pub fn semicolon_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::SEMICOLON) + } + #[inline] + pub fn end_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::END_KW) + } + #[inline] + pub fn loop_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::LOOP_KW) + } +} + #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct PlpgsqlNotNull { pub(crate) syntax: SyntaxNode, @@ -19959,6 +20051,45 @@ impl PlpgsqlVarName { } } +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct PlpgsqlWhileStmt { + pub(crate) syntax: SyntaxNode, +} +impl PlpgsqlWhileStmt { + #[inline] + pub fn body(&self) -> Option { + support::child(&self.syntax) + } + #[inline] + pub fn cond(&self) -> Option { + support::child(&self.syntax) + } + #[inline] + pub fn end_label(&self) -> Option { + support::child(&self.syntax) + } + #[inline] + pub fn label(&self) -> Option { + support::child(&self.syntax) + } + #[inline] + pub fn semicolon_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::SEMICOLON) + } + #[inline] + pub fn end_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::END_KW) + } + #[inline] + pub fn loop_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::LOOP_KW) + } + #[inline] + pub fn while_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::WHILE_KW) + } +} + #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct Policy { pub(crate) syntax: SyntaxNode, @@ -29988,8 +30119,12 @@ pub enum PlpgsqlDeclType { pub enum PlpgsqlStmt { PlpgsqlBlock(PlpgsqlBlock), PlpgsqlCaseStmt(PlpgsqlCaseStmt), + PlpgsqlContinueStmt(PlpgsqlContinueStmt), + PlpgsqlExitStmt(PlpgsqlExitStmt), PlpgsqlIfStmt(PlpgsqlIfStmt), + PlpgsqlLoopStmt(PlpgsqlLoopStmt), PlpgsqlNullStmt(PlpgsqlNullStmt), + PlpgsqlWhileStmt(PlpgsqlWhileStmt), } #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -46746,6 +46881,24 @@ impl AstNode for PlpgsqlCondition { &self.syntax } } +impl AstNode for PlpgsqlContinueStmt { + #[inline] + fn can_cast(kind: SyntaxKind) -> bool { + kind == SyntaxKind::PLPGSQL_CONTINUE_STMT + } + #[inline] + fn cast(syntax: SyntaxNode) -> Option { + if Self::can_cast(syntax.kind()) { + Some(Self { syntax }) + } else { + None + } + } + #[inline] + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} impl AstNode for PlpgsqlCursorArg { #[inline] fn can_cast(kind: SyntaxKind) -> bool { @@ -46890,6 +47043,42 @@ impl AstNode for PlpgsqlExceptionSection { &self.syntax } } +impl AstNode for PlpgsqlExitStmt { + #[inline] + fn can_cast(kind: SyntaxKind) -> bool { + kind == SyntaxKind::PLPGSQL_EXIT_STMT + } + #[inline] + fn cast(syntax: SyntaxNode) -> Option { + if Self::can_cast(syntax.kind()) { + Some(Self { syntax }) + } else { + None + } + } + #[inline] + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +impl AstNode for PlpgsqlExitWhen { + #[inline] + fn can_cast(kind: SyntaxKind) -> bool { + kind == SyntaxKind::PLPGSQL_EXIT_WHEN + } + #[inline] + fn cast(syntax: SyntaxNode) -> Option { + if Self::can_cast(syntax.kind()) { + Some(Self { syntax }) + } else { + None + } + } + #[inline] + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} impl AstNode for PlpgsqlIfStmt { #[inline] fn can_cast(kind: SyntaxKind) -> bool { @@ -46962,6 +47151,24 @@ impl AstNode for PlpgsqlLabelNameRef { &self.syntax } } +impl AstNode for PlpgsqlLoopStmt { + #[inline] + fn can_cast(kind: SyntaxKind) -> bool { + kind == SyntaxKind::PLPGSQL_LOOP_STMT + } + #[inline] + fn cast(syntax: SyntaxNode) -> Option { + if Self::can_cast(syntax.kind()) { + Some(Self { syntax }) + } else { + None + } + } + #[inline] + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} impl AstNode for PlpgsqlNotNull { #[inline] fn can_cast(kind: SyntaxKind) -> bool { @@ -47070,6 +47277,24 @@ impl AstNode for PlpgsqlVarName { &self.syntax } } +impl AstNode for PlpgsqlWhileStmt { + #[inline] + fn can_cast(kind: SyntaxKind) -> bool { + kind == SyntaxKind::PLPGSQL_WHILE_STMT + } + #[inline] + fn cast(syntax: SyntaxNode) -> Option { + if Self::can_cast(syntax.kind()) { + Some(Self { syntax }) + } else { + None + } + } + #[inline] + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} impl AstNode for Policy { #[inline] fn can_cast(kind: SyntaxKind) -> bool { @@ -66898,8 +67123,12 @@ impl AstNode for PlpgsqlStmt { kind, SyntaxKind::PLPGSQL_BLOCK | SyntaxKind::PLPGSQL_CASE_STMT + | SyntaxKind::PLPGSQL_CONTINUE_STMT + | SyntaxKind::PLPGSQL_EXIT_STMT | SyntaxKind::PLPGSQL_IF_STMT + | SyntaxKind::PLPGSQL_LOOP_STMT | SyntaxKind::PLPGSQL_NULL_STMT + | SyntaxKind::PLPGSQL_WHILE_STMT ) } #[inline] @@ -66909,10 +67138,22 @@ impl AstNode for PlpgsqlStmt { SyntaxKind::PLPGSQL_CASE_STMT => { PlpgsqlStmt::PlpgsqlCaseStmt(PlpgsqlCaseStmt { syntax }) } + SyntaxKind::PLPGSQL_CONTINUE_STMT => { + PlpgsqlStmt::PlpgsqlContinueStmt(PlpgsqlContinueStmt { syntax }) + } + SyntaxKind::PLPGSQL_EXIT_STMT => { + PlpgsqlStmt::PlpgsqlExitStmt(PlpgsqlExitStmt { syntax }) + } SyntaxKind::PLPGSQL_IF_STMT => PlpgsqlStmt::PlpgsqlIfStmt(PlpgsqlIfStmt { syntax }), + SyntaxKind::PLPGSQL_LOOP_STMT => { + PlpgsqlStmt::PlpgsqlLoopStmt(PlpgsqlLoopStmt { syntax }) + } SyntaxKind::PLPGSQL_NULL_STMT => { PlpgsqlStmt::PlpgsqlNullStmt(PlpgsqlNullStmt { syntax }) } + SyntaxKind::PLPGSQL_WHILE_STMT => { + PlpgsqlStmt::PlpgsqlWhileStmt(PlpgsqlWhileStmt { syntax }) + } _ => { return None; } @@ -66924,8 +67165,12 @@ impl AstNode for PlpgsqlStmt { match self { PlpgsqlStmt::PlpgsqlBlock(it) => &it.syntax, PlpgsqlStmt::PlpgsqlCaseStmt(it) => &it.syntax, + PlpgsqlStmt::PlpgsqlContinueStmt(it) => &it.syntax, + PlpgsqlStmt::PlpgsqlExitStmt(it) => &it.syntax, PlpgsqlStmt::PlpgsqlIfStmt(it) => &it.syntax, + PlpgsqlStmt::PlpgsqlLoopStmt(it) => &it.syntax, PlpgsqlStmt::PlpgsqlNullStmt(it) => &it.syntax, + PlpgsqlStmt::PlpgsqlWhileStmt(it) => &it.syntax, } } } @@ -66941,18 +67186,42 @@ impl From for PlpgsqlStmt { PlpgsqlStmt::PlpgsqlCaseStmt(node) } } +impl From for PlpgsqlStmt { + #[inline] + fn from(node: PlpgsqlContinueStmt) -> PlpgsqlStmt { + PlpgsqlStmt::PlpgsqlContinueStmt(node) + } +} +impl From for PlpgsqlStmt { + #[inline] + fn from(node: PlpgsqlExitStmt) -> PlpgsqlStmt { + PlpgsqlStmt::PlpgsqlExitStmt(node) + } +} impl From for PlpgsqlStmt { #[inline] fn from(node: PlpgsqlIfStmt) -> PlpgsqlStmt { PlpgsqlStmt::PlpgsqlIfStmt(node) } } +impl From for PlpgsqlStmt { + #[inline] + fn from(node: PlpgsqlLoopStmt) -> PlpgsqlStmt { + PlpgsqlStmt::PlpgsqlLoopStmt(node) + } +} impl From for PlpgsqlStmt { #[inline] fn from(node: PlpgsqlNullStmt) -> PlpgsqlStmt { PlpgsqlStmt::PlpgsqlNullStmt(node) } } +impl From for PlpgsqlStmt { + #[inline] + fn from(node: PlpgsqlWhileStmt) -> PlpgsqlStmt { + PlpgsqlStmt::PlpgsqlWhileStmt(node) + } +} impl AstNode for PolicyCommandKind { #[inline] fn can_cast(kind: SyntaxKind) -> bool { diff --git a/crates/squawk_syntax/src/postgresql.ungram b/crates/squawk_syntax/src/postgresql.ungram index 2471d48a..1ec67986 100644 --- a/crates/squawk_syntax/src/postgresql.ungram +++ b/crates/squawk_syntax/src/postgresql.ungram @@ -2146,8 +2146,12 @@ PlpgsqlBody = PlpgsqlStmt = PlpgsqlBlock | PlpgsqlCaseStmt +| PlpgsqlContinueStmt +| PlpgsqlExitStmt | PlpgsqlIfStmt +| PlpgsqlLoopStmt | PlpgsqlNullStmt +| PlpgsqlWhileStmt PlpgsqlNullStmt = 'null' ';' @@ -2184,6 +2188,40 @@ PlpgsqlCaseWhen = 'then' body:PlpgsqlBody +PlpgsqlLoopStmt = + label:PlpgsqlLabel? + 'loop' + body:PlpgsqlBody + 'end' 'loop' + end_label:PlpgsqlLabelNameRef? + ';' + +PlpgsqlWhileStmt = + label:PlpgsqlLabel? + 'while' + cond:Expr + 'loop' + body:PlpgsqlBody + 'end' 'loop' + end_label:PlpgsqlLabelNameRef? + ';' + +PlpgsqlExitStmt = + 'exit' + label:PlpgsqlLabelNameRef? + when_clause:PlpgsqlExitWhen? + ';' + +PlpgsqlContinueStmt = + 'continue' + label:PlpgsqlLabelNameRef? + when_clause:PlpgsqlExitWhen? + ';' + +PlpgsqlExitWhen = + 'when' + cond:Expr + FuncOptionList = options:(FuncOption*) diff --git a/crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_loop_stmt_err.snap b/crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_loop_stmt_err.snap new file mode 100644 index 00000000..89a37344 --- /dev/null +++ b/crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_loop_stmt_err.snap @@ -0,0 +1,84 @@ +--- +source: crates/squawk_syntax/src/test.rs +input_file: crates/squawk_parser/tests/data/plpgsql/err/loop_stmt.sql +--- +PLPGSQL@0..114 + WHITESPACE@0..1 "\n" + PLPGSQL_BLOCK@1..113 + BEGIN_KW@1..6 "begin" + WHITESPACE@6..9 "\n " + PLPGSQL_BODY@9..113 + PLPGSQL_LOOP_STMT@9..30 + LOOP_KW@9..13 "loop" + WHITESPACE@13..18 "\n " + PLPGSQL_BODY@18..23 + PLPGSQL_NULL_STMT@18..23 + NULL_KW@18..22 "null" + SEMICOLON@22..23 ";" + WHITESPACE@23..26 "\n " + END_KW@26..29 "end" + SEMICOLON@29..30 ";" + WHITESPACE@30..34 "\n\n " + PLPGSQL_WHILE_STMT@34..67 + WHILE_KW@34..39 "while" + WHITESPACE@39..40 " " + BIN_EXPR@40..45 + LITERAL@40..41 + INT_NUMBER@40..41 "1" + WHITESPACE@41..42 " " + EQ@42..43 "=" + WHITESPACE@43..44 " " + LITERAL@44..45 + INT_NUMBER@44..45 "1" + WHITESPACE@45..50 "\n " + PLPGSQL_BODY@50..55 + PLPGSQL_NULL_STMT@50..55 + NULL_KW@50..54 "null" + SEMICOLON@54..55 ";" + WHITESPACE@55..58 "\n " + END_KW@58..61 "end" + WHITESPACE@61..62 " " + LOOP_KW@62..66 "loop" + SEMICOLON@66..67 ";" + WHITESPACE@67..71 "\n\n " + PLPGSQL_LOOP_STMT@71..104 + LOOP_KW@71..75 "loop" + WHITESPACE@75..80 "\n " + PLPGSQL_BODY@80..85 + PLPGSQL_NULL_STMT@80..85 + NULL_KW@80..84 "null" + SEMICOLON@84..85 ";" + WHITESPACE@85..88 "\n " + END_KW@88..91 "end" + WHITESPACE@91..92 " " + LOOP_KW@92..96 "loop" + WHITESPACE@96..100 "\n\n " + PLPGSQL_LABEL_NAME_REF@100..104 + IDENT@100..104 "exit" + WHITESPACE@104..105 " " + ERROR@105..113 + WHEN_KW@105..109 "when" + WHITESPACE@109..110 "\n" + END_KW@110..113 "end" + WHITESPACE@113..114 "\n" + +error[syntax-error]: expected LOOP_KW + ╭▸ +5 │ end; + ╰╴ ━ +error[syntax-error]: expected LOOP_KW + ╭▸ +7 │ while 1 = 1 + ╰╴ ━ +error[syntax-error]: expected SEMICOLON + ╭▸ +15 │ exit when + ╰╴ ━ +error[syntax-error]: expected a statement, found WHEN_KW + ╭▸ +15 │ exit when + ╰╴ ━ +error[syntax-error]: expected END_KW + ╭▸ +16 │ end + ╰╴ ━ diff --git a/crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_loop_stmt_ok.snap b/crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_loop_stmt_ok.snap new file mode 100644 index 00000000..8ee5aa13 --- /dev/null +++ b/crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_loop_stmt_ok.snap @@ -0,0 +1,253 @@ +--- +source: crates/squawk_syntax/src/test.rs +input_file: crates/squawk_parser/tests/data/plpgsql/ok/loop_stmt.sql +--- +PLPGSQL@0..452 + WHITESPACE@0..1 "\n" + PLPGSQL_BLOCK@1..451 + PLPGSQL_DECLARE_SECTION@1..22 + DECLARE_KW@1..8 "declare" + WHITESPACE@8..11 "\n " + PLPGSQL_VAR_DECL@11..22 + PLPGSQL_VAR_NAME@11..12 + IDENT@11..12 "i" + WHITESPACE@12..13 " " + PATH_TYPE@13..16 + PATH_REF@13..16 + PATH_SEGMENT_REF@13..16 + INT_KW@13..16 "int" + WHITESPACE@16..17 " " + PLPGSQL_VAR_INIT@17..21 + COLON_EQ@17..19 ":=" + WHITESPACE@19..20 " " + LITERAL@20..21 + INT_NUMBER@20..21 "0" + SEMICOLON@21..22 ";" + WHITESPACE@22..23 "\n" + BEGIN_KW@23..28 "begin" + WHITESPACE@28..31 "\n " + PLPGSQL_BODY@31..447 + PLPGSQL_LOOP_STMT@31..57 + LOOP_KW@31..35 "loop" + WHITESPACE@35..40 "\n " + PLPGSQL_BODY@40..45 + PLPGSQL_EXIT_STMT@40..45 + EXIT_KW@40..44 "exit" + SEMICOLON@44..45 ";" + WHITESPACE@45..48 "\n " + END_KW@48..51 "end" + WHITESPACE@51..52 " " + LOOP_KW@52..56 "loop" + SEMICOLON@56..57 ";" + WHITESPACE@57..61 "\n\n " + PLPGSQL_LOOP_STMT@61..138 + LOOP_KW@61..65 "loop" + WHITESPACE@65..70 "\n " + PLPGSQL_BODY@70..126 + PLPGSQL_EXIT_STMT@70..87 + EXIT_KW@70..74 "exit" + WHITESPACE@74..75 " " + PLPGSQL_EXIT_WHEN@75..86 + WHEN_KW@75..79 "when" + WHITESPACE@79..80 " " + BIN_EXPR@80..86 + NAME_REF@80..81 + IDENT@80..81 "i" + WHITESPACE@81..82 " " + R_ANGLE@82..83 ">" + WHITESPACE@83..84 " " + LITERAL@84..86 + INT_NUMBER@84..86 "10" + SEMICOLON@86..87 ";" + WHITESPACE@87..92 "\n " + PLPGSQL_CONTINUE_STMT@92..112 + CONTINUE_KW@92..100 "continue" + WHITESPACE@100..101 " " + PLPGSQL_EXIT_WHEN@101..111 + WHEN_KW@101..105 "when" + WHITESPACE@105..106 " " + BIN_EXPR@106..111 + NAME_REF@106..107 + IDENT@106..107 "i" + WHITESPACE@107..108 " " + EQ@108..109 "=" + WHITESPACE@109..110 " " + LITERAL@110..111 + INT_NUMBER@110..111 "2" + SEMICOLON@111..112 ";" + WHITESPACE@112..117 "\n " + PLPGSQL_CONTINUE_STMT@117..126 + CONTINUE_KW@117..125 "continue" + SEMICOLON@125..126 ";" + WHITESPACE@126..129 "\n " + END_KW@129..132 "end" + WHITESPACE@132..133 " " + LOOP_KW@133..137 "loop" + SEMICOLON@137..138 ";" + WHITESPACE@138..142 "\n\n " + PLPGSQL_WHILE_STMT@142..181 + WHILE_KW@142..147 "while" + WHITESPACE@147..148 " " + BIN_EXPR@148..154 + NAME_REF@148..149 + IDENT@148..149 "i" + WHITESPACE@149..150 " " + L_ANGLE@150..151 "<" + WHITESPACE@151..152 " " + LITERAL@152..154 + INT_NUMBER@152..154 "10" + WHITESPACE@154..155 " " + LOOP_KW@155..159 "loop" + WHITESPACE@159..164 "\n " + PLPGSQL_BODY@164..169 + PLPGSQL_NULL_STMT@164..169 + NULL_KW@164..168 "null" + SEMICOLON@168..169 ";" + WHITESPACE@169..172 "\n " + END_KW@172..175 "end" + WHITESPACE@175..176 " " + LOOP_KW@176..180 "loop" + SEMICOLON@180..181 ";" + WHITESPACE@181..185 "\n\n " + PLPGSQL_WHILE_STMT@185..258 + WHILE_KW@185..190 "while" + WHITESPACE@190..191 " " + CASE_EXPR@191..231 + CASE_KW@191..195 "case" + WHITESPACE@195..196 " " + WHEN_CLAUSE_LIST@196..216 + WHEN_CLAUSE@196..216 + WHEN_KW@196..200 "when" + WHITESPACE@200..201 " " + BIN_EXPR@201..206 + NAME_REF@201..202 + IDENT@201..202 "i" + WHITESPACE@202..203 " " + EQ@203..204 "=" + WHITESPACE@204..205 " " + LITERAL@205..206 + INT_NUMBER@205..206 "1" + WHITESPACE@206..207 " " + THEN_KW@207..211 "then" + WHITESPACE@211..212 " " + LITERAL@212..216 + TRUE_KW@212..216 "true" + WHITESPACE@216..217 " " + ELSE_CLAUSE@217..227 + ELSE_KW@217..221 "else" + WHITESPACE@221..222 " " + LITERAL@222..227 + FALSE_KW@222..227 "false" + WHITESPACE@227..228 " " + END_KW@228..231 "end" + WHITESPACE@231..232 " " + LOOP_KW@232..236 "loop" + WHITESPACE@236..241 "\n " + PLPGSQL_BODY@241..246 + PLPGSQL_NULL_STMT@241..246 + NULL_KW@241..245 "null" + SEMICOLON@245..246 ";" + WHITESPACE@246..249 "\n " + END_KW@249..252 "end" + WHITESPACE@252..253 " " + LOOP_KW@253..257 "loop" + SEMICOLON@257..258 ";" + WHITESPACE@258..262 "\n\n " + PLPGSQL_LOOP_STMT@262..403 + PLPGSQL_LABEL@262..271 + LESS_LESS@262..264 "<<" + PLPGSQL_LABEL_NAME@264..269 + OUTER_KW@264..269 "outer" + GREATER_GREATER@269..271 ">>" + WHITESPACE@271..274 "\n " + LOOP_KW@274..278 "loop" + WHITESPACE@278..283 "\n " + PLPGSQL_BODY@283..385 + PLPGSQL_WHILE_STMT@283..385 + PLPGSQL_LABEL@283..292 + LESS_LESS@283..285 "<<" + PLPGSQL_LABEL_NAME@285..290 + INNER_KW@285..290 "inner" + GREATER_GREATER@290..292 ">>" + WHITESPACE@292..297 "\n " + WHILE_KW@297..302 "while" + WHITESPACE@302..303 " " + BIN_EXPR@303..309 + NAME_REF@303..304 + IDENT@303..304 "i" + WHITESPACE@304..305 " " + L_ANGLE@305..306 "<" + WHITESPACE@306..307 " " + LITERAL@307..309 + INT_NUMBER@307..309 "10" + WHITESPACE@309..310 " " + LOOP_KW@310..314 "loop" + WHITESPACE@314..321 "\n " + PLPGSQL_BODY@321..365 + PLPGSQL_CONTINUE_STMT@321..347 + CONTINUE_KW@321..329 "continue" + WHITESPACE@329..330 " " + PLPGSQL_LABEL_NAME_REF@330..335 + OUTER_KW@330..335 "outer" + WHITESPACE@335..336 " " + PLPGSQL_EXIT_WHEN@336..346 + WHEN_KW@336..340 "when" + WHITESPACE@340..341 " " + BIN_EXPR@341..346 + NAME_REF@341..342 + IDENT@341..342 "i" + WHITESPACE@342..343 " " + EQ@343..344 "=" + WHITESPACE@344..345 " " + LITERAL@345..346 + INT_NUMBER@345..346 "2" + SEMICOLON@346..347 ";" + WHITESPACE@347..354 "\n " + PLPGSQL_EXIT_STMT@354..365 + EXIT_KW@354..358 "exit" + WHITESPACE@358..359 " " + PLPGSQL_LABEL_NAME_REF@359..364 + INNER_KW@359..364 "inner" + SEMICOLON@364..365 ";" + WHITESPACE@365..370 "\n " + END_KW@370..373 "end" + WHITESPACE@373..374 " " + LOOP_KW@374..378 "loop" + WHITESPACE@378..379 " " + PLPGSQL_LABEL_NAME_REF@379..384 + INNER_KW@379..384 "inner" + SEMICOLON@384..385 ";" + WHITESPACE@385..388 "\n " + END_KW@388..391 "end" + WHITESPACE@391..392 " " + LOOP_KW@392..396 "loop" + WHITESPACE@396..397 " " + PLPGSQL_LABEL_NAME_REF@397..402 + OUTER_KW@397..402 "outer" + SEMICOLON@402..403 ";" + WHITESPACE@403..407 "\n\n " + PLPGSQL_BLOCK@407..447 + PLPGSQL_LABEL@407..414 + LESS_LESS@407..409 "<<" + PLPGSQL_LABEL_NAME@409..412 + IDENT@409..412 "blk" + GREATER_GREATER@412..414 ">>" + WHITESPACE@414..417 "\n " + BEGIN_KW@417..422 "begin" + WHITESPACE@422..427 "\n " + PLPGSQL_BODY@427..436 + PLPGSQL_EXIT_STMT@427..436 + EXIT_KW@427..431 "exit" + WHITESPACE@431..432 " " + PLPGSQL_LABEL_NAME_REF@432..435 + IDENT@432..435 "blk" + SEMICOLON@435..436 ";" + WHITESPACE@436..439 "\n " + END_KW@439..442 "end" + WHITESPACE@442..443 " " + PLPGSQL_LABEL_NAME_REF@443..446 + IDENT@443..446 "blk" + SEMICOLON@446..447 ";" + WHITESPACE@447..448 "\n" + END_KW@448..451 "end" + WHITESPACE@451..452 "\n" diff --git a/crates/squawk_syntax/src/test.rs b/crates/squawk_syntax/src/test.rs index 2c23b961..fe296866 100644 --- a/crates/squawk_syntax/src/test.rs +++ b/crates/squawk_syntax/src/test.rs @@ -271,11 +271,11 @@ fn plpgsql_suite_score() { assert_snapshot!(table, @" file bodies tokens unparsed err - plpgsql.sql 254 9887 6445 807 + plpgsql.sql 254 9887 6391 796 plpgsql_array.sql 26 948 629 70 plpgsql_cache.sql 2 60 44 5 plpgsql_call.sql 45 1687 1366 176 - plpgsql_control.sql 27 1400 1152 161 + plpgsql_control.sql 27 1381 906 120 plpgsql_copy.sql 4 28 16 4 plpgsql_domain.sql 23 300 179 30 plpgsql_misc.sql 16 261 114 12 @@ -285,7 +285,7 @@ fn plpgsql_suite_score() { plpgsql_trap.sql 7 354 247 44 plpgsql_trigger.sql 1 55 42 5 plpgsql_varprops.sql 33 716 379 56 - total 549 19105 13244 1704 + total 549 19086 12944 1652 "); } From c0103ed1db111f4bcd793f3b8431c34539ff0357 Mon Sep 17 00:00:00 2001 From: Steve Dignam Date: Fri, 18 Sep 2026 19:08:16 -0400 Subject: [PATCH 10/11] assert, raises, call, do, perform, commit, return, rollback --- crates/squawk_ide/src/goto_definition.rs | 2 +- crates/squawk_ide/src/hover.rs | 2 + .../src/generated/syntax_kind.rs | 31 + crates/squawk_parser/src/grammar.rs | 56 +- crates/squawk_parser/src/plpgsql_grammar.rs | 313 ++++ .../tests/data/plpgsql/err/assert_stmt.sql | 6 + .../tests/data/plpgsql/err/assign_stmt.sql | 9 + .../tests/data/plpgsql/err/call_stmt.sql | 7 + .../tests/data/plpgsql/err/comp_option.sql | 41 + .../tests/data/plpgsql/err/do_stmt.sql | 6 + .../tests/data/plpgsql/err/perform_stmt.sql | 10 + .../tests/data/plpgsql/err/raise_stmt.sql | 9 + .../tests/data/plpgsql/err/return_stmt.sql | 9 + .../data/plpgsql/err/transaction_stmt.sql | 9 + .../tests/data/plpgsql/ok/assert_stmt.sql | 10 + .../tests/data/plpgsql/ok/assign_stmt.sql | 19 + .../tests/data/plpgsql/ok/call_stmt.sql | 10 + .../tests/data/plpgsql/ok/comp_option.sql | 42 + .../tests/data/plpgsql/ok/do_stmt.sql | 7 + .../tests/data/plpgsql/ok/perform_stmt.sql | 14 + .../tests/data/plpgsql/ok/raise_stmt.sql | 22 + .../tests/data/plpgsql/ok/return_stmt.sql | 20 + .../data/plpgsql/ok/transaction_stmt.sql | 15 + .../squawk_syntax/src/ast/generated/nodes.rs | 1595 ++++++++++++++++- crates/squawk_syntax/src/ast/node_ext.rs | 19 + crates/squawk_syntax/src/plpgsql.rs | 50 +- crates/squawk_syntax/src/postgresql.ungram | 159 +- ...syntax__test__plpgsql_assert_stmt_err.snap | 48 + ..._syntax__test__plpgsql_assert_stmt_ok.snap | 114 ++ ...syntax__test__plpgsql_assign_stmt_err.snap | 104 ++ ..._syntax__test__plpgsql_assign_stmt_ok.snap | 282 +++ ...k_syntax__test__plpgsql_call_stmt_err.snap | 76 + ...wk_syntax__test__plpgsql_call_stmt_ok.snap | 109 ++ ...syntax__test__plpgsql_comp_option_err.snap | 211 +++ ..._syntax__test__plpgsql_comp_option_ok.snap | 177 ++ ...awk_syntax__test__plpgsql_do_stmt_err.snap | 45 + ...uawk_syntax__test__plpgsql_do_stmt_ok.snap | 48 + ...yntax__test__plpgsql_perform_stmt_err.snap | 170 ++ ...syntax__test__plpgsql_perform_stmt_ok.snap | 236 +++ ..._syntax__test__plpgsql_raise_stmt_err.snap | 125 ++ ...k_syntax__test__plpgsql_raise_stmt_ok.snap | 271 +++ ...syntax__test__plpgsql_return_stmt_err.snap | 86 + ..._syntax__test__plpgsql_return_stmt_ok.snap | 242 +++ ...x__test__plpgsql_transaction_stmt_err.snap | 90 + ...ax__test__plpgsql_transaction_stmt_ok.snap | 117 ++ crates/squawk_syntax/src/test.rs | 28 +- crates/squawk_syntax/src/validation.rs | 19 + crates/xtask/src/codegen.rs | 34 +- 48 files changed, 5066 insertions(+), 58 deletions(-) create mode 100644 crates/squawk_parser/tests/data/plpgsql/err/assert_stmt.sql create mode 100644 crates/squawk_parser/tests/data/plpgsql/err/assign_stmt.sql create mode 100644 crates/squawk_parser/tests/data/plpgsql/err/call_stmt.sql create mode 100644 crates/squawk_parser/tests/data/plpgsql/err/comp_option.sql create mode 100644 crates/squawk_parser/tests/data/plpgsql/err/do_stmt.sql create mode 100644 crates/squawk_parser/tests/data/plpgsql/err/perform_stmt.sql create mode 100644 crates/squawk_parser/tests/data/plpgsql/err/raise_stmt.sql create mode 100644 crates/squawk_parser/tests/data/plpgsql/err/return_stmt.sql create mode 100644 crates/squawk_parser/tests/data/plpgsql/err/transaction_stmt.sql create mode 100644 crates/squawk_parser/tests/data/plpgsql/ok/assert_stmt.sql create mode 100644 crates/squawk_parser/tests/data/plpgsql/ok/assign_stmt.sql create mode 100644 crates/squawk_parser/tests/data/plpgsql/ok/call_stmt.sql create mode 100644 crates/squawk_parser/tests/data/plpgsql/ok/comp_option.sql create mode 100644 crates/squawk_parser/tests/data/plpgsql/ok/do_stmt.sql create mode 100644 crates/squawk_parser/tests/data/plpgsql/ok/perform_stmt.sql create mode 100644 crates/squawk_parser/tests/data/plpgsql/ok/raise_stmt.sql create mode 100644 crates/squawk_parser/tests/data/plpgsql/ok/return_stmt.sql create mode 100644 crates/squawk_parser/tests/data/plpgsql/ok/transaction_stmt.sql create mode 100644 crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_assert_stmt_err.snap create mode 100644 crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_assert_stmt_ok.snap create mode 100644 crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_assign_stmt_err.snap create mode 100644 crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_assign_stmt_ok.snap create mode 100644 crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_call_stmt_err.snap create mode 100644 crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_call_stmt_ok.snap create mode 100644 crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_comp_option_err.snap create mode 100644 crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_comp_option_ok.snap create mode 100644 crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_do_stmt_err.snap create mode 100644 crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_do_stmt_ok.snap create mode 100644 crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_perform_stmt_err.snap create mode 100644 crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_perform_stmt_ok.snap create mode 100644 crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_raise_stmt_err.snap create mode 100644 crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_raise_stmt_ok.snap create mode 100644 crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_return_stmt_err.snap create mode 100644 crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_return_stmt_ok.snap create mode 100644 crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_transaction_stmt_err.snap create mode 100644 crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_transaction_stmt_ok.snap diff --git a/crates/squawk_ide/src/goto_definition.rs b/crates/squawk_ide/src/goto_definition.rs index df990634..951f6dd8 100644 --- a/crates/squawk_ide/src/goto_definition.rs +++ b/crates/squawk_ide/src/goto_definition.rs @@ -232,7 +232,7 @@ pub fn goto_definition(db: &dyn Db, position: InFile) -> SmallVec<[Loc resolve::resolve_name_ref(db, InFile::new(definition_file, &name_ref)) }) } - ast::AnyNameRef::PlpgsqlLabelNameRef(_) => None, + ast::AnyNameRef::PlpgsqlLabelNameRef(_) | ast::AnyNameRef::PlpgsqlVarNameRef(_) => None, ast::AnyNameRef::PolicyRef(name_ref) => resolve_in_files(db, file, |definition_file| { resolve::resolve_policy_ref(db, InFile::new(definition_file, &name_ref)) }), diff --git a/crates/squawk_ide/src/hover.rs b/crates/squawk_ide/src/hover.rs index 11029293..3c0e330e 100644 --- a/crates/squawk_ide/src/hover.rs +++ b/crates/squawk_ide/src/hover.rs @@ -230,7 +230,9 @@ pub fn hover(db: &dyn Db, position: InFile) -> Option { | ast::AnyName::PlpgsqlCondition(_) | ast::AnyName::PlpgsqlLabelName(_) | ast::AnyName::PlpgsqlLabelNameRef(_) + | ast::AnyName::PlpgsqlOptionValue(_) | ast::AnyName::PlpgsqlVarName(_) + | ast::AnyName::PlpgsqlVarNameRef(_) | ast::AnyName::PolicyRef(_) | ast::AnyName::PreparedStatementRef(_) | ast::AnyName::PropertyName(_) diff --git a/crates/squawk_parser/src/generated/syntax_kind.rs b/crates/squawk_parser/src/generated/syntax_kind.rs index be8808ec..46f88199 100644 --- a/crates/squawk_parser/src/generated/syntax_kind.rs +++ b/crates/squawk_parser/src/generated/syntax_kind.rs @@ -1506,16 +1506,25 @@ pub enum SyntaxKind { PLPGSQL, PLPGSQL_ALIAS_DECL, PLPGSQL_ALIAS_TARGET, + PLPGSQL_ASSERT_STMT, + PLPGSQL_ASSIGN_STMT, + PLPGSQL_ASSIGN_TARGET, PLPGSQL_BLOCK, PLPGSQL_BODY, + PLPGSQL_CALL_STMT, PLPGSQL_CASE_STMT, PLPGSQL_CASE_WHEN, + PLPGSQL_COMMIT_STMT, + PLPGSQL_COMP_OPTION_DUMP, + PLPGSQL_COMP_OPTION_PRINT_STRICT_PARAMS, + PLPGSQL_COMP_OPTION_VARIABLE_CONFLICT, PLPGSQL_CONDITION, PLPGSQL_CONTINUE_STMT, PLPGSQL_CURSOR_ARG, PLPGSQL_CURSOR_ARG_LIST, PLPGSQL_CURSOR_DECL, PLPGSQL_DECLARE_SECTION, + PLPGSQL_DO_STMT, PLPGSQL_ELSE_CLAUSE, PLPGSQL_ELSIF_CLAUSE, PLPGSQL_EXCEPTION_HANDLER, @@ -1529,10 +1538,32 @@ pub enum SyntaxKind { PLPGSQL_LOOP_STMT, PLPGSQL_NOT_NULL, PLPGSQL_NULL_STMT, + PLPGSQL_OPTION_VALUE, PLPGSQL_PERCENT_ROWTYPE, + PLPGSQL_PERFORM_STMT, + PLPGSQL_RAISE_LEVEL, + PLPGSQL_RAISE_MESSAGE, + PLPGSQL_RAISE_OPTION_COLUMN, + PLPGSQL_RAISE_OPTION_CONSTRAINT, + PLPGSQL_RAISE_OPTION_DATATYPE, + PLPGSQL_RAISE_OPTION_DETAIL, + PLPGSQL_RAISE_OPTION_ERRCODE, + PLPGSQL_RAISE_OPTION_HINT, + PLPGSQL_RAISE_OPTION_MESSAGE, + PLPGSQL_RAISE_OPTION_SCHEMA, + PLPGSQL_RAISE_OPTION_TABLE, + PLPGSQL_RAISE_STMT, + PLPGSQL_RAISE_USING_CLAUSE, + PLPGSQL_RETURN_NEXT_STMT, + PLPGSQL_RETURN_QUERY_EXECUTE_STMT, + PLPGSQL_RETURN_QUERY_STMT, + PLPGSQL_RETURN_STMT, + PLPGSQL_ROLLBACK_STMT, + PLPGSQL_USING_CLAUSE, PLPGSQL_VAR_DECL, PLPGSQL_VAR_INIT, PLPGSQL_VAR_NAME, + PLPGSQL_VAR_NAME_REF, PLPGSQL_WHILE_STMT, POLICY, POLICY_COMMAND, diff --git a/crates/squawk_parser/src/grammar.rs b/crates/squawk_parser/src/grammar.rs index 57de1622..9584d074 100644 --- a/crates/squawk_parser/src/grammar.rs +++ b/crates/squawk_parser/src/grammar.rs @@ -3539,6 +3539,29 @@ fn select(p: &mut Parser, m: Option, r: &SelectRestrictions) -> Option) -> CompletedMarker { + assert!(p.nth_at_contextual_kw(0, PERFORM_KW)); + let m = p.start(); + let clause = p.start(); + // Postgres rewrites this to `select` before passing to the sql parser, but + // we can't do that + p.bump_remap(PERFORM_KW); + opt_select_all_or_distinct(p); + opt_target_list(p); + clause.complete(p, SELECT_CLAUSE); + select_tail(p, m, &SelectRestrictions::default(), SELECT) +} + +fn select_tail( + p: &mut Parser, + m: Marker, + r: &SelectRestrictions, + out_kind: SyntaxKind, +) -> CompletedMarker { + let mut out_kind = out_kind; if opt_into_clause(p).is_some() { out_kind = SELECT_INTO; } @@ -3550,20 +3573,20 @@ fn select(p: &mut Parser, m: Option, r: &SelectRestrictions) -> Option) -> bool { p.at(PERIOD_KW) && p.nth_at_ts(1, NAME_REF_FIRST) } -fn accessors(p: &mut Parser<'_>) { +pub(crate) fn accessors(p: &mut Parser<'_>) { while !p.at(EOF) { match p.current() { DOT => field_accessor(p), @@ -6266,7 +6289,7 @@ const COL_LABEL_FIRST: TokenSet = TokenSet::new(&[IDENT]) .union(TYPE_FUNC_NAME_KEYWORDS) .union(RESERVED_KEYWORDS); -const NAME_FIRST: TokenSet = TokenSet::new(&[IDENT]) +pub(crate) const NAME_FIRST: TokenSet = TokenSet::new(&[IDENT]) .union(UNRESERVED_KEYWORDS) .union(COL_NAME_KEYWORD_FIRST); @@ -7735,7 +7758,7 @@ fn commit(p: &mut Parser<'_>) -> CompletedMarker { m.complete(p, kind) } -fn opt_chain_clause(p: &mut Parser<'_>) { +pub(crate) fn opt_chain_clause(p: &mut Parser<'_>) { if !p.at(AND_KW) { return; } @@ -7833,7 +7856,7 @@ fn begin(p: &mut Parser<'_>) -> CompletedMarker { } // Sconst -fn opt_string_literal(p: &mut Parser<'_>) -> Option { +pub(crate) fn opt_string_literal(p: &mut Parser<'_>) -> Option { if p.at_ts(STRING_FIRST) { literal(p) } else { @@ -7967,7 +7990,8 @@ pub(crate) struct StmtRestrictions { pub(crate) fn stmt(p: &mut Parser, r: &StmtRestrictions) -> Option { match (p.current(), p.nth(1)) { - (SEMICOLON, _) => Some(empty_stmt(p)), + (SEMICOLON, _) if r.semi_allowed => Some(empty_stmt(p)), + (SEMICOLON, _) => None, (ABORT_KW, _) => Some(rollback(p)), (ALTER_KW, AGGREGATE_KW) => Some(alter_aggregate(p)), (ALTER_KW, COLLATION_KW) => Some(alter_collation(p)), @@ -8020,7 +8044,7 @@ pub(crate) fn stmt(p: &mut Parser, r: &StmtRestrictions) -> Option Some(alter_view(p)), (ANALYZE_KW | ANALYSE_KW, _) => Some(analyze(p)), (BEGIN_KW, _) if r.begin_end_allowed => Some(begin(p)), - (CALL_KW, _) => Some(call(p)), + (CALL_KW, _) => Some(call(p, r.semi_allowed)), (CHECKPOINT_KW, _) => Some(checkpoint(p)), (CLOSE_KW, _) => Some(close(p)), (CLUSTER_KW, _) => Some(cluster(p)), @@ -8120,7 +8144,7 @@ pub(crate) fn stmt(p: &mut Parser, r: &StmtRestrictions) -> Option Some(declare(p)), (DELETE_KW, _) => Some(delete(p, None, r.semi_allowed)), (DISCARD_KW, _) => Some(discard(p)), - (DO_KW, _) => Some(do_(p)), + (DO_KW, _) => Some(do_(p, r.semi_allowed)), (DROP_KW, ACCESS_KW) => Some(drop_access_method(p)), (DROP_KW, AGGREGATE_KW) => Some(drop_aggregate(p)), (DROP_KW, CAST_KW) => Some(drop_cast(p)), @@ -15987,14 +16011,16 @@ fn opt_do_language(p: &mut Parser<'_>) { } // DO [ LANGUAGE lang_name ] code -fn do_(p: &mut Parser<'_>) -> CompletedMarker { +fn do_(p: &mut Parser<'_>, semi_allowed: bool) -> CompletedMarker { assert!(p.at(DO_KW)); let m = p.start(); p.bump(DO_KW); opt_do_language(p); string_literal(p); opt_do_language(p); - p.eat(SEMICOLON); + if semi_allowed { + p.eat(SEMICOLON); + } m.complete(p, DO) } @@ -16720,7 +16746,7 @@ fn preparable_stmt(p: &mut Parser<'_>) { // https://www.postgresql.org/docs/17/sql-call.html // CALL name ( [ argument ] [, ...] ) -fn call(p: &mut Parser<'_>) -> CompletedMarker { +fn call(p: &mut Parser<'_>, semi_allowed: bool) -> CompletedMarker { assert!(p.at(CALL_KW)); let m = p.start(); p.bump(CALL_KW); @@ -16730,7 +16756,9 @@ fn call(p: &mut Parser<'_>) -> CompletedMarker { } else { p.error("expected L_PAREN"); } - p.eat(SEMICOLON); + if semi_allowed { + p.eat(SEMICOLON); + } m.complete(p, CALL) } diff --git a/crates/squawk_parser/src/plpgsql_grammar.rs b/crates/squawk_parser/src/plpgsql_grammar.rs index 8f3d50d0..c544c864 100644 --- a/crates/squawk_parser/src/plpgsql_grammar.rs +++ b/crates/squawk_parser/src/plpgsql_grammar.rs @@ -10,6 +10,9 @@ use crate::{ pub(crate) fn plpgsql_entry_point(p: &mut Parser) { let m = p.start(); + while !p.at(EOF) && p.at(POUND) { + comp_option(p); + } while !p.at(EOF) { if at_block_start(p) { opt_block(p); @@ -20,6 +23,50 @@ pub(crate) fn plpgsql_entry_point(p: &mut Parser) { m.complete(p, PLPGSQL); } +const VARIABLE_CONFLICT_VALUES: [SyntaxKind; 3] = [ERROR_KW, USE_VARIABLE_KW, USE_COLUMN_KW]; + +fn comp_option(p: &mut Parser) { + assert!(p.at(POUND)); + let m = p.start(); + p.bump(POUND); + let kind = if p.at(OPTION_KW) { + p.bump(OPTION_KW); + expect_contextual_kw(p, DUMP_KW); + PLPGSQL_COMP_OPTION_DUMP + } else if p.nth_at_contextual_kw(0, VARIABLE_CONFLICT_KW) { + p.bump_remap(VARIABLE_CONFLICT_KW); + variable_conflict_value(p); + PLPGSQL_COMP_OPTION_VARIABLE_CONFLICT + } else if p.nth_at_contextual_kw(0, PRINT_STRICT_PARAMS_KW) { + p.bump_remap(PRINT_STRICT_PARAMS_KW); + option_value(p); + PLPGSQL_COMP_OPTION_PRINT_STRICT_PARAMS + } else { + p.error("expected OPTION, PRINT_STRICT_PARAMS, or VARIABLE_CONFLICT"); + ERROR + }; + m.complete(p, kind); +} + +fn variable_conflict_value(p: &mut Parser) { + match VARIABLE_CONFLICT_VALUES + .into_iter() + .find(|&kw| at_maybe_contextual_kw(p, 0, kw)) + { + Some(kw) => bump_maybe_contextual_kw(p, kw), + None => p.error("expected ERROR, USE_VARIABLE, or USE_COLUMN"), + } +} + +fn option_value(p: &mut Parser) { + if !at_name(p, 0) { + let kind = p.current(); + p.error(format!("expected an option value, found {kind:?}")); + return; + } + name(p, PLPGSQL_OPTION_VALUE); +} + const BLOCK_FIRST: TokenSet = TokenSet::new(&[BEGIN_KW, DECLARE_KW]); fn opt_block(p: &mut Parser) { @@ -270,8 +317,24 @@ fn stmt(p: &mut Parser) { opt_block(p); } else if at_loop_start(p) { loop_stmt(p); + } else if at_assign_stmt(p) { + assign_stmt(p); } else if at_exit_stmt(p) { exit_stmt(p); + } else if p.at(CALL_KW) { + call_stmt(p); + } else if p.at(DO_KW) { + do_stmt(p); + } else if p.nth_at_contextual_kw(0, PERFORM_KW) { + perform_stmt(p); + } else if p.at(RETURN_KW) { + return_stmt(p); + } else if p.nth_at_contextual_kw(0, ASSERT_KW) { + assert_stmt(p); + } else if p.nth_at_contextual_kw(0, RAISE_KW) { + raise_stmt(p); + } else if at_transaction_stmt(p) { + transaction_stmt(p); } else if p.at(CASE_KW) { case_stmt(p); } else if p.at(IF_KW) { @@ -286,6 +349,231 @@ fn stmt(p: &mut Parser) { } } +fn call_stmt(p: &mut Parser) { + assert!(p.at(CALL_KW)); + let m = p.start(); + match grammar::stmt(p, &grammar::StmtRestrictions::default()).map(|x| x.kind()) { + Some(CALL) => (), + _ => { + p.error("expected a CALL statement"); + } + } + p.expect(SEMICOLON); + m.complete(p, PLPGSQL_CALL_STMT); +} + +fn do_stmt(p: &mut Parser) { + assert!(p.at(DO_KW)); + let m = p.start(); + match grammar::stmt(p, &grammar::StmtRestrictions::default()).map(|x| x.kind()) { + Some(DO) => (), + _ => { + p.error("expected a DO statement"); + } + } + p.expect(SEMICOLON); + m.complete(p, PLPGSQL_DO_STMT); +} + +fn perform_stmt(p: &mut Parser) { + assert!(p.nth_at_contextual_kw(0, PERFORM_KW)); + let m = p.start(); + match grammar::perform_select(p).kind() { + SELECT | SELECT_INTO | COMPOUND_SELECT => (), + _ => { + p.error("expected a SELECT statement"); + } + } + p.expect(SEMICOLON); + m.complete(p, PLPGSQL_PERFORM_STMT); +} + +fn return_stmt(p: &mut Parser) { + assert!(p.at(RETURN_KW)); + let m = p.start(); + p.bump(RETURN_KW); + let composite = p.nth_at(1, DOT); + let kind = if p.at(NEXT_KW) && !composite { + p.bump(NEXT_KW); + if !p.at(SEMICOLON) { + expr(p); + } + PLPGSQL_RETURN_NEXT_STMT + } else if p.nth_at_contextual_kw(0, QUERY_KW) && !composite && p.nth_at(1, EXECUTE_KW) { + p.bump_remap(QUERY_KW); + p.bump(EXECUTE_KW); + expr(p); + opt_using_clause(p); + PLPGSQL_RETURN_QUERY_EXECUTE_STMT + } else if p.nth_at_contextual_kw(0, QUERY_KW) && !composite { + p.bump_remap(QUERY_KW); + if grammar::stmt(p, &grammar::StmtRestrictions::default()).is_none() { + p.error("expected a query"); + } + PLPGSQL_RETURN_QUERY_STMT + } else { + if !p.at(SEMICOLON) { + expr(p); + } + PLPGSQL_RETURN_STMT + }; + p.expect(SEMICOLON); + m.complete(p, kind); +} + +fn assert_stmt(p: &mut Parser) { + assert!(p.nth_at_contextual_kw(0, ASSERT_KW)); + let m = p.start(); + p.bump_remap(ASSERT_KW); + expr(p); + if p.eat(COMMA) { + expr(p); + } + p.expect(SEMICOLON); + m.complete(p, PLPGSQL_ASSERT_STMT); +} + +fn opt_using_clause(p: &mut Parser) { + if !p.at(USING_KW) { + return; + } + let m = p.start(); + p.bump(USING_KW); + expr(p); + while !p.at(EOF) && p.eat(COMMA) { + expr(p); + } + m.complete(p, PLPGSQL_USING_CLAUSE); +} + +const RAISE_LEVELS: [SyntaxKind; 6] = [ + EXCEPTION_KW, + WARNING_KW, + NOTICE_KW, + INFO_KW, + LOG_KW, + DEBUG_KW, +]; + +const RAISE_OPTIONS: [(SyntaxKind, SyntaxKind); 9] = [ + (ERRCODE_KW, PLPGSQL_RAISE_OPTION_ERRCODE), + (MESSAGE_KW, PLPGSQL_RAISE_OPTION_MESSAGE), + (DETAIL_KW, PLPGSQL_RAISE_OPTION_DETAIL), + (HINT_KW, PLPGSQL_RAISE_OPTION_HINT), + (COLUMN_KW, PLPGSQL_RAISE_OPTION_COLUMN), + (CONSTRAINT_KW, PLPGSQL_RAISE_OPTION_CONSTRAINT), + (DATATYPE_KW, PLPGSQL_RAISE_OPTION_DATATYPE), + (TABLE_KW, PLPGSQL_RAISE_OPTION_TABLE), + (SCHEMA_KW, PLPGSQL_RAISE_OPTION_SCHEMA), +]; + +fn raise_stmt(p: &mut Parser) { + assert!(p.nth_at_contextual_kw(0, RAISE_KW)); + let m = p.start(); + p.bump_remap(RAISE_KW); + if !p.at(SEMICOLON) { + opt_raise_level(p); + if opt_raise_message(p) { + while !p.at(EOF) && p.eat(COMMA) { + expr(p); + } + } else if !p.at(USING_KW) { + condition(p); + } + opt_raise_using_clause(p); + } + p.expect(SEMICOLON); + m.complete(p, PLPGSQL_RAISE_STMT); +} + +fn opt_raise_level(p: &mut Parser) { + let Some(kw) = RAISE_LEVELS + .into_iter() + .find(|&kw| at_maybe_contextual_kw(p, 0, kw)) + else { + return; + }; + let m = p.start(); + bump_maybe_contextual_kw(p, kw); + m.complete(p, PLPGSQL_RAISE_LEVEL); +} + +fn opt_raise_message(p: &mut Parser) -> bool { + let Some(literal) = grammar::opt_string_literal(p) else { + return false; + }; + literal.precede(p).complete(p, PLPGSQL_RAISE_MESSAGE); + true +} + +fn opt_raise_using_clause(p: &mut Parser) { + if !p.at(USING_KW) { + return; + } + let m = p.start(); + p.bump(USING_KW); + raise_option(p); + while !p.at(EOF) && p.eat(COMMA) { + raise_option(p); + } + m.complete(p, PLPGSQL_RAISE_USING_CLAUSE); +} + +fn raise_option(p: &mut Parser) { + let m = p.start(); + let kind = match RAISE_OPTIONS + .into_iter() + .find(|&(kw, _)| at_maybe_contextual_kw(p, 0, kw)) + { + Some((kw, kind)) => { + bump_maybe_contextual_kw(p, kw); + kind + } + None => { + p.error("unrecognized RAISE statement option"); + ERROR + } + }; + if !p.eat(COLON_EQ) { + p.expect(EQ); + } + expr(p); + m.complete(p, kind); +} + +fn transaction_stmt(p: &mut Parser) { + assert!(at_transaction_stmt(p)); + let m = p.start(); + let kind = if p.eat(COMMIT_KW) { + PLPGSQL_COMMIT_STMT + } else { + p.bump(ROLLBACK_KW); + PLPGSQL_ROLLBACK_STMT + }; + grammar::opt_chain_clause(p); + p.expect(SEMICOLON); + m.complete(p, kind); +} + +fn assign_stmt(p: &mut Parser) { + assert!(at_assign_stmt(p)); + let m = p.start(); + assign_target(p); + if !p.eat(COLON_EQ) { + p.expect(EQ); + } + expr(p); + p.expect(SEMICOLON); + m.complete(p, PLPGSQL_ASSIGN_STMT); +} + +fn assign_target(p: &mut Parser) { + let m = p.start(); + name(p, PLPGSQL_VAR_NAME_REF); + grammar::accessors(p); + m.complete(p, PLPGSQL_ASSIGN_TARGET); +} + fn if_stmt(p: &mut Parser) { assert!(p.at(IF_KW)); let m = p.start(); @@ -509,6 +797,19 @@ fn at_loop_kw(p: &Parser, n: usize) -> bool { p.nth_at_contextual_kw(n, LOOP_KW) || p.nth_at_contextual_kw(n, WHILE_KW) } +fn at_assign_stmt(p: &Parser) -> bool { + at_assign_target(p) + && (p.nth_at(1, COLON_EQ) || p.nth_at(1, EQ) || p.nth_at(1, L_BRACK) || p.nth_at(1, DOT)) +} + +fn at_assign_target(p: &Parser) -> bool { + at_name(p, 0) && (p.at_ts(grammar::NAME_FIRST) || p.at(POSITIONAL_PARAM)) +} + +fn at_transaction_stmt(p: &Parser) -> bool { + p.at(COMMIT_KW) || p.at(ROLLBACK_KW) +} + fn at_exit_stmt(p: &Parser) -> bool { p.at(CONTINUE_KW) || p.nth_at_contextual_kw(0, EXIT_KW) } @@ -523,6 +824,18 @@ fn at_name(p: &Parser, n: usize) -> bool { p.nth_at_ts(n, ALL_KEYWORDS) && !p.nth_at_ts(n, PLPGSQL_RESERVED_KEYWORDS) } +fn at_maybe_contextual_kw(p: &Parser, n: usize, kw: SyntaxKind) -> bool { + p.nth_at(n, kw) || p.nth_at_contextual_kw(n, kw) +} + +fn bump_maybe_contextual_kw(p: &mut Parser, kw: SyntaxKind) { + if p.at(kw) { + p.bump(kw); + } else { + p.bump_remap(kw); + } +} + fn at_elsif(p: &Parser) -> bool { p.nth_at_contextual_kw(0, ELSIF_KW) || p.nth_at_contextual_kw(0, ELSEIF_KW) } diff --git a/crates/squawk_parser/tests/data/plpgsql/err/assert_stmt.sql b/crates/squawk_parser/tests/data/plpgsql/err/assert_stmt.sql new file mode 100644 index 00000000..89f1309f --- /dev/null +++ b/crates/squawk_parser/tests/data/plpgsql/err/assert_stmt.sql @@ -0,0 +1,6 @@ +do $$ +begin + assert; + assert 1 = 1, ; +end +$$; diff --git a/crates/squawk_parser/tests/data/plpgsql/err/assign_stmt.sql b/crates/squawk_parser/tests/data/plpgsql/err/assign_stmt.sql new file mode 100644 index 00000000..820c21b0 --- /dev/null +++ b/crates/squawk_parser/tests/data/plpgsql/err/assign_stmt.sql @@ -0,0 +1,9 @@ +do $$ +begin + x := ; + a[1 := 2; + x 1; + all := 1; + select := 1; +end +$$; diff --git a/crates/squawk_parser/tests/data/plpgsql/err/call_stmt.sql b/crates/squawk_parser/tests/data/plpgsql/err/call_stmt.sql new file mode 100644 index 00000000..0b83a633 --- /dev/null +++ b/crates/squawk_parser/tests/data/plpgsql/err/call_stmt.sql @@ -0,0 +1,7 @@ +do $$ +begin + call; + call p1(1, 2) + call p2 1; +end +$$; diff --git a/crates/squawk_parser/tests/data/plpgsql/err/comp_option.sql b/crates/squawk_parser/tests/data/plpgsql/err/comp_option.sql new file mode 100644 index 00000000..6cf1e6bc --- /dev/null +++ b/crates/squawk_parser/tests/data/plpgsql/err/comp_option.sql @@ -0,0 +1,41 @@ +do $$ +#option dumpp +begin end $$; + +do $$ +#foo bar +begin end $$; + +do $$ +#variable_conflict +begin end $$; + +do $$ +#variable_conflict use_default +begin end $$; + +-- a number is a syntax error +do $$ +#print_strict_params 1 +begin end $$; + +-- must be on/off +do $$ +#print_strict_params banana +begin end $$; + +-- a quoted value keeps its case, so this one doesn't match `on` +do $$ +#print_strict_params "ON" +begin end $$; + +-- `loop` is PL/pgSQL-reserved +do $$ +#print_strict_params loop +begin end $$; + +-- options only come before the block +do $$ +declare x int; +#option dump +begin end $$; diff --git a/crates/squawk_parser/tests/data/plpgsql/err/do_stmt.sql b/crates/squawk_parser/tests/data/plpgsql/err/do_stmt.sql new file mode 100644 index 00000000..9ddafb87 --- /dev/null +++ b/crates/squawk_parser/tests/data/plpgsql/err/do_stmt.sql @@ -0,0 +1,6 @@ +do $$ +begin + do := 1; + do; +end +$$; diff --git a/crates/squawk_parser/tests/data/plpgsql/err/perform_stmt.sql b/crates/squawk_parser/tests/data/plpgsql/err/perform_stmt.sql new file mode 100644 index 00000000..59817d2a --- /dev/null +++ b/crates/squawk_parser/tests/data/plpgsql/err/perform_stmt.sql @@ -0,0 +1,10 @@ +do $$ +begin + perform from; + perform 1 into x; + perform 1 into strict x; + perform with x as (select 1) select * from x; + perform table onecol; + perform values (1); +end +$$; diff --git a/crates/squawk_parser/tests/data/plpgsql/err/raise_stmt.sql b/crates/squawk_parser/tests/data/plpgsql/err/raise_stmt.sql new file mode 100644 index 00000000..89531d7f --- /dev/null +++ b/crates/squawk_parser/tests/data/plpgsql/err/raise_stmt.sql @@ -0,0 +1,9 @@ +do $$ +begin + raise notice 'x' using bogus = 1; + raise notice 'x' using errcode; + raise notice 'x' using errcode = ; + raise sqlstate; + raise exception; +end +$$; diff --git a/crates/squawk_parser/tests/data/plpgsql/err/return_stmt.sql b/crates/squawk_parser/tests/data/plpgsql/err/return_stmt.sql new file mode 100644 index 00000000..7ed7ef8c --- /dev/null +++ b/crates/squawk_parser/tests/data/plpgsql/err/return_stmt.sql @@ -0,0 +1,9 @@ +do $$ +begin + return next[1]; + return query; + return query execute; + return query execute 'select 1' using; + return 1 +end +$$; diff --git a/crates/squawk_parser/tests/data/plpgsql/err/transaction_stmt.sql b/crates/squawk_parser/tests/data/plpgsql/err/transaction_stmt.sql new file mode 100644 index 00000000..36cbe4b3 --- /dev/null +++ b/crates/squawk_parser/tests/data/plpgsql/err/transaction_stmt.sql @@ -0,0 +1,9 @@ +do $$ +begin + commit work; + commit prepared 'x'; + rollback to savepoint s; + commit and; + commit chain; +end +$$; diff --git a/crates/squawk_parser/tests/data/plpgsql/ok/assert_stmt.sql b/crates/squawk_parser/tests/data/plpgsql/ok/assert_stmt.sql new file mode 100644 index 00000000..f71af202 --- /dev/null +++ b/crates/squawk_parser/tests/data/plpgsql/ok/assert_stmt.sql @@ -0,0 +1,10 @@ +do $$ +declare + assert int := 1; +begin + assert true; + assert 1 = 1, 'boom'; + assert (select count(*) from onecol) > 0, 'empty: ' || 'x'; + assert := 2; +end +$$; diff --git a/crates/squawk_parser/tests/data/plpgsql/ok/assign_stmt.sql b/crates/squawk_parser/tests/data/plpgsql/ok/assign_stmt.sql new file mode 100644 index 00000000..7bea1484 --- /dev/null +++ b/crates/squawk_parser/tests/data/plpgsql/ok/assign_stmt.sql @@ -0,0 +1,19 @@ +do $$ +declare + a int[]; + r record; +begin + x := 1; + x = 1; + a[1] := 2; + a[1:2] := array[1,2]; + a[1:2].i := array[11,12]; + r.f1 := 3; + r.c1[1].i := 11; + $1 := 4; + values := 5; + perform := 6; + x := case when r.f1 = 1 then 'a' else 'b' end; + x := (select max(f1) from onecol); +end +$$; diff --git a/crates/squawk_parser/tests/data/plpgsql/ok/call_stmt.sql b/crates/squawk_parser/tests/data/plpgsql/ok/call_stmt.sql new file mode 100644 index 00000000..dc035111 --- /dev/null +++ b/crates/squawk_parser/tests/data/plpgsql/ok/call_stmt.sql @@ -0,0 +1,10 @@ +do $$ +declare + call int := 1; +begin + call transaction_test1(); + call p1(1, 2); + call p2(a => 1, b := 2); + call := 2; +end +$$; diff --git a/crates/squawk_parser/tests/data/plpgsql/ok/comp_option.sql b/crates/squawk_parser/tests/data/plpgsql/ok/comp_option.sql new file mode 100644 index 00000000..c00627c9 --- /dev/null +++ b/crates/squawk_parser/tests/data/plpgsql/ok/comp_option.sql @@ -0,0 +1,42 @@ +do $$ +#option dump +begin end $$; + +do $$ +#variable_conflict error +begin end $$; + +do $$ +#variable_conflict use_variable +begin end $$; + +do $$ +#variable_conflict use_column +begin end $$; + +do $$ +#print_strict_params on +begin end $$; + +do $$ +#print_strict_params off +begin end $$; + +-- unquoted values are down-cased, so these are the same option +do $$ +#print_strict_params ON +begin end $$; + +do $$ +#print_strict_params "on" +begin end $$; + +-- whitespace after the `#`, and more than one option, are both fine +do $$ # option dump +#variable_conflict use_variable +#print_strict_params on +#variable_conflict use_column +begin end $$; + +-- no newline required before the block +do $$ #option dump begin end $$; diff --git a/crates/squawk_parser/tests/data/plpgsql/ok/do_stmt.sql b/crates/squawk_parser/tests/data/plpgsql/ok/do_stmt.sql new file mode 100644 index 00000000..bbfea178 --- /dev/null +++ b/crates/squawk_parser/tests/data/plpgsql/ok/do_stmt.sql @@ -0,0 +1,7 @@ +do $$ +begin + do $q$ begin null; end $q$; + do language plpgsql $q$ begin null; end $q$; + do $q$ begin null; end $q$ language plpgsql; +end +$$; diff --git a/crates/squawk_parser/tests/data/plpgsql/ok/perform_stmt.sql b/crates/squawk_parser/tests/data/plpgsql/ok/perform_stmt.sql new file mode 100644 index 00000000..c464ebe3 --- /dev/null +++ b/crates/squawk_parser/tests/data/plpgsql/ok/perform_stmt.sql @@ -0,0 +1,14 @@ +do $$ +declare + perform int := 1; +begin + perform 1; + perform; + perform distinct f1 from onecol; + perform * from onecol order by 1 limit 1; + perform 1 union select 2; + perform (select max(f1) from onecol); + perform f1 from onecol where f1 > 0 group by f1 having count(*) > 1; + perform := 2; +end +$$; diff --git a/crates/squawk_parser/tests/data/plpgsql/ok/raise_stmt.sql b/crates/squawk_parser/tests/data/plpgsql/ok/raise_stmt.sql new file mode 100644 index 00000000..1c8a95f9 --- /dev/null +++ b/crates/squawk_parser/tests/data/plpgsql/ok/raise_stmt.sql @@ -0,0 +1,22 @@ +do $$ +declare + raise int := 1; +begin + raise; + raise notice 'plain'; + raise notice 'fmt % %', 1, 2; + raise 'no level'; + raise exception 'boom' using errcode := '22012'; + raise warning using message = 'm', detail = 'd', hint = 'h'; + raise exception using column = 'c', constraint = 'k', datatype = 'd', + table = 't', schema = 's'; + raise division_by_zero; + raise sqlstate '22012'; + raise debug 'dbg'; + raise log 'lg'; + raise info 'nf'; + raise notice E'esc'; + raise using errcode = '22012'; + raise := 2; +end +$$; diff --git a/crates/squawk_parser/tests/data/plpgsql/ok/return_stmt.sql b/crates/squawk_parser/tests/data/plpgsql/ok/return_stmt.sql new file mode 100644 index 00000000..183f08b3 --- /dev/null +++ b/crates/squawk_parser/tests/data/plpgsql/ok/return_stmt.sql @@ -0,0 +1,20 @@ +do $$ +declare + return int := 1; + next rec; +begin + return; + return 1; + return (select max(f1) from onecol); + return next; + return next 1; + return next.x; + return query select 1; + return query values (1), (2); + return query insert into t values (1) returning a; + return query execute 'select 1'; + return query execute 'select $1' using 7; + return query execute 'select $1, $2' using 7, 8; + return := 2; +end +$$; diff --git a/crates/squawk_parser/tests/data/plpgsql/ok/transaction_stmt.sql b/crates/squawk_parser/tests/data/plpgsql/ok/transaction_stmt.sql new file mode 100644 index 00000000..0d3d8511 --- /dev/null +++ b/crates/squawk_parser/tests/data/plpgsql/ok/transaction_stmt.sql @@ -0,0 +1,15 @@ +do $$ +declare + commit int := 1; + rollback int := 1; +begin + commit; + commit and chain; + commit and no chain; + rollback; + rollback and chain; + rollback and no chain; + commit := 2; + rollback := 3; +end +$$; diff --git a/crates/squawk_syntax/src/ast/generated/nodes.rs b/crates/squawk_syntax/src/ast/generated/nodes.rs index a1741fdd..e05ba888 100644 --- a/crates/squawk_syntax/src/ast/generated/nodes.rs +++ b/crates/squawk_syntax/src/ast/generated/nodes.rs @@ -19414,6 +19414,10 @@ impl Plpgsql { pub fn block(&self) -> Option { support::child(&self.syntax) } + #[inline] + pub fn comp_options(&self) -> AstChildren { + support::children(&self.syntax) + } } #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -19450,6 +19454,55 @@ impl PlpgsqlAliasTarget { } } +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct PlpgsqlAssertStmt { + pub(crate) syntax: SyntaxNode, +} +impl PlpgsqlAssertStmt { + #[inline] + pub fn semicolon_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::SEMICOLON) + } + #[inline] + pub fn assert_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::ASSERT_KW) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct PlpgsqlAssignStmt { + pub(crate) syntax: SyntaxNode, +} +impl PlpgsqlAssignStmt { + #[inline] + pub fn colon_eq_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::COLON_EQ) + } + #[inline] + pub fn semicolon_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::SEMICOLON) + } + #[inline] + pub fn eq_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::EQ) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct PlpgsqlAssignTarget { + pub(crate) syntax: SyntaxNode, +} +impl PlpgsqlAssignTarget { + #[inline] + pub fn accessors(&self) -> AstChildren { + support::children(&self.syntax) + } + #[inline] + pub fn name(&self) -> Option { + support::child(&self.syntax) + } +} + #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct PlpgsqlBlock { pub(crate) syntax: SyntaxNode, @@ -19500,6 +19553,21 @@ impl PlpgsqlBody { } } +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct PlpgsqlCallStmt { + pub(crate) syntax: SyntaxNode, +} +impl PlpgsqlCallStmt { + #[inline] + pub fn call(&self) -> Option { + support::child(&self.syntax) + } + #[inline] + pub fn semicolon_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::SEMICOLON) + } +} + #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct PlpgsqlCaseStmt { pub(crate) syntax: SyntaxNode, @@ -19554,6 +19622,90 @@ impl PlpgsqlCaseWhen { } } +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct PlpgsqlCommitStmt { + pub(crate) syntax: SyntaxNode, +} +impl PlpgsqlCommitStmt { + #[inline] + pub fn chain_clause(&self) -> Option { + support::child(&self.syntax) + } + #[inline] + pub fn semicolon_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::SEMICOLON) + } + #[inline] + pub fn commit_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::COMMIT_KW) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct PlpgsqlCompOptionDump { + pub(crate) syntax: SyntaxNode, +} +impl PlpgsqlCompOptionDump { + #[inline] + pub fn pound_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::POUND) + } + #[inline] + pub fn dump_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::DUMP_KW) + } + #[inline] + pub fn option_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::OPTION_KW) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct PlpgsqlCompOptionPrintStrictParams { + pub(crate) syntax: SyntaxNode, +} +impl PlpgsqlCompOptionPrintStrictParams { + #[inline] + pub fn option_value(&self) -> Option { + support::child(&self.syntax) + } + #[inline] + pub fn pound_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::POUND) + } + #[inline] + pub fn print_strict_params_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::PRINT_STRICT_PARAMS_KW) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct PlpgsqlCompOptionVariableConflict { + pub(crate) syntax: SyntaxNode, +} +impl PlpgsqlCompOptionVariableConflict { + #[inline] + pub fn pound_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::POUND) + } + #[inline] + pub fn error_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::ERROR_KW) + } + #[inline] + pub fn use_column_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::USE_COLUMN_KW) + } + #[inline] + pub fn use_variable_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::USE_VARIABLE_KW) + } + #[inline] + pub fn variable_conflict_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::VARIABLE_CONFLICT_KW) + } +} + #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct PlpgsqlCondition { pub(crate) syntax: SyntaxNode, @@ -19684,6 +19836,21 @@ impl PlpgsqlDeclareSection { } } +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct PlpgsqlDoStmt { + pub(crate) syntax: SyntaxNode, +} +impl PlpgsqlDoStmt { + #[inline] + pub fn do_(&self) -> Option { + support::child(&self.syntax) + } + #[inline] + pub fn semicolon_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::SEMICOLON) + } +} + #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct PlpgsqlElseClause { pub(crate) syntax: SyntaxNode, @@ -19951,6 +20118,21 @@ impl PlpgsqlNullStmt { } } +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct PlpgsqlOptionValue { + pub(crate) syntax: SyntaxNode, +} +impl PlpgsqlOptionValue { + #[inline] + pub fn positional_param_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::POSITIONAL_PARAM) + } + #[inline] + pub fn ident_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::IDENT) + } +} + #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct PlpgsqlPercentRowtype { pub(crate) syntax: SyntaxNode, @@ -19978,6 +20160,458 @@ impl PlpgsqlPercentRowtype { } } +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct PlpgsqlPerformStmt { + pub(crate) syntax: SyntaxNode, +} +impl PlpgsqlPerformStmt { + #[inline] + pub fn compound_select(&self) -> Option { + support::child(&self.syntax) + } + #[inline] + pub fn select(&self) -> Option