diff --git a/src/main/java/net/sf/jsqlparser/statement/AssertStatement.java b/src/main/java/net/sf/jsqlparser/statement/AssertStatement.java index 0879faf68..1e5a74298 100644 --- a/src/main/java/net/sf/jsqlparser/statement/AssertStatement.java +++ b/src/main/java/net/sf/jsqlparser/statement/AssertStatement.java @@ -12,6 +12,8 @@ import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.expression.StringValue; +import java.util.function.Consumer; + /** * BigQuery's {@code ASSERT expression [AS description]}, which fails the script when the expression * does not evaluate to {@code TRUE}. @@ -62,9 +64,16 @@ public AssertStatement withDescription(StringValue description) { } public StringBuilder appendTo(StringBuilder builder) { - builder.append("ASSERT ").append(expression); + return appendTo(builder, builder::append); + } + + /** Shares statement syntax with deparsers while visiting both expression and description. */ + public StringBuilder appendTo(StringBuilder builder, Consumer expressionRenderer) { + builder.append("ASSERT "); + expressionRenderer.accept(expression); if (description != null) { - builder.append(" AS ").append(description); + builder.append(" AS "); + expressionRenderer.accept(description); } return builder; } diff --git a/src/main/java/net/sf/jsqlparser/statement/create/macro/CreateMacro.java b/src/main/java/net/sf/jsqlparser/statement/create/macro/CreateMacro.java index c08b866bb..11aa3f5b1 100644 --- a/src/main/java/net/sf/jsqlparser/statement/create/macro/CreateMacro.java +++ b/src/main/java/net/sf/jsqlparser/statement/create/macro/CreateMacro.java @@ -16,6 +16,7 @@ import java.io.Serializable; import java.util.List; +import java.util.function.Consumer; /** * DuckDB's {@code CREATE [OR REPLACE] [TEMPORARY] MACRO name (parameters) AS [TABLE] body}, a named @@ -115,6 +116,12 @@ public boolean isTable() { } public StringBuilder appendTo(StringBuilder builder) { + return appendTo(builder, builder::append, builder::append); + } + + /** Shares punctuation while exposing parameter defaults and both kinds of macro body. */ + public StringBuilder appendTo(StringBuilder builder, Consumer expressionRenderer, + Consumer selectRenderer) { builder.append("EXPORT DATA"); if (connectionName != null) { builder.append(" WITH CONNECTION ").append(connectionName); } if (options != null) { - builder.append(" OPTIONS (").append(options).append(")"); + builder.append(" OPTIONS ("); + for (int i = 0; i < options.size(); i++) { + if (i > 0) { + builder.append(", "); + } + expressionRenderer.accept(options.get(i)); + } + builder.append(")"); } - builder.append(" AS ").append(select); + builder.append(" AS "); + selectRenderer.accept(select); return builder; } 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 ffd5fe900..4ec423e27 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java @@ -570,7 +570,8 @@ public StringBuilder visit(PragmaStatement pragmaStatement, S context) { @Override public StringBuilder visit(AssertStatement assertStatement, S context) { - assertStatement.appendTo(builder); + assertStatement.appendTo(builder, + expression -> expression.accept(expressionDeParser, context)); return builder; } @@ -582,7 +583,9 @@ public StringBuilder visit(ExtensionStatement extensionStatement, S context) @Override public StringBuilder visit(ExportDataStatement exportDataStatement, S context) { - exportDataStatement.appendTo(builder); + exportDataStatement.appendTo(builder, + expression -> expression.accept(expressionDeParser, context), + select -> select.accept((SelectVisitor) selectDeParser, context)); return builder; } @@ -630,7 +633,9 @@ public StringBuilder visit(CopyStatement copyStatement, S context) { @Override public StringBuilder visit(CreateMacro createMacro, S context) { - createMacro.appendTo(builder); + createMacro.appendTo(builder, + expression -> expression.accept(expressionDeParser, context), + select -> select.accept((SelectVisitor) selectDeParser, context)); return builder; } diff --git a/src/test/java/net/sf/jsqlparser/util/deparser/StructuredStatementVisitorTest.java b/src/test/java/net/sf/jsqlparser/util/deparser/StructuredStatementVisitorTest.java new file mode 100644 index 000000000..b4f4232e1 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/util/deparser/StructuredStatementVisitorTest.java @@ -0,0 +1,95 @@ +/*- + * #%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.util.deparser; + +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statement; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class StructuredStatementVisitorTest { + @ParameterizedTest + @ValueSource(strings = { + "CREATE MACRO m(x, y := 7) AS x + y + 8", + "CREATE MACRO m() AS TABLE WITH c AS (SELECT 7 AS id) SELECT id FROM c UNION ALL SELECT 8", + "ASSERT (1 = 1) AS 'expected'", + "ASSERT EXISTS (SELECT 7 FROM users WHERE id = 8)", + "EXPORT DATA OPTIONS(uri='gs://example/*.csv',format='CSV') AS SELECT 7 FROM users", + "EXPORT DATA OPTIONS(uri='gs://example/*.csv',format='CSV') AS WITH c AS (SELECT 7 AS id) SELECT id FROM c UNION ALL SELECT 8" + }) + void defaultDeparserMatchesStatementOutput(String sql) throws Exception { + Statement statement = CCJSqlParserUtil.parse(sql); + StringBuilder output = new StringBuilder(); + statement.accept(new StatementDeParser(output), null); + assertEquals(statement.toString(), output.toString()); + assertEquals(output.toString(), CCJSqlParserUtil.parse(output.toString()).toString()); + } + + @Test + void visitsMacroDefaultsAndScalarBody() throws Exception { + assertVisited("CREATE MACRO m(x, y := 7) AS x + y + 8", List.of("7", "8"), + "CREATE MACRO m (x, y := 107) AS x + y + 108"); + } + + @Test + void visitsMacroQueriesAndCtes() throws Exception { + assertVisited( + "CREATE MACRO m() AS TABLE WITH c AS (SELECT 7 AS id) SELECT id FROM c UNION ALL SELECT 8", + List.of("7", "8"), + "CREATE MACRO m () AS TABLE WITH c AS (SELECT 107 AS id) SELECT id FROM c UNION ALL SELECT 108"); + } + + @Test + void visitsAssertionAndDescription() throws Exception { + assertVisited("ASSERT (1 = 1) AS 'expected'", List.of("1", "1", "expected"), + "ASSERT (101 = 101) AS 'changed'"); + } + + @Test + void visitsExportOptionsAndQuery() throws Exception { + assertVisited( + "EXPORT DATA OPTIONS(uri='gs://example/*.csv',format='CSV') AS SELECT 7 FROM users WHERE id = 8", + List.of("gs://example/*.csv", "CSV", "7", "8"), + "EXPORT DATA OPTIONS (uri = 'changed', format = 'changed') AS SELECT 107 FROM users WHERE id = 108"); + } + + private void assertVisited(String sql, List expectedVisits, String expectedOutput) + throws Exception { + List visited = new ArrayList<>(); + ExpressionDeParser expressions = new ExpressionDeParser() { + @Override + public StringBuilder visit(LongValue value, S context) { + visited.add(value.getStringValue()); + return getBuilder().append(value.getValue() + 100); + } + + @Override + public StringBuilder visit(StringValue value, S context) { + visited.add(value.getValue()); + return getBuilder().append("'changed'"); + } + }; + StringBuilder output = new StringBuilder(); + Statement statement = CCJSqlParserUtil.parse(sql); + statement.accept(new StatementDeParser(expressions, new SelectDeParser(), output), + "context"); + assertEquals(expectedVisits, visited); + assertEquals(expectedOutput, output.toString()); + assertEquals(expectedOutput, CCJSqlParserUtil.parse(output.toString()).toString()); + } +}