From b31d30c07774c2086e60833779e109f0132fa847 Mon Sep 17 00:00:00 2001 From: Filipe Guerreiro Date: Fri, 13 Feb 2026 16:44:46 +0900 Subject: [PATCH 1/3] Add SETOF support for PostgreSQL function return types --- src/ast/data_type.rs | 6 ++++++ src/ast/spans.rs | 27 --------------------------- src/keywords.rs | 1 + src/parser/mod.rs | 4 ++++ tests/sqlparser_postgres.rs | 24 ++++++++++++++++++++++++ 5 files changed, 35 insertions(+), 27 deletions(-) diff --git a/src/ast/data_type.rs b/src/ast/data_type.rs index 285eec5054..4182bc620d 100644 --- a/src/ast/data_type.rs +++ b/src/ast/data_type.rs @@ -61,6 +61,11 @@ pub enum DataType { /// Table columns. columns: Vec, }, + /// SETOF type modifier for [PostgreSQL] function return types, + /// e.g. `CREATE FUNCTION ... RETURNS SETOF text`. + /// + /// [PostgreSQL]: https://www.postgresql.org/docs/current/sql-createfunction.html + SetOf(Box), /// Fixed-length character type, e.g. CHARACTER(10). Character(Option), /// Fixed-length char type, e.g. CHAR(10). @@ -808,6 +813,7 @@ impl fmt::Display for DataType { DataType::NamedTable { name, columns } => { write!(f, "{} TABLE ({})", name, display_comma_separated(columns)) } + DataType::SetOf(inner) => write!(f, "SETOF {inner}"), DataType::GeometricType(kind) => write!(f, "{kind}"), DataType::TsVector => write!(f, "TSVECTOR"), DataType::TsQuery => write!(f, "TSQUERY"), diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 74f19e831e..1c5ecc9ab0 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -632,33 +632,6 @@ impl Spanned for TableConstraint { } } -impl Spanned for PartitionBoundValue { - fn span(&self) -> Span { - match self { - PartitionBoundValue::Expr(expr) => expr.span(), - // MINVALUE and MAXVALUE are keywords without tracked spans - PartitionBoundValue::MinValue => Span::empty(), - PartitionBoundValue::MaxValue => Span::empty(), - } - } -} - -impl Spanned for ForValues { - fn span(&self) -> Span { - match self { - ForValues::In(exprs) => union_spans(exprs.iter().map(|e| e.span())), - ForValues::From { from, to } => union_spans( - from.iter() - .map(|v| v.span()) - .chain(to.iter().map(|v| v.span())), - ), - // WITH (MODULUS n, REMAINDER r) - u64 values have no spans - ForValues::With { .. } => Span::empty(), - ForValues::Default => Span::empty(), - } - } -} - impl Spanned for CreateIndex { fn span(&self) -> Span { let CreateIndex { diff --git a/src/keywords.rs b/src/keywords.rs index cc2b9e9dd0..07d02f3b2c 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -933,6 +933,7 @@ define_keywords!( SESSION_USER, SET, SETERROR, + SETOF, SETS, SETTINGS, SHARE, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 6c9314d951..17a0d7c913 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -12064,6 +12064,10 @@ impl<'a> Parser<'a> { let field_defs = self.parse_click_house_tuple_def()?; Ok(DataType::Tuple(field_defs)) } + Keyword::SETOF => { + let inner = self.parse_data_type()?; + Ok(DataType::SetOf(Box::new(inner))) + } Keyword::TRIGGER => Ok(DataType::Trigger), Keyword::ANY if self.peek_keyword(Keyword::TYPE) => { let _ = self.parse_keyword(Keyword::TYPE); diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index d79e2b833e..6dc6f213ee 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -4603,6 +4603,30 @@ fn parse_create_function_detailed() { ); } +#[test] +fn parse_create_function_returns_setof() { + pg_and_generic().verified_stmt( + "CREATE FUNCTION get_users() RETURNS SETOF TEXT LANGUAGE sql AS 'SELECT name FROM users'", + ); + pg_and_generic().verified_stmt( + "CREATE FUNCTION get_ids() RETURNS SETOF INTEGER LANGUAGE sql AS 'SELECT id FROM users'", + ); + pg_and_generic().verified_stmt( + r#"CREATE FUNCTION get_all() RETURNS SETOF my_schema."MyType" LANGUAGE sql AS 'SELECT * FROM t'"#, + ); + pg_and_generic().verified_stmt( + "CREATE FUNCTION get_rows() RETURNS SETOF RECORD LANGUAGE sql AS 'SELECT * FROM t'", + ); + + let sql = "CREATE FUNCTION get_names() RETURNS SETOF TEXT LANGUAGE sql AS 'SELECT name FROM t'"; + match pg_and_generic().verified_stmt(sql) { + Statement::CreateFunction(CreateFunction { return_type, .. }) => { + assert_eq!(return_type, Some(DataType::SetOf(Box::new(DataType::Text)))); + } + _ => panic!("Expected CreateFunction"), + } +} + #[test] fn parse_create_function_with_security() { let sql = From 11a37c673cfdf68598afb4fd4a252c9270ed7539 Mon Sep 17 00:00:00 2001 From: Filipe Guerreiro Date: Fri, 20 Feb 2026 11:49:21 +0900 Subject: [PATCH 2/3] refactor(ast): move SETOF from DataType into FunctionReturnType enum SETOF is a PostgreSQL-specific modifier for function return types, not a standalone data type. Introduce FunctionReturnType enum with DataType and SetOf variants, replacing DataType::SetOf. Addresses review feedback on #2217. --- src/ast/data_type.rs | 6 ------ src/ast/ddl.rs | 24 +++++++++++++++++++++++- src/ast/mod.rs | 19 ++++++++++--------- src/ast/spans.rs | 25 +++++++++++++++++++++++++ src/parser/mod.rs | 27 +++++++++++++++++---------- tests/sqlparser_bigquery.rs | 2 +- tests/sqlparser_mssql.rs | 4 ++-- tests/sqlparser_postgres.rs | 20 ++++++++++---------- 8 files changed, 88 insertions(+), 39 deletions(-) diff --git a/src/ast/data_type.rs b/src/ast/data_type.rs index 4182bc620d..285eec5054 100644 --- a/src/ast/data_type.rs +++ b/src/ast/data_type.rs @@ -61,11 +61,6 @@ pub enum DataType { /// Table columns. columns: Vec, }, - /// SETOF type modifier for [PostgreSQL] function return types, - /// e.g. `CREATE FUNCTION ... RETURNS SETOF text`. - /// - /// [PostgreSQL]: https://www.postgresql.org/docs/current/sql-createfunction.html - SetOf(Box), /// Fixed-length character type, e.g. CHARACTER(10). Character(Option), /// Fixed-length char type, e.g. CHAR(10). @@ -813,7 +808,6 @@ impl fmt::Display for DataType { DataType::NamedTable { name, columns } => { write!(f, "{} TABLE ({})", name, display_comma_separated(columns)) } - DataType::SetOf(inner) => write!(f, "SETOF {inner}"), DataType::GeometricType(kind) => write!(f, "{kind}"), DataType::TsVector => write!(f, "TSVECTOR"), DataType::TsQuery => write!(f, "TSQUERY"), diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 0c4f93e647..3d41c0cabc 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -3466,6 +3466,28 @@ impl fmt::Display for CreateDomain { } } +/// The return type of a `CREATE FUNCTION` statement. +/// +/// [PostgreSQL](https://www.postgresql.org/docs/current/sql-createfunction.html) +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum FunctionReturnType { + /// RETURNS + DataType(DataType), + /// RETURNS SETOF + SetOf(DataType), +} + +impl fmt::Display for FunctionReturnType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + FunctionReturnType::DataType(data_type) => write!(f, "{data_type}"), + FunctionReturnType::SetOf(data_type) => write!(f, "SETOF {data_type}"), + } + } +} + #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] @@ -3486,7 +3508,7 @@ pub struct CreateFunction { /// List of arguments for the function. pub args: Option>, /// The return type of the function. - pub return_type: Option, + pub return_type: Option, /// The expression that defines the function. /// /// Examples: diff --git a/src/ast/mod.rs b/src/ast/mod.rs index d534b300b4..8691cecdd8 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -71,15 +71,16 @@ pub use self::ddl::{ CreateIndex, CreateOperator, CreateOperatorClass, CreateOperatorFamily, CreatePolicy, CreatePolicyCommand, CreatePolicyType, CreateTable, CreateTrigger, CreateView, Deduplicate, DeferrableInitial, DropBehavior, DropExtension, DropFunction, DropOperator, DropOperatorClass, - DropOperatorFamily, DropOperatorSignature, DropPolicy, DropTrigger, ForValues, GeneratedAs, - GeneratedExpressionMode, IdentityParameters, IdentityProperty, IdentityPropertyFormatKind, - IdentityPropertyKind, IdentityPropertyOrder, IndexColumn, IndexOption, IndexType, - KeyOrIndexDisplay, Msck, NullsDistinctOption, OperatorArgTypes, OperatorClassItem, - OperatorFamilyDropItem, OperatorFamilyItem, OperatorOption, OperatorPurpose, Owner, Partition, - PartitionBoundValue, ProcedureParam, ReferentialAction, RenameTableNameKind, ReplicaIdentity, - TagsColumnOption, TriggerObjectKind, Truncate, UserDefinedTypeCompositeAttributeDef, - UserDefinedTypeInternalLength, UserDefinedTypeRangeOption, UserDefinedTypeRepresentation, - UserDefinedTypeSqlDefinitionOption, UserDefinedTypeStorage, ViewColumnDef, + DropOperatorFamily, DropOperatorSignature, DropPolicy, DropTrigger, ForValues, + FunctionReturnType, GeneratedAs, GeneratedExpressionMode, IdentityParameters, IdentityProperty, + IdentityPropertyFormatKind, IdentityPropertyKind, IdentityPropertyOrder, IndexColumn, + IndexOption, IndexType, KeyOrIndexDisplay, Msck, NullsDistinctOption, OperatorArgTypes, + OperatorClassItem, OperatorFamilyDropItem, OperatorFamilyItem, OperatorOption, OperatorPurpose, + Owner, Partition, PartitionBoundValue, ProcedureParam, ReferentialAction, RenameTableNameKind, + ReplicaIdentity, TagsColumnOption, TriggerObjectKind, Truncate, + UserDefinedTypeCompositeAttributeDef, UserDefinedTypeInternalLength, + UserDefinedTypeRangeOption, UserDefinedTypeRepresentation, UserDefinedTypeSqlDefinitionOption, + UserDefinedTypeStorage, ViewColumnDef, }; pub use self::dml::{ Delete, Insert, Merge, MergeAction, MergeClause, MergeClauseKind, MergeInsertExpr, diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 1c5ecc9ab0..07698fa19f 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -597,6 +597,31 @@ impl Spanned for CreateTable { } } +impl Spanned for PartitionBoundValue { + fn span(&self) -> Span { + match self { + PartitionBoundValue::Expr(expr) => expr.span(), + PartitionBoundValue::MinValue => Span::empty(), + PartitionBoundValue::MaxValue => Span::empty(), + } + } +} + +impl Spanned for ForValues { + fn span(&self) -> Span { + match self { + ForValues::In(exprs) => union_spans(exprs.iter().map(|e| e.span())), + ForValues::From { from, to } => union_spans( + from.iter() + .map(|v| v.span()) + .chain(to.iter().map(|v| v.span())), + ), + ForValues::With { .. } => Span::empty(), + ForValues::Default => Span::empty(), + } + } +} + impl Spanned for ColumnDef { fn span(&self) -> Span { let ColumnDef { diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 17a0d7c913..675c1de9e3 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5545,7 +5545,7 @@ impl<'a> Parser<'a> { self.expect_token(&Token::RParen)?; let return_type = if self.parse_keyword(Keyword::RETURNS) { - Some(self.parse_data_type()?) + Some(self.parse_function_return_type()?) } else { None }; @@ -5724,7 +5724,7 @@ impl<'a> Parser<'a> { let (name, args) = self.parse_create_function_name_and_params()?; let return_type = if self.parse_keyword(Keyword::RETURNS) { - Some(self.parse_data_type()?) + Some(self.parse_function_return_type()?) } else { None }; @@ -5827,11 +5827,11 @@ impl<'a> Parser<'a> { }) })?; - let return_type = if return_table.is_some() { - return_table - } else { - Some(self.parse_data_type()?) + let data_type = match return_table { + Some(table_type) => table_type, + None => self.parse_data_type()?, }; + let return_type = Some(FunctionReturnType::DataType(data_type)); let _ = self.parse_keyword(Keyword::AS); @@ -5883,6 +5883,17 @@ impl<'a> Parser<'a> { }) } + /// Parse a [`FunctionReturnType`] after the `RETURNS` keyword. + /// + /// Handles `RETURNS SETOF ` and plain `RETURNS `. + fn parse_function_return_type(&mut self) -> Result { + if self.parse_keyword(Keyword::SETOF) { + Ok(FunctionReturnType::SetOf(self.parse_data_type()?)) + } else { + Ok(FunctionReturnType::DataType(self.parse_data_type()?)) + } + } + fn parse_create_function_name_and_params( &mut self, ) -> Result<(ObjectName, Vec), ParserError> { @@ -12064,10 +12075,6 @@ impl<'a> Parser<'a> { let field_defs = self.parse_click_house_tuple_def()?; Ok(DataType::Tuple(field_defs)) } - Keyword::SETOF => { - let inner = self.parse_data_type()?; - Ok(DataType::SetOf(Box::new(inner))) - } Keyword::TRIGGER => Ok(DataType::Trigger), Keyword::ANY if self.peek_keyword(Keyword::TYPE) => { let _ = self.parse_keyword(Keyword::TYPE); diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index cf843ea2b3..28f306f356 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -2279,7 +2279,7 @@ fn test_bigquery_create_function() { Ident::new("myfunction"), ]), args: Some(vec![OperateFunctionArg::with_name("x", DataType::Float64),]), - return_type: Some(DataType::Float64), + return_type: Some(FunctionReturnType::DataType(DataType::Float64)), function_body: Some(CreateFunctionBody::AsAfterOptions(Expr::Value( number("42").with_empty_span() ))), diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index d7d11ba669..72b60b5116 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -255,7 +255,7 @@ fn parse_create_function() { default_expr: None, }, ]), - return_type: Some(DataType::Int(None)), + return_type: Some(FunctionReturnType::DataType(DataType::Int(None))), function_body: Some(CreateFunctionBody::AsBeginEnd(BeginEndStatements { begin_token: AttachedToken::empty(), statements: vec![Statement::Return(ReturnStatement { @@ -430,7 +430,7 @@ fn parse_create_function_parameter_default_values() { data_type: DataType::Int(None), default_expr: Some(Expr::Value((number("42")).with_empty_span())), },]), - return_type: Some(DataType::Int(None)), + return_type: Some(FunctionReturnType::DataType(DataType::Int(None))), function_body: Some(CreateFunctionBody::AsBeginEnd(BeginEndStatements { begin_token: AttachedToken::empty(), statements: vec![Statement::Return(ReturnStatement { diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 6dc6f213ee..338eb451eb 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -4346,7 +4346,7 @@ $$"#; DataType::Varchar(None), ), ]), - return_type: Some(DataType::Boolean), + return_type: Some(FunctionReturnType::DataType(DataType::Boolean)), language: Some("plpgsql".into()), behavior: None, called_on_null: None, @@ -4389,7 +4389,7 @@ $$"#; DataType::Int(None) ) ]), - return_type: Some(DataType::Boolean), + return_type: Some(FunctionReturnType::DataType(DataType::Boolean)), language: Some("plpgsql".into()), behavior: None, called_on_null: None, @@ -4436,7 +4436,7 @@ $$"#; DataType::Int(None) ), ]), - return_type: Some(DataType::Boolean), + return_type: Some(FunctionReturnType::DataType(DataType::Boolean)), language: Some("plpgsql".into()), behavior: None, called_on_null: None, @@ -4483,7 +4483,7 @@ $$"#; DataType::Int(None) ), ]), - return_type: Some(DataType::Boolean), + return_type: Some(FunctionReturnType::DataType(DataType::Boolean)), language: Some("plpgsql".into()), behavior: None, called_on_null: None, @@ -4523,7 +4523,7 @@ $$"#; ), OperateFunctionArg::with_name("b", DataType::Varchar(None)), ]), - return_type: Some(DataType::Boolean), + return_type: Some(FunctionReturnType::DataType(DataType::Boolean)), language: Some("plpgsql".into()), behavior: None, called_on_null: None, @@ -4566,7 +4566,7 @@ fn parse_create_function() { OperateFunctionArg::unnamed(DataType::Integer(None)), OperateFunctionArg::unnamed(DataType::Integer(None)), ]), - return_type: Some(DataType::Integer(None)), + return_type: Some(FunctionReturnType::DataType(DataType::Integer(None))), language: Some("SQL".into()), behavior: Some(FunctionBehavior::Immutable), called_on_null: Some(FunctionCalledOnNull::Strict), @@ -4621,7 +4621,7 @@ fn parse_create_function_returns_setof() { let sql = "CREATE FUNCTION get_names() RETURNS SETOF TEXT LANGUAGE sql AS 'SELECT name FROM t'"; match pg_and_generic().verified_stmt(sql) { Statement::CreateFunction(CreateFunction { return_type, .. }) => { - assert_eq!(return_type, Some(DataType::SetOf(Box::new(DataType::Text)))); + assert_eq!(return_type, Some(FunctionReturnType::SetOf(DataType::Text))); } _ => panic!("Expected CreateFunction"), } @@ -4702,10 +4702,10 @@ fn parse_create_function_c_with_module_pathname() { "input", DataType::Custom(ObjectName::from(vec![Ident::new("cstring")]), vec![]), ),]), - return_type: Some(DataType::Custom( + return_type: Some(FunctionReturnType::DataType(DataType::Custom( ObjectName::from(vec![Ident::new("cas")]), vec![] - )), + ))), language: Some("c".into()), behavior: Some(FunctionBehavior::Immutable), called_on_null: None, @@ -6399,7 +6399,7 @@ fn parse_trigger_related_functions() { if_not_exists: false, name: ObjectName::from(vec![Ident::new("emp_stamp")]), args: Some(vec![]), - return_type: Some(DataType::Trigger), + return_type: Some(FunctionReturnType::DataType(DataType::Trigger)), function_body: Some( CreateFunctionBody::AsBeforeOptions { body: Expr::Value(( From e5a095861f689ebf64288b18ee5b7ec41bce7694 Mon Sep 17 00:00:00 2001 From: Filipe Guerreiro Date: Fri, 20 Feb 2026 12:35:45 +0900 Subject: [PATCH 3/3] fix(docs): escape angle brackets in FunctionReturnType doc comments --- src/ast/ddl.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 3d41c0cabc..316cb7b41c 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -3473,9 +3473,9 @@ impl fmt::Display for CreateDomain { #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum FunctionReturnType { - /// RETURNS + /// `RETURNS ` DataType(DataType), - /// RETURNS SETOF + /// `RETURNS SETOF ` SetOf(DataType), }