From 880585947ab05049ab1826156f150953640325d7 Mon Sep 17 00:00:00 2001 From: Luis Neves Date: Wed, 10 Jun 2026 16:43:41 +0100 Subject: [PATCH] fix: splitter now supports explain --- crates/pgls_statement_splitter/src/lib.rs | 35 +++++++++++++++++++ .../src/splitter/common.rs | 3 +- .../src/splitter/dml.rs | 31 ++++++++++++++++ 3 files changed, 68 insertions(+), 1 deletion(-) diff --git a/crates/pgls_statement_splitter/src/lib.rs b/crates/pgls_statement_splitter/src/lib.rs index 6f1ba02d0..3c8329956 100644 --- a/crates/pgls_statement_splitter/src/lib.rs +++ b/crates/pgls_statement_splitter/src/lib.rs @@ -241,6 +241,41 @@ END;", } } + #[test] + fn explain() { + let stmts = vec![ + "EXPLAIN SELECT 1;", + "EXPLAIN ANALYZE SELECT 1;", + "EXPLAIN ANALYSE SELECT 1;", + "EXPLAIN VERBOSE SELECT 1;", + "EXPLAIN ANALYZE VERBOSE SELECT 1;", + "EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM my_table;", + "EXPLAIN (FORMAT JSON) SELECT * FROM my_table WHERE id = 1;", + "EXPLAIN INSERT INTO my_table (id) VALUES (1);", + "EXPLAIN ANALYZE UPDATE my_table SET col = 1 WHERE id = 2;", + "EXPLAIN DELETE FROM my_table WHERE id = 1;", + "EXPLAIN WITH cte AS (SELECT 1 AS x) SELECT * FROM cte;", + "EXPLAIN CREATE TABLE my_copy AS SELECT * FROM my_table;", + "explain analyze select 1;", + ]; + + for stmt in stmts { + Tester::from(stmt).expect_statements(vec![stmt]); + } + } + + #[test] + fn explain_between_statements() { + Tester::from("EXPLAIN SELECT 1;\nSELECT 2;") + .expect_statements(vec!["EXPLAIN SELECT 1;", "SELECT 2;"]); + + Tester::from("explain select 1\n\nselect 2") + .expect_statements(vec!["explain select 1", "select 2"]); + + Tester::from("explain select 1\nselect 2") + .expect_statements(vec!["explain select 1", "select 2"]); + } + #[test] fn revoke() { Tester::from("revoke delete on table \"public\".\"voice_call\" from \"anon\";") diff --git a/crates/pgls_statement_splitter/src/splitter/common.rs b/crates/pgls_statement_splitter/src/splitter/common.rs index df8b09760..21619d75c 100644 --- a/crates/pgls_statement_splitter/src/splitter/common.rs +++ b/crates/pgls_statement_splitter/src/splitter/common.rs @@ -7,7 +7,7 @@ use super::{ Splitter, data::at_statement_start, ddl::{alter, create}, - dml::{cte, delete, insert, select, update}, + dml::{cte, delete, explain, insert, select, update}, }; #[derive(Debug)] @@ -58,6 +58,7 @@ pub(crate) fn statement(p: &mut Splitter) -> SplitterResult { SyntaxKind::DELETE_KW => delete(p), SyntaxKind::CREATE_KW => create(p), SyntaxKind::ALTER_KW => alter(p), + SyntaxKind::EXPLAIN_KW => explain(p), _ => unknown(p, &[]), }; diff --git a/crates/pgls_statement_splitter/src/splitter/dml.rs b/crates/pgls_statement_splitter/src/splitter/dml.rs index 9af68593a..f48811b20 100644 --- a/crates/pgls_statement_splitter/src/splitter/dml.rs +++ b/crates/pgls_statement_splitter/src/splitter/dml.rs @@ -5,6 +5,7 @@ use crate::splitter::common::SplitterResult; use super::{ Splitter, common::{parenthesis, unknown}, + ddl::create, }; pub(crate) fn cte(p: &mut Splitter) -> SplitterResult { @@ -64,3 +65,33 @@ pub(crate) fn delete(p: &mut Splitter) -> SplitterResult { unknown(p, &[]) } + +/// `EXPLAIN [ ANALYZE ] [ VERBOSE ] statement` and +/// `EXPLAIN ( option [, ...] ) statement` +/// +/// The explained statement must not be treated as the start of a new +/// statement, so we consume the EXPLAIN prefix and delegate to the splitter +/// function of the wrapped statement. +pub(crate) fn explain(p: &mut Splitter) -> SplitterResult { + p.expect(SyntaxKind::EXPLAIN_KW)?; + + p.eat(SyntaxKind::ANALYZE_KW)?; + p.eat(SyntaxKind::ANALYSE_KW)?; + p.eat(SyntaxKind::VERBOSE_KW)?; + + if p.current() == SyntaxKind::L_PAREN { + parenthesis(p)?; + } + + match p.current() { + SyntaxKind::WITH_KW => cte(p), + SyntaxKind::SELECT_KW => select(p), + SyntaxKind::INSERT_KW => insert(p), + SyntaxKind::UPDATE_KW => update(p), + SyntaxKind::DELETE_KW => delete(p), + // EXPLAIN CREATE TABLE ... AS / CREATE MATERIALIZED VIEW ... AS + SyntaxKind::CREATE_KW => create(p), + // VALUES, EXECUTE, DECLARE, MERGE + _ => unknown(p, &[]), + } +}