From e66c2bb8911102ffd378f687f1840a9ec5f418ad Mon Sep 17 00:00:00 2001 From: Sean Linsley Date: Sun, 7 Dec 2025 11:38:00 -0500 Subject: [PATCH 1/2] Support parsing complex queries with deeply nested ASTs --- Cargo.lock | 12 +++---- Cargo.toml | 4 +-- README.md | 10 ++++++ build.rs | 1 + src/protobuf.rs | 83 ++++++++++++++++++++++---------------------- src/query.rs | 5 ++- tests/parse_tests.rs | 14 ++++---- 7 files changed, 71 insertions(+), 58 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6a52b62..73eee31 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -448,8 +448,7 @@ dependencies = [ [[package]] name = "prost" version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" +source = "git+https://github.com/pganalyze/prost?branch=recursion-limit-macro#86b943956f36697406a9b4eb4200caa8894b81ef" dependencies = [ "bytes", "prost-derive", @@ -458,8 +457,7 @@ dependencies = [ [[package]] name = "prost-build" version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf" +source = "git+https://github.com/pganalyze/prost?branch=recursion-limit-macro#86b943956f36697406a9b4eb4200caa8894b81ef" dependencies = [ "heck", "itertools", @@ -478,8 +476,7 @@ dependencies = [ [[package]] name = "prost-derive" version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" +source = "git+https://github.com/pganalyze/prost?branch=recursion-limit-macro#86b943956f36697406a9b4eb4200caa8894b81ef" dependencies = [ "anyhow", "itertools", @@ -491,8 +488,7 @@ dependencies = [ [[package]] name = "prost-types" version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16" +source = "git+https://github.com/pganalyze/prost?branch=recursion-limit-macro#86b943956f36697406a9b4eb4200caa8894b81ef" dependencies = [ "prost", ] diff --git a/Cargo.toml b/Cargo.toml index be3340f..ef54efd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,7 @@ include = [ [dependencies] itertools = "0.10.3" -prost = "0.13.5" +prost = { git = "https://github.com/pganalyze/prost", branch = "recursion-limit-macro" } serde = { version = "1.0.139", features = ["derive"] } serde_json = "1.0.82" thiserror = "1.0.31" @@ -28,7 +28,7 @@ thiserror = "1.0.31" [build-dependencies] bindgen = "0.66.1" clippy = { version = "0.0.302", optional = true } -prost-build = "0.13.5" +prost-build = { git = "https://github.com/pganalyze/prost", branch = "recursion-limit-macro" } fs_extra = "1.2.0" cc = "1.0.83" glob = "0.3.1" diff --git a/README.md b/README.md index 177c18d..61fbb59 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,16 @@ let result = pg_query::parse(query).unwrap(); assert_eq!(result.truncate(32).unwrap(), "INSERT INTO x (...) VALUES (...)"); ``` +## Caveats + +When parsing very complex queries you may run into a stack overflow. This can be worked around by using a thread with a custom stack size ([stdlib](https://doc.rust-lang.org/std/thread/index.html#stack-size), [tokio](https://docs.rs/tokio/latest/tokio/runtime/struct.Builder.html#method.thread_stack_size)), or using the stacker crate to resize the main thread's stack: + +```rust +stacker::grow(20 * 1024 * 1024, || pg_query::parse(query)) +``` + +However, a sufficiently complex query could still run into a stack overflow after you increase the stack size. With some work it may be possible to add an adapter API to the prost crate in order to dynamically increase the stack size as needed like [serde_stacker](https://crates.io/crates/serde_stacker) does (if anyone wants to take that on). + ## Credits Thanks to [Paul Mason](https://github.com/paupino) for his work on [pg_parse](https://github.com/paupino/pg_parse) that this crate is based on. diff --git a/build.rs b/build.rs index a722ad0..0a1f851 100644 --- a/build.rs +++ b/build.rs @@ -75,6 +75,7 @@ fn main() -> Result<(), Box> { env::set_var("OUT_DIR", &src_dir); let mut prost_build = prost_build::Config::new(); + prost_build.recursion_limit("ParseResult", 1000); prost_build.type_attribute(".", "#[derive(serde::Serialize)]"); prost_build.compile_protos(&[&out_protobuf_path.join("pg_query").with_extension("proto")], &[&out_protobuf_path])?; diff --git a/src/protobuf.rs b/src/protobuf.rs index a020f83..5a55d61 100644 --- a/src/protobuf.rs +++ b/src/protobuf.rs @@ -1,6 +1,7 @@ // This file is @generated by prost-build. #[derive(serde::Serialize)] #[derive(Clone, PartialEq, ::prost::Message)] +#[prost(recursion_limit = 1000)] pub struct ParseResult { #[prost(int32, tag = "1")] pub version: i32, @@ -576,34 +577,34 @@ pub mod node { } } #[derive(serde::Serialize)] -#[derive(Clone, Copy, PartialEq, ::prost::Message)] +#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] pub struct Integer { /// machine integer #[prost(int32, tag = "1")] pub ival: i32, } #[derive(serde::Serialize)] -#[derive(Clone, PartialEq, ::prost::Message)] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct Float { /// string #[prost(string, tag = "1")] pub fval: ::prost::alloc::string::String, } #[derive(serde::Serialize)] -#[derive(Clone, Copy, PartialEq, ::prost::Message)] +#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] pub struct Boolean { #[prost(bool, tag = "1")] pub boolval: bool, } #[derive(serde::Serialize)] -#[derive(Clone, PartialEq, ::prost::Message)] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct String { /// string #[prost(string, tag = "1")] pub sval: ::prost::alloc::string::String, } #[derive(serde::Serialize)] -#[derive(Clone, PartialEq, ::prost::Message)] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct BitString { /// string #[prost(string, tag = "1")] @@ -628,7 +629,7 @@ pub struct IntList { pub items: ::prost::alloc::vec::Vec, } #[derive(serde::Serialize)] -#[derive(Clone, PartialEq, ::prost::Message)] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct AConst { #[prost(bool, tag = "10")] pub isnull: bool, @@ -640,7 +641,7 @@ pub struct AConst { /// Nested message and enum types in `A_Const`. pub mod a_const { #[derive(serde::Serialize)] - #[derive(Clone, PartialEq, ::prost::Oneof)] + #[derive(Clone, PartialEq, Eq, Hash, ::prost::Oneof)] pub enum Val { #[prost(message, tag = "1")] Ival(super::Integer), @@ -1379,7 +1380,7 @@ pub struct XmlExpr { pub location: i32, } #[derive(serde::Serialize)] -#[derive(Clone, Copy, PartialEq, ::prost::Message)] +#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] pub struct JsonFormat { #[prost(enumeration = "JsonFormatType", tag = "1")] pub format_type: i32, @@ -1389,7 +1390,7 @@ pub struct JsonFormat { pub location: i32, } #[derive(serde::Serialize)] -#[derive(Clone, Copy, PartialEq, ::prost::Message)] +#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] pub struct JsonReturning { #[prost(message, optional, tag = "1")] pub format: ::core::option::Option, @@ -1495,7 +1496,7 @@ pub struct JsonExpr { pub location: i32, } #[derive(serde::Serialize)] -#[derive(Clone, PartialEq, ::prost::Message)] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct JsonTablePath { #[prost(string, tag = "1")] pub name: ::prost::alloc::string::String, @@ -1669,7 +1670,7 @@ pub struct TargetEntry { pub resjunk: bool, } #[derive(serde::Serialize)] -#[derive(Clone, Copy, PartialEq, ::prost::Message)] +#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] pub struct RangeTblRef { #[prost(int32, tag = "1")] pub rtindex: i32, @@ -1841,7 +1842,7 @@ pub struct ColumnRef { pub location: i32, } #[derive(serde::Serialize)] -#[derive(Clone, Copy, PartialEq, ::prost::Message)] +#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] pub struct ParamRef { #[prost(int32, tag = "1")] pub number: i32, @@ -1883,7 +1884,7 @@ pub struct CollateClause { pub location: i32, } #[derive(serde::Serialize)] -#[derive(Clone, PartialEq, ::prost::Message)] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct RoleSpec { #[prost(enumeration = "RoleSpecType", tag = "1")] pub roletype: i32, @@ -1919,7 +1920,7 @@ pub struct FuncCall { pub location: i32, } #[derive(serde::Serialize)] -#[derive(Clone, Copy, PartialEq, ::prost::Message)] +#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] pub struct AStar {} #[derive(serde::Serialize)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -2244,7 +2245,7 @@ pub struct PartitionRangeDatum { pub location: i32, } #[derive(serde::Serialize)] -#[derive(Clone, Copy, PartialEq, ::prost::Message)] +#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] pub struct SinglePartitionSpec {} #[derive(serde::Serialize)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -2327,7 +2328,7 @@ pub struct RangeTblEntry { pub security_quals: ::prost::alloc::vec::Vec, } #[derive(serde::Serialize)] -#[derive(Clone, PartialEq, ::prost::Message)] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct RtePermissionInfo { #[prost(uint32, tag = "1")] pub relid: u32, @@ -2387,7 +2388,7 @@ pub struct WithCheckOption { pub cascaded: bool, } #[derive(serde::Serialize)] -#[derive(Clone, Copy, PartialEq, ::prost::Message)] +#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] pub struct SortGroupClause { #[prost(uint32, tag = "1")] pub tle_sort_group_ref: u32, @@ -2443,7 +2444,7 @@ pub struct WindowClause { pub copied_order: bool, } #[derive(serde::Serialize)] -#[derive(Clone, Copy, PartialEq, ::prost::Message)] +#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] pub struct RowMarkClause { #[prost(uint32, tag = "1")] pub rti: u32, @@ -2573,7 +2574,7 @@ pub struct MergeWhenClause { pub values: ::prost::alloc::vec::Vec, } #[derive(serde::Serialize)] -#[derive(Clone, PartialEq, ::prost::Message)] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct TriggerTransition { #[prost(string, tag = "1")] pub name: ::prost::alloc::string::String, @@ -2987,7 +2988,7 @@ pub struct AlterTableStmt { pub missing_ok: bool, } #[derive(serde::Serialize)] -#[derive(Clone, PartialEq, ::prost::Message)] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct ReplicaIdentityStmt { #[prost(string, tag = "1")] pub identity_type: ::prost::alloc::string::String, @@ -3135,7 +3136,7 @@ pub struct VariableSetStmt { pub is_local: bool, } #[derive(serde::Serialize)] -#[derive(Clone, PartialEq, ::prost::Message)] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct VariableShowStmt { #[prost(string, tag = "1")] pub name: ::prost::alloc::string::String, @@ -3247,7 +3248,7 @@ pub struct CreateTableSpaceStmt { pub options: ::prost::alloc::vec::Vec, } #[derive(serde::Serialize)] -#[derive(Clone, PartialEq, ::prost::Message)] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct DropTableSpaceStmt { #[prost(string, tag = "1")] pub tablespacename: ::prost::alloc::string::String, @@ -3389,7 +3390,7 @@ pub struct AlterUserMappingStmt { pub options: ::prost::alloc::vec::Vec, } #[derive(serde::Serialize)] -#[derive(Clone, PartialEq, ::prost::Message)] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct DropUserMappingStmt { #[prost(message, optional, tag = "1")] pub user: ::core::option::Option, @@ -3503,7 +3504,7 @@ pub struct CreateEventTrigStmt { pub funcname: ::prost::alloc::vec::Vec, } #[derive(serde::Serialize)] -#[derive(Clone, PartialEq, ::prost::Message)] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct AlterEventTrigStmt { #[prost(string, tag = "1")] pub trigname: ::prost::alloc::string::String, @@ -3729,13 +3730,13 @@ pub struct DeclareCursorStmt { pub query: ::core::option::Option<::prost::alloc::boxed::Box>, } #[derive(serde::Serialize)] -#[derive(Clone, PartialEq, ::prost::Message)] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct ClosePortalStmt { #[prost(string, tag = "1")] pub portalname: ::prost::alloc::string::String, } #[derive(serde::Serialize)] -#[derive(Clone, PartialEq, ::prost::Message)] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct FetchStmt { #[prost(enumeration = "FetchDirection", tag = "1")] pub direction: i32, @@ -3881,7 +3882,7 @@ pub struct DoStmt { pub args: ::prost::alloc::vec::Vec, } #[derive(serde::Serialize)] -#[derive(Clone, PartialEq, ::prost::Message)] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct InlineCodeBlock { #[prost(string, tag = "1")] pub source_text: ::prost::alloc::string::String, @@ -3903,7 +3904,7 @@ pub struct CallStmt { pub outargs: ::prost::alloc::vec::Vec, } #[derive(serde::Serialize)] -#[derive(Clone, Copy, PartialEq, ::prost::Message)] +#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] pub struct CallContext { #[prost(bool, tag = "1")] pub atomic: bool, @@ -4003,7 +4004,7 @@ pub struct RuleStmt { pub replace: bool, } #[derive(serde::Serialize)] -#[derive(Clone, PartialEq, ::prost::Message)] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct NotifyStmt { #[prost(string, tag = "1")] pub conditionname: ::prost::alloc::string::String, @@ -4011,13 +4012,13 @@ pub struct NotifyStmt { pub payload: ::prost::alloc::string::String, } #[derive(serde::Serialize)] -#[derive(Clone, PartialEq, ::prost::Message)] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct ListenStmt { #[prost(string, tag = "1")] pub conditionname: ::prost::alloc::string::String, } #[derive(serde::Serialize)] -#[derive(Clone, PartialEq, ::prost::Message)] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct UnlistenStmt { #[prost(string, tag = "1")] pub conditionname: ::prost::alloc::string::String, @@ -4095,7 +4096,7 @@ pub struct ViewStmt { pub with_check_option: i32, } #[derive(serde::Serialize)] -#[derive(Clone, PartialEq, ::prost::Message)] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct LoadStmt { #[prost(string, tag = "1")] pub filename: ::prost::alloc::string::String, @@ -4117,7 +4118,7 @@ pub struct AlterDatabaseStmt { pub options: ::prost::alloc::vec::Vec, } #[derive(serde::Serialize)] -#[derive(Clone, PartialEq, ::prost::Message)] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct AlterDatabaseRefreshCollStmt { #[prost(string, tag = "1")] pub dbname: ::prost::alloc::string::String, @@ -4209,10 +4210,10 @@ pub struct RefreshMatViewStmt { pub relation: ::core::option::Option, } #[derive(serde::Serialize)] -#[derive(Clone, Copy, PartialEq, ::prost::Message)] +#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] pub struct CheckPointStmt {} #[derive(serde::Serialize)] -#[derive(Clone, Copy, PartialEq, ::prost::Message)] +#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] pub struct DiscardStmt { #[prost(enumeration = "DiscardMode", tag = "1")] pub target: i32, @@ -4308,7 +4309,7 @@ pub struct ExecuteStmt { pub params: ::prost::alloc::vec::Vec, } #[derive(serde::Serialize)] -#[derive(Clone, PartialEq, ::prost::Message)] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct DeallocateStmt { #[prost(string, tag = "1")] pub name: ::prost::alloc::string::String, @@ -4434,7 +4435,7 @@ pub struct AlterSubscriptionStmt { pub options: ::prost::alloc::vec::Vec, } #[derive(serde::Serialize)] -#[derive(Clone, PartialEq, ::prost::Message)] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct DropSubscriptionStmt { #[prost(string, tag = "1")] pub subname: ::prost::alloc::string::String, @@ -4444,7 +4445,7 @@ pub struct DropSubscriptionStmt { pub behavior: i32, } #[derive(serde::Serialize)] -#[derive(Clone, Copy, PartialEq, ::prost::Message)] +#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] pub struct ScanToken { #[prost(int32, tag = "1")] pub start: i32, @@ -4487,7 +4488,7 @@ pub struct SummaryResult { /// Nested message and enum types in `SummaryResult`. pub mod summary_result { #[derive(serde::Serialize)] - #[derive(Clone, PartialEq, ::prost::Message)] + #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct Table { #[prost(string, tag = "1")] pub name: ::prost::alloc::string::String, @@ -4499,7 +4500,7 @@ pub mod summary_result { pub context: i32, } #[derive(serde::Serialize)] - #[derive(Clone, PartialEq, ::prost::Message)] + #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct Function { #[prost(string, tag = "1")] pub name: ::prost::alloc::string::String, @@ -4512,7 +4513,7 @@ pub mod summary_result { pub context: i32, } #[derive(serde::Serialize)] - #[derive(Clone, PartialEq, ::prost::Message)] + #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct FilterColumn { /// optional #[prost(string, tag = "1")] diff --git a/src/query.rs b/src/query.rs index 287faba..563e2cd 100644 --- a/src/query.rs +++ b/src/query.rs @@ -37,7 +37,10 @@ pub fn parse(statement: &str) -> Result { } else { let data = unsafe { std::slice::from_raw_parts(result.parse_tree.data as *const u8, result.parse_tree.len as usize) }; let stderr = unsafe { CStr::from_ptr(result.stderr_buffer) }.to_string_lossy().to_string(); - protobuf::ParseResult::decode(data).map_err(Error::Decode).map(|result| ParseResult::new(result, stderr)) + match protobuf::ParseResult::decode(data) { + Ok(result) => Ok(ParseResult::new(result, stderr)), + Err(error) => Err(Error::Decode(error)), + } }; unsafe { pg_query_free_protobuf_parse_result(result) }; parse_result diff --git a/tests/parse_tests.rs b/tests/parse_tests.rs index c029690..3f178ef 100644 --- a/tests/parse_tests.rs +++ b/tests/parse_tests.rs @@ -4,6 +4,9 @@ #[cfg(test)] use itertools::sorted; +#[cfg(test)] +use std::thread::Builder; + use pg_query::{ parse, protobuf::{self, a_const::Val}, @@ -34,20 +37,19 @@ fn it_handles_errors() { fn it_serializes_as_json() { let result = parse("SELECT 1 FROM pg_class").unwrap(); let json = serde_json::to_string(&result.protobuf); - assert!(json.is_ok(), "Protobuf should be serializable: {json:?}"); } #[test] -fn it_handles_recursion_error() { +fn it_handles_recursion_without_error_1() { let query = "SELECT a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(a(b))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))"; - parse(query).err().unwrap(); - // TODO: unsure how to unwrap the private fields on a protobuf decode error - // assert_eq!(error, Error::Decode("recursion limit reached".into())); + let result = Builder::new().stack_size(20 * 1024 * 1024).spawn(move || parse(query)).unwrap().join().unwrap().unwrap(); + assert_eq!(result.tables().len(), 0); + assert_eq!(result.statement_types(), ["SelectStmt"]); } #[test] -fn it_handles_recursion_without_error() { +fn it_handles_recursion_without_error_2() { // The Ruby version of pg_query fails here because of Ruby protobuf limitations let query = r#"SELECT * FROM "t0" JOIN "t1" ON (1) JOIN "t2" ON (1) JOIN "t3" ON (1) JOIN "t4" ON (1) JOIN "t5" ON (1) From d5b0cdad7be00acbc4f86730af680898152334f0 Mon Sep 17 00:00:00 2001 From: Sean Linsley Date: Thu, 18 Dec 2025 08:43:26 -0500 Subject: [PATCH 2/2] Summary: Fix bugs and performance issues --- libpg_query | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libpg_query b/libpg_query index 03e2f43..693209e 160000 --- a/libpg_query +++ b/libpg_query @@ -1 +1 @@ -Subproject commit 03e2f436c999a1d22dbce439573e8cfabced5720 +Subproject commit 693209eb78c632bacc2a8195d21093b49627f94b