From 1742a360533aa7d93ea4b328616157eec21b16d3 Mon Sep 17 00:00:00 2001 From: minleejae Date: Fri, 11 Sep 2026 15:02:38 +0900 Subject: [PATCH] Structure SQL Server routine operations and table return types --- .../sf/jsqlparser/parser/feature/Feature.java | 2 + .../statement/CreateFunctionalStatement.java | 94 ++++++++-- .../statement/StatementVisitorAdapter.java | 7 + .../create/function/FunctionReturnType.java | 98 ++++++++++ .../util/deparser/StatementDeParser.java | 3 +- .../validation/feature/SqlServerVersion.java | 1 + .../validator/StatementValidator.java | 21 ++- .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 100 ++++++++++ src/site/sphinx/usage.rst | 20 ++ .../statement/SqlServerRoutineTest.java | 177 ++++++++++++++++++ 10 files changed, 502 insertions(+), 21 deletions(-) create mode 100644 src/main/java/net/sf/jsqlparser/statement/create/function/FunctionReturnType.java create mode 100644 src/test/java/net/sf/jsqlparser/statement/SqlServerRoutineTest.java diff --git a/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java b/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java index 6d0492fe7c..b9cff34812 100644 --- a/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java +++ b/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java @@ -660,6 +660,8 @@ public enum Feature { * @see CreateFunctionalStatement */ functionalStatement, + + alterFunction, alterProcedure, createOrAlterRoutine, /** * SQL block starting with "BEGIN" and ends with "END" statement is allowed * diff --git a/src/main/java/net/sf/jsqlparser/statement/CreateFunctionalStatement.java b/src/main/java/net/sf/jsqlparser/statement/CreateFunctionalStatement.java index 536a2b68f2..0845742e9b 100644 --- a/src/main/java/net/sf/jsqlparser/statement/CreateFunctionalStatement.java +++ b/src/main/java/net/sf/jsqlparser/statement/CreateFunctionalStatement.java @@ -14,6 +14,9 @@ import java.util.Collections; import java.util.List; import java.util.Optional; +import java.util.function.Consumer; +import net.sf.jsqlparser.statement.create.function.FunctionReturnType; +import net.sf.jsqlparser.statement.create.table.TableElement; /** * A base for the declaration of function like statements @@ -23,6 +26,39 @@ public abstract class CreateFunctionalStatement implements Statement { private String kind; private boolean orReplace = false; + public enum Operation { + CREATE, ALTER, CREATE_OR_ALTER + } + + private Operation operation = Operation.CREATE; + private FunctionReturnType returnType; + private List routineBodyParts; + + public Operation getOperation() { + return operation; + } + + public void setOperation(Operation operation) { + this.operation = operation; + } + + public FunctionReturnType getReturnType() { + return returnType; + } + + public void setReturnType(FunctionReturnType returnType) { + this.returnType = returnType; + } + + public List getRoutineBodyParts() { + return routineBodyParts; + } + + public void setRoutineBodyParts(List parts) { + routineBodyParts = parts; + } + + private List functionDeclarationParts; protected CreateFunctionalStatement(String kind) { @@ -41,7 +77,9 @@ protected CreateFunctionalStatement(boolean orReplace, String kind, } /** - * @return the declaration parts after {@code CREATE FUNCTION|PROCEDURE} + * @return the declaration parts after {@code CREATE FUNCTION|PROCEDURE}. For a SQL Server + * function with a structured {@link #getReturnType()}, these are the name and parameter + * tokens before RETURNS; {@link #getRoutineBodyParts()} holds the remaining tokens. */ public List getFunctionDeclarationParts() { return functionDeclarationParts; @@ -66,22 +104,43 @@ public void setOrReplace(boolean orReplace) { * @return a whitespace appended String with the declaration parts with some minimal formatting. */ public String formatDeclaration() { - StringBuilder declaration = new StringBuilder(); - int currIndex = 0; - while (currIndex < functionDeclarationParts.size()) { - String token = functionDeclarationParts.get(currIndex); - declaration.append(token); - // if the next token is a ; don't put a space - if (currIndex + 1 < functionDeclarationParts.size()) { - // peek ahead just to format nicely - String nextToken = functionDeclarationParts.get(currIndex + 1); - if (!nextToken.equals(";")) { - declaration.append(" "); - } + StringBuilder builder = new StringBuilder(); + return appendDeclarationTo(builder, builder::append).toString(); + } + + private StringBuilder appendDeclarationTo(StringBuilder builder, + Consumer printer) { + appendTokens(builder, functionDeclarationParts); + if (returnType != null) { + builder.append(' '); + returnType.appendTo(builder, printer); + if (routineBodyParts != null && !routineBodyParts.isEmpty()) { + builder.append(' '); + appendTokens(builder, routineBodyParts); } - currIndex++; } - return declaration.toString(); + return builder; + } + + private static void appendTokens(StringBuilder builder, List tokens) { + if (tokens == null) { + return; + } + for (int i = 0; i < tokens.size(); i++) { + if (i > 0 && !";".equals(tokens.get(i))) { + builder.append(' '); + } + builder.append(tokens.get(i)); + } + } + + public StringBuilder appendTo(StringBuilder builder, Consumer printer) { + builder.append(operation.name().replace('_', ' ')).append(' '); + if (orReplace && operation == Operation.CREATE) { + builder.append("OR REPLACE "); + } + builder.append(kind).append(' '); + return appendDeclarationTo(builder, printer); } @Override @@ -91,9 +150,8 @@ public T accept(StatementVisitor statementVisitor, S context) { @Override public String toString() { - return "CREATE " - + (orReplace ? "OR REPLACE " : "") - + kind + " " + formatDeclaration(); + StringBuilder builder = new StringBuilder(); + return appendTo(builder, builder::append).toString(); } public CreateFunctionalStatement withFunctionDeclarationParts( diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java b/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java index ef5ed487f3..ddbfa5aae3 100644 --- a/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java +++ b/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java @@ -548,6 +548,13 @@ public T visit(AlterSequence alterSequence, S context) { @Override public T visit(CreateFunctionalStatement createFunctionalStatement, S context) { + if (createFunctionalStatement.getReturnType() != null + && createFunctionalStatement.getReturnType().getTableElements() != null) { + createFunctionalStatement.getReturnType().getTableElements() + .forEach(element -> TableDefinitionTraversal.visit(element, + expression -> expression.accept(expressionVisitor, context), + table -> table.accept(fromItemVisitor, context))); + } return null; } diff --git a/src/main/java/net/sf/jsqlparser/statement/create/function/FunctionReturnType.java b/src/main/java/net/sf/jsqlparser/statement/create/function/FunctionReturnType.java new file mode 100644 index 0000000000..128bd90307 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/function/FunctionReturnType.java @@ -0,0 +1,98 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2026 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.statement.create.function; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import net.sf.jsqlparser.statement.create.table.ColDataType; +import net.sf.jsqlparser.statement.create.table.TableElement; + +/** A SQL Server scalar, inline table or declared table return type. */ +public class FunctionReturnType implements Serializable { + private ColDataType dataType; + private boolean table; + private String tableVariable; + private List tableElements; + + public ColDataType getDataType() { + return dataType; + } + + public void setDataType(ColDataType dataType) { + this.dataType = dataType; + } + + public boolean isTable() { + return table; + } + + public void setTable(boolean table) { + this.table = table; + } + + public String getTableVariable() { + return tableVariable; + } + + public void setTableVariable(String variable) { + this.tableVariable = variable; + } + + /** Null for an inline return table; otherwise columns and constraints in source order. */ + public List getTableElements() { + return tableElements; + } + + public void setTableElements(List elements) { + this.tableElements = elements; + } + + public List getTableElements(Class type) { + List result = new ArrayList<>(); + if (tableElements != null) { + for (TableElement element : tableElements) { + if (type.isInstance(element)) { + result.add(type.cast(element)); + } + } + } + return result; + } + + public StringBuilder appendTo(StringBuilder builder, Consumer printer) { + builder.append("RETURNS "); + if (!table) { + return builder.append(dataType); + } + if (tableVariable != null) { + builder.append(tableVariable).append(' '); + } + builder.append("TABLE"); + if (tableElements != null) { + builder.append(" ("); + for (int i = 0; i < tableElements.size(); i++) { + if (i > 0) { + builder.append(", "); + } + printer.accept(tableElements.get(i)); + } + builder.append(')'); + } + return builder; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + return appendTo(builder, builder::append).toString(); + } +} diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java index 3bd785a8ce..46faa747af 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java @@ -491,7 +491,8 @@ public StringBuilder visit(AlterSequence alterSequence, S context) { @Override public StringBuilder visit(CreateFunctionalStatement createFunctionalStatement, S context) { - builder.append(createFunctionalStatement.toString()); + createFunctionalStatement.appendTo(builder, + new TableElementDeParser(builder, expressionDeParser)::deParse); return builder; } diff --git a/src/main/java/net/sf/jsqlparser/util/validation/feature/SqlServerVersion.java b/src/main/java/net/sf/jsqlparser/util/validation/feature/SqlServerVersion.java index 5e7f7c8adb..863fbc839c 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/feature/SqlServerVersion.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/feature/SqlServerVersion.java @@ -96,6 +96,7 @@ public enum SqlServerVersion implements Version { Feature.createTableFromSelect, // https://docs.microsoft.com/en-us/sql/t-sql/statements/create-function-transact-sql?view=sql-server-ver15 // https://docs.microsoft.com/en-us/sql/t-sql/statements/create-procedure-transact-sql?view=sql-server-ver15 Feature.functionalStatement, Feature.createProcedure, Feature.createFunction, + Feature.alterFunction, Feature.alterProcedure, Feature.createOrAlterRoutine, Feature.block, Feature.declare, Feature.tableVariable, // https://docs.microsoft.com/en-us/sql/t-sql/statements/create-schema-transact-sql?view=sql-server-ver15 diff --git a/src/main/java/net/sf/jsqlparser/util/validation/validator/StatementValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/StatementValidator.java index d4709c761d..eee332678c 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/validator/StatementValidator.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/StatementValidator.java @@ -378,10 +378,27 @@ public Void visit(AlterSequence alterSequence, S context) { @Override public Void visit(CreateFunctionalStatement createFunctionalStatement, S context) { validateFeature(Feature.functionalStatement); + if (createFunctionalStatement + .getOperation() == CreateFunctionalStatement.Operation.CREATE_OR_ALTER) { + validateFeature(Feature.createOrAlterRoutine); + } + if (createFunctionalStatement.getReturnType() != null + && createFunctionalStatement.getReturnType().getTableElements() != null) { + createFunctionalStatement.getReturnType().getTableElements() + .forEach(element -> net.sf.jsqlparser.util.TableDefinitionTraversal.visit( + element, + this::validateOptionalExpression, this::validateOptionalFromItem)); + } if (createFunctionalStatement instanceof CreateFunction) { - validateFeature(Feature.createFunction); + validateFeature(createFunctionalStatement + .getOperation() == CreateFunctionalStatement.Operation.ALTER + ? Feature.alterFunction + : Feature.createFunction); } else if (createFunctionalStatement instanceof CreateProcedure) { - validateFeature(Feature.createProcedure); + validateFeature(createFunctionalStatement + .getOperation() == CreateFunctionalStatement.Operation.ALTER + ? Feature.alterProcedure + : Feature.createProcedure); } return null; } diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index e89a86094c..040798f06c 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -2669,6 +2669,13 @@ Statement SingleStatement() : } { ( + LOOKAHEAD({ Dialect.SQLSERVER.name().equals(getAsString(Feature.dialect)) + && ((getToken(1).kind == K_ALTER && (getToken(2).kind == K_FUNCTION || getToken(2).kind == K_PROCEDURE)) + || (getToken(1).kind == K_CREATE && (getToken(2).kind == K_FUNCTION || getToken(2).kind == K_PROCEDURE + || (getToken(2).kind == K_OR && getToken(3).kind == K_ALTER + && (getToken(4).kind == K_FUNCTION || getToken(4).kind == K_PROCEDURE))))) }) + stm = SqlServerRoutine() + | LOOKAHEAD(3) ( [ LOOKAHEAD(2) with=WithList() ] ( @@ -16284,6 +16291,99 @@ CreateFunctionalStatement CreateFunctionStatement(boolean isUsingOrReplace): } } +CreateFunctionalStatement SqlServerRoutine(): +{ + CreateFunctionalStatement result; + CreateFunctionalStatement.Operation operation = CreateFunctionalStatement.Operation.CREATE; + List parts; + FunctionReturnType returnType; +} +{ + ( [ { operation = CreateFunctionalStatement.Operation.CREATE_OR_ALTER; } ] + | { operation = CreateFunctionalStatement.Operation.ALTER; } ) + ( + parts=captureSqlServerFunctionHeader() + returnType=SqlServerFunctionReturnType() + { result = new CreateFunction(parts); result.setReturnType(returnType); } + parts=captureSqlServerRoutineBody(false) { result.setRoutineBodyParts(parts); } + | parts=captureSqlServerRoutineBody(true) { result = new CreateProcedure(parts); } + ) + { result.setOperation(operation); return result; } +} + +FunctionReturnType SqlServerFunctionReturnType(): +{ + FunctionReturnType result = new FunctionReturnType(); + UserVariable variable; ColDataType dataType; + TableElement element; List elements = new ArrayList(); +} +{ + + ( + LOOKAHEAD({ getToken(1).kind == K_TABLE || getToken(1).kind == S_AT_IDENTIFIER }) + [ variable=UserVariable() { result.setTableVariable(variable.toString()); } ] + { result.setTable(true); } + [ "(" element=SqlServerReturnTableElement() { elements.add(element); } + ( "," element=SqlServerReturnTableElement() { elements.add(element); } )* ")" + { result.setTableElements(elements); } ] + { requireDdlSyntax(result.getTableVariable() == null || result.getTableElements() != null, + "A return table variable requires column definitions"); } + | dataType=ColDataType() { result.setDataType(dataType); } + ) + { return result; } +} + +TableElement SqlServerReturnTableElement(): +{ TableElement element; } +{ + ( LOOKAHEAD(3) element=CreateTableConstraint() | element=ColumnDefinition() ) + { return element; } +} + +JAVACODE +List captureSqlServerFunctionHeader() { + List parts = new ArrayList(); + int depth = 0; + while (true) { + Token next = getToken(1); + if (next.kind == K_RETURNS && depth == 0) break; + if (next.kind == EOF || next.kind == ST_SEMICOLON) { + throw new ParseException("A SQL Server function requires RETURNS"); + } + if ("(".equals(next.image)) depth++; + if (")".equals(next.image)) depth--; + if (depth < 0) throw new ParseException("Unbalanced function parameters"); + parts.add(getNextToken().image); + } + if (parts.size() < 3 || "(".equals(parts.get(0)) || !")".equals(parts.get(parts.size() - 1))) { + throw new ParseException("A SQL Server function requires a name and parameter parentheses"); + } + return parts; +} + +JAVACODE +List captureSqlServerRoutineBody(boolean procedure) { + List parts = new ArrayList(); + int depth = 0; + while (true) { + Token next = getToken(1); + // Procedure definitions occupy the rest of their batch, including statements + // after an END. Their opaque bodies may contain BEGIN TRANSACTION and TRY/CATCH. + if (next.kind == EOF || (!procedure && next.kind == ST_SEMICOLON && depth == 0)) break; + if (!procedure) { + if (next.kind == K_BEGIN || next.kind == K_CASE) { + depth++; + } else if (next.kind == K_END) { + if (--depth < 0) throw new ParseException("Unmatched END in routine"); + } + } + parts.add(getNextToken().image); + } + if (depth != 0) throw new ParseException("Unterminated SQL Server routine block"); + if (parts.isEmpty()) throw new ParseException("A routine body is required"); + return parts; +} + CreateSynonym CreateSynonym(boolean isUsingOrReplace): { CreateSynonym createSynonym = new CreateSynonym(); diff --git a/src/site/sphinx/usage.rst b/src/site/sphinx/usage.rst index 6d3995c0c8..846da72d69 100644 --- a/src/site/sphinx/usage.rst +++ b/src/site/sphinx/usage.rst @@ -872,3 +872,23 @@ References: `CREATE ROLE `_, `ALTER DEFAULT PRIVILEGES `_, `CREATE TRIGGER `_. + +SQL Server routine declarations +------------------------------- + +``Dialect.SQLSERVER`` uses a shared declaration path for ``CREATE``, ``ALTER`` and +``CREATE OR ALTER FUNCTION/PROCEDURE``. ``CreateFunctionalStatement.getOperation()`` +identifies the operation. For functions, ``getReturnType()`` exposes scalar types, +inline ``RETURNS TABLE``, and a return variable with ordered ``TableElement`` column +and constraint definitions. Table elements reuse the existing definition traversal +and deparser, including custom expression visitors. + +With a structured return type, ``getFunctionDeclarationParts()`` contains the name +and parameter tokens; ``getRoutineBodyParts()`` contains the following options and +body. These remain opaque tokens, so this does not implement a T-SQL body AST or +resolve tables used inside a routine. Other dialects retain the existing token-list +representation. New operations have separate validation capabilities. + +Parse procedure definitions one SQL Server batch at a time: a procedure consumes the +remaining batch, including SQL after an ``END``. Client-side ``GO`` batch splitting is +not performed by this routine declaration parser. diff --git a/src/test/java/net/sf/jsqlparser/statement/SqlServerRoutineTest.java b/src/test/java/net/sf/jsqlparser/statement/SqlServerRoutineTest.java new file mode 100644 index 0000000000..f1644d0e3a --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/SqlServerRoutineTest.java @@ -0,0 +1,177 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2026 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.statement; + +import static org.junit.jupiter.api.Assertions.*; +import java.util.ArrayList; +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.ExpressionVisitorAdapter; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.parser.feature.Feature; +import net.sf.jsqlparser.statement.create.function.CreateFunction; +import net.sf.jsqlparser.statement.create.table.ColumnDefinition; +import net.sf.jsqlparser.statement.create.table.CheckConstraint; +import net.sf.jsqlparser.statement.create.table.Index; +import net.sf.jsqlparser.statement.select.SelectVisitorAdapter; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.ExpressionDeParser; +import net.sf.jsqlparser.util.deparser.SelectDeParser; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import net.sf.jsqlparser.util.validation.Validation; +import net.sf.jsqlparser.util.validation.feature.FeaturesAllowed; +import net.sf.jsqlparser.util.validation.feature.SqlServerVersion; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class SqlServerRoutineTest { + private static CreateFunctionalStatement parse(String sql) throws Exception { + CreateFunctionalStatement statement = + (CreateFunctionalStatement) CCJSqlParserUtil.parse(sql, + p -> p.withDialect(Dialect.SQLSERVER)); + StringBuilder output = new StringBuilder(); + statement.accept(new StatementDeParser(output)); + assertEquals(statement.toString(), output.toString()); + assertEquals(statement.toString(), CCJSqlParserUtil.parse(output.toString(), + p -> p.withDialect(Dialect.SQLSERVER)).toString()); + return statement; + } + + @ParameterizedTest + @ValueSource(strings = {"CREATE", "ALTER", "CREATE OR ALTER"}) + void supportsIssue1978AndSharedOperations(String prefix) throws Exception { + CreateFunctionalStatement function = parse( + prefix + " FUNCTION getPayments() RETURNS TABLE AS RETURN SELECT * from Payments;"); + assertEquals(prefix.replace(' ', '_'), function.getOperation().name()); + assertTrue(function.getReturnType().isTable()); + assertNull(function.getReturnType().getTableElements()); + CreateFunctionalStatement procedure = parse(prefix + + " PROCEDURE SPPayment AS SET NOCOUNT ON; BEGIN SELECT * FROM Payments; END"); + assertEquals("PROCEDURE", procedure.getKind()); + assertNull(procedure.getReturnType()); + assertTrue(procedure.formatDeclaration().contains("NOCOUNT ON; BEGIN")); + assertTrue(procedure.getFeatures().modifiesSchema()); + assertThrows(UnsupportedOperationException.class, + () -> new TablesNamesFinder().getTables(procedure)); + } + + @Test + void exposesIssue715ReturnTableAndConstraintExpressions() throws Exception { + CreateFunctionalStatement statement = parse( + "CREATE OR ALTER FUNCTION dbo.f(@id int = 1) RETURNS @result TABLE (id int NOT NULL, amount decimal(10, 2), PRIMARY KEY (id), CHECK (amount > 0)) AS BEGIN INSERT INTO @result SELECT id, amount FROM payments; RETURN; END;"); + assertEquals("@result", statement.getReturnType().getTableVariable()); + assertEquals(4, statement.getReturnType().getTableElements().size()); + List columns = + statement.getReturnType().getTableElements(ColumnDefinition.class); + assertEquals(2, columns.size()); + assertEquals(2, statement.getReturnType().getTableElements(Index.class).size()); + assertEquals("id", columns.get(0).getColumnName()); + List visited = new ArrayList<>(); + ExpressionVisitorAdapter expressions = new ExpressionVisitorAdapter() { + @Override + public Void visit(LongValue value, S context) { + assertEquals("ctx", context); + visited.add(value.getValue()); + return null; + } + }; + statement.accept(new StatementVisitorAdapter<>(new SelectVisitorAdapter<>(expressions)), + "ctx"); + assertEquals(List.of(0L), visited); + columns.get(0).setColumnName("payment_id"); + statement.getReturnType().setTableVariable("@rows"); + assertTrue(statement.toString().contains("RETURNS @rows TABLE (payment_id int NOT NULL")); + CheckConstraint check = + statement.getReturnType().getTableElements(CheckConstraint.class).get(0); + check.setExpression(CCJSqlParserUtil.parseCondExpression("amount > 10")); + StringBuilder output = new StringBuilder(); + ExpressionDeParser deparser = new ExpressionDeParser() { + @Override + public StringBuilder visit(LongValue value, S context) { + return getBuilder().append(value.getValue() + 1); + } + }; + statement.accept(new StatementDeParser(deparser, new SelectDeParser(), output)); + assertTrue(output.toString().contains("CHECK (amount > 11)")); + parse(output.toString()); + } + + @ParameterizedTest + @ValueSource(strings = { + "CREATE FUNCTION dbo.f() RETURNS int AS BEGIN RETURN CASE WHEN 1 = 1 THEN 1 ELSE 0 END; END;", + "ALTER FUNCTION dbo.f() RETURNS @r TABLE (id int) AS BEGIN BEGIN INSERT INTO @r SELECT 1; END; RETURN; END;", + "CREATE FUNCTION dbo.f() RETURNS TABLE (id int) AS EXTERNAL NAME assembly.class.method;" + }) + void preservesScalarClrAndNestedBodies(String sql) throws Exception { + parse(sql); + } + + @Test + void retainsFollowingStatements() throws Exception { + for (String sql : List.of( + "ALTER FUNCTION f() RETURNS TABLE AS RETURN SELECT 1 AS id; SELECT 2;", + "ALTER FUNCTION f() RETURNS int AS BEGIN RETURN CASE WHEN 1=1 THEN 1 END; END; SELECT 2;")) { + assertEquals(2, CCJSqlParserUtil + .parseStatements(sql, p -> p.withDialect(Dialect.SQLSERVER)).size()); + } + } + + @Test + void preservesTheEntireProcedureBatch() throws Exception { + String sql = "ALTER PROCEDURE p AS BEGIN TRY BEGIN TRANSACTION; SELECT 1; COMMIT; END TRY; " + + "BEGIN CATCH ROLLBACK; END CATCH; SELECT 2;"; + CreateFunctionalStatement statement = parse(sql); + assertTrue(statement.formatDeclaration().endsWith("END CATCH; SELECT 2;")); + assertEquals(1, CCJSqlParserUtil.parseStatements(sql, p -> p.withDialect(Dialect.SQLSERVER)) + .size()); + } + + @Test + void keepsDefaultAndOtherDialectsUnchanged() throws Exception { + CreateFunction old = (CreateFunction) CCJSqlParserUtil + .parse("CREATE FUNCTION f() RETURNS @r TABLE (id int) AS BEGIN RETURN; END;"); + assertNull(old.getReturnType()); + String pg = "CREATE OR REPLACE FUNCTION f() RETURNS int AS $$ SELECT 1; $$ LANGUAGE sql;"; + assertTrue(CCJSqlParserUtil.parse(pg, p -> p.withDialect(Dialect.POSTGRESQL)).toString() + .startsWith("CREATE OR REPLACE FUNCTION")); + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil + .parse("CREATE OR ALTER FUNCTION f() RETURNS TABLE AS RETURN SELECT 1")); + } + + @ParameterizedTest + @ValueSource(strings = {"CREATE FUNCTION f() AS RETURN 1", + "ALTER FUNCTION f() RETURNS @r TABLE AS BEGIN RETURN; END", + "CREATE FUNCTION f() RETURNS @r TABLE () AS BEGIN RETURN; END", + "ALTER FUNCTION f() RETURNS int AS BEGIN RETURN 1;", + "ALTER FUNCTION f() RETURNS TABLE"}) + void rejectsIncompleteDeclarations(String sql) { + assertThrows(JSQLParserException.class, () -> parse(sql)); + } + + @Test + void validatesOperationCapabilities() { + for (String prefix : List.of("CREATE", "ALTER", "CREATE OR ALTER")) { + assertTrue(new Validation( + CCJSqlParserUtil.newParser("SELECT 1").withDialect(Dialect.SQLSERVER) + .getConfiguration(), + List.of(SqlServerVersion.V2019), + prefix + " FUNCTION f() RETURNS TABLE AS RETURN SELECT 1").validate() + .isEmpty()); + } + assertFalse(new Validation( + CCJSqlParserUtil.newParser("SELECT 1").withDialect(Dialect.SQLSERVER) + .getConfiguration(), + List.of(new FeaturesAllowed(Feature.functionalStatement, Feature.createFunction)), + "ALTER FUNCTION f() RETURNS TABLE AS RETURN SELECT 1").validate().isEmpty()); + } +}