From b79f20dea117eedabf3b8913fcdaf1ca72fa9701 Mon Sep 17 00:00:00 2001 From: minleejae Date: Wed, 14 Oct 2026 12:42:46 +0900 Subject: [PATCH] fix: render single spaces in ALTER visibility, FORCE and row level security actions Rendered SQL had stray spaces in several ALTER TABLE actions: SET INVISIBLE, ALTER INDEX i INVISIBLE, a trailing space after FORCE and [NO] FORCE/ENABLE/DISABLE ROW LEVEL SECURITY, MODIFY ( c INT ), and ADD CONSTRAINT UNIQUE KEY uk (a). An index without a declaration printed a leading space, and argument-less JSON_ARRAY()/JSON_OBJECT() rendered as JSON_ARRAY( ) and JSON_OBJECT( ) with a trailing space. Separate these words with exactly one space and add no leading or trailing space. toString() and StatementDeParser stay identical. Signed-off-by: minleejae --- .../jsqlparser/expression/JsonFunction.java | 18 ++-- .../statement/alter/AlterExpression.java | 31 ++++--- .../statement/create/table/Index.java | 27 +++--- .../alter/DeparseWhitespaceTest.java | 93 ++++++++++++++++++- 4 files changed, 130 insertions(+), 39 deletions(-) diff --git a/src/main/java/net/sf/jsqlparser/expression/JsonFunction.java b/src/main/java/net/sf/jsqlparser/expression/JsonFunction.java index 03a3e3da0..7331ded7d 100644 --- a/src/main/java/net/sf/jsqlparser/expression/JsonFunction.java +++ b/src/main/java/net/sf/jsqlparser/expression/JsonFunction.java @@ -436,12 +436,11 @@ public StringBuilder appendObject(StringBuilder builder) { @SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.NPathComplexity"}) public StringBuilder appendObject(StringBuilder builder, Consumer expressionPrinter) { - builder.append("JSON_OBJECT( "); + builder.append("JSON_OBJECT("); + int start = builder.length(); int i = 0; for (JsonKeyValuePair keyValuePair : keyValuePairs) { - if (i > 0) { - builder.append(", "); - } + builder.append(i > 0 ? ", " : " "); keyValuePair.appendTo(builder, expressionPrinter); i++; } @@ -453,7 +452,8 @@ public StringBuilder appendObject(StringBuilder builder, appendUniqueKeys(builder); appendReturningClause(builder, true); - builder.append(" ) "); + // An empty argument list renders as JSON_OBJECT() + builder.append(builder.length() > start ? " )" : ")"); return builder; } @@ -495,20 +495,18 @@ public StringBuilder appendArray(StringBuilder builder) { @SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.NPathComplexity"}) public StringBuilder appendArray(StringBuilder builder, Consumer expressionPrinter) { - builder.append("JSON_ARRAY( "); + builder.append("JSON_ARRAY("); int i = 0; for (JsonFunctionExpression expr : expressions) { - if (i > 0) { - builder.append(", "); - } + builder.append(i > 0 ? ", " : " "); expr.appendTo(builder, expressionPrinter); i++; } appendOnNullType(builder); appendReturningClause(builder, true); - builder.append(") "); + builder.append(')'); return builder; } diff --git a/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpression.java b/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpression.java index 046e6bb23..944c6a96e 100644 --- a/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpression.java +++ b/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpression.java @@ -1304,8 +1304,7 @@ protected void toStringConstraintAlter(StringBuilder b) { } } else { b.append("ADD CONSTRAINT ").append(getConstraintType()).append(" ") - .append(getConstraintSymbol()) - .append(" "); + .append(getConstraintSymbol()); if (constraint instanceof KeyConstraint) { b.append(" ") .append(PlainSelect.getStringList( @@ -1519,9 +1518,9 @@ protected void toStringPartition(StringBuilder b) { b.append(partitions.get(0)).append(" WITH TABLE ") .append(exchangePartitionTableName); if (exchangePartitionWithValidation) { - b.append(" WITH VALIDATION "); + b.append(" WITH VALIDATION"); } else if (exchangePartitionWithoutValidation) { - b.append(" WITHOUT VALIDATION "); + b.append(" WITHOUT VALIDATION"); } break; case ANALYZE_PARTITION: @@ -1575,18 +1574,20 @@ public void appendColumnDefinitionsTo(StringBuilder b, Consumer expr "PMD.ExcessiveMethodLength"}) private void toStringGeneral(StringBuilder b, Consumer columnPrinter) { if (operation == AlterOperation.COMMENT_WITH_EQUAL_SIGN) { - b.append("COMMENT =").append(" "); + b.append("COMMENT ="); } else if (operation == AlterOperation.ENABLE_ROW_LEVEL_SECURITY) { - b.append("ENABLE ROW LEVEL SECURITY").append(" "); + b.append("ENABLE ROW LEVEL SECURITY"); } else if (operation == AlterOperation.DISABLE_ROW_LEVEL_SECURITY) { - b.append("DISABLE ROW LEVEL SECURITY").append(" "); + b.append("DISABLE ROW LEVEL SECURITY"); } else if (operation == AlterOperation.FORCE_ROW_LEVEL_SECURITY) { - b.append("FORCE ROW LEVEL SECURITY").append(" "); + b.append("FORCE ROW LEVEL SECURITY"); } else if (operation == AlterOperation.NO_FORCE_ROW_LEVEL_SECURITY) { - b.append("NO FORCE ROW LEVEL SECURITY").append(" "); + b.append("NO FORCE ROW LEVEL SECURITY"); } else { - b.append(operation).append(" "); + b.append(operation); } + int keywordEnd = b.length(); + b.append(' '); if (commentText != null) { if (columnName != null) { b.append(columnName).append(" COMMENT "); @@ -1624,7 +1625,7 @@ private void toStringGeneral(StringBuilder b, Consumer columnPri } } if (useBrackets && colDataTypeList.size() == 1) { - b.append(" ( "); + b.append('('); } for (int i = 0; i < colDataTypeList.size(); i++) { if (i > 0) { @@ -1633,7 +1634,7 @@ private void toStringGeneral(StringBuilder b, Consumer columnPri columnPrinter.accept(colDataTypeList.get(i)); } if (useBrackets && colDataTypeList.size() == 1) { - b.append(" ) "); + b.append(')'); } if (colDataTypeList.size() > 1) { b.append(")"); @@ -1658,6 +1659,10 @@ private void toStringGeneral(StringBuilder b, Consumer columnPri } else if (index != null) { b.append(index); } + if (b.length() == keywordEnd + 1) { + // a keyword without payload, such as FORCE, ends without a separator + b.setLength(keywordEnd); + } if (getConstraints() != null && !getConstraints().isEmpty()) { b.append(' ').append(PlainSelect.getStringList(constraints, false, false)); @@ -2125,7 +2130,7 @@ public boolean isVisible() { @Override public String toString() { - return columnName + " SET " + (visible ? " VISIBLE" : " INVISIBLE"); + return columnName + " SET " + (visible ? "VISIBLE" : "INVISIBLE"); } } diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/Index.java b/src/main/java/net/sf/jsqlparser/statement/create/table/Index.java index f0b1a1dea..7ac01b3af 100644 --- a/src/main/java/net/sf/jsqlparser/statement/create/table/Index.java +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/Index.java @@ -211,16 +211,16 @@ public String toString() { } public void appendTo(StringBuilder sql, Consumer expressionPrinter) { - appendDeclarationTo(sql); - appendColumnsAndSpecTo(sql, expressionPrinter); + int start = sql.length(); + appendDeclarationTo(sql, start); + appendColumnsAndSpecTo(sql, start, expressionPrinter); appendIndexOptionsTo(sql, expressionPrinter); if (tailParameters != null && !tailParameters.isEmpty()) { sql.append(' ').append(PlainSelect.getStringList(tailParameters, false, false)); } } - private void appendDeclarationTo(StringBuilder sql) { - int start = sql.length(); + private void appendDeclarationTo(StringBuilder sql, int start) { if (type != null) { sql.append(type); } @@ -237,24 +237,27 @@ private void appendDeclarationTo(StringBuilder sql) { sql.append(nullsDistinctClause()).append(clusteringClause()); } - /** Separates {@code word} from earlier declaration words, without a leading space. */ + /** Separates {@code word} from earlier index words, without a leading space. */ private static void appendWord(StringBuilder sql, int start, String word) { + appendSeparator(sql, start); + sql.append(word); + } + + private static void appendSeparator(StringBuilder sql, int start) { if (sql.length() > start) { sql.append(' '); } - sql.append(word); } - private void appendColumnsAndSpecTo(StringBuilder sql, + private void appendColumnsAndSpecTo(StringBuilder sql, int start, Consumer expressionPrinter) { - String spec = PlainSelect.getStringList(getIndexSpec(), false, false); - boolean hasColumns = columns != null && !columns.isEmpty(); - if (hasColumns) { - sql.append(' '); + if (columns != null && !columns.isEmpty()) { + appendSeparator(sql, start); appendColumnsTo(sql, expressionPrinter); } + String spec = PlainSelect.getStringList(getIndexSpec(), false, false); if (!spec.isEmpty()) { - sql.append(hasColumns ? " " : " ").append(spec); + appendWord(sql, start, spec); } } } diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/DeparseWhitespaceTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/DeparseWhitespaceTest.java index ddd76cdcf..6dd6bee37 100644 --- a/src/test/java/net/sf/jsqlparser/statement/alter/DeparseWhitespaceTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/alter/DeparseWhitespaceTest.java @@ -10,11 +10,14 @@ package net.sf.jsqlparser.statement.alter; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import java.util.List; import net.sf.jsqlparser.JSQLParserException; import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; import net.sf.jsqlparser.parser.CCJSqlParserUtil; import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.create.index.CreateIndex; import net.sf.jsqlparser.statement.create.table.Index; import net.sf.jsqlparser.util.deparser.StatementDeParser; import org.junit.jupiter.params.ParameterizedTest; @@ -34,15 +37,97 @@ class DeparseWhitespaceTest { "RENAME a TO b", "ALTER TABLE t ADD INDEX i (body (10))", "ALTER TABLE t ADD KEY k (a, b)", - "CREATE TABLE t (a INT, INDEX i (a))"}) + "CREATE TABLE t (a INT, INDEX i (a))", + "ALTER TABLE t ALTER COLUMN c SET INVISIBLE", + "ALTER TABLE t ALTER COLUMN c SET VISIBLE", + "ALTER TABLE t ALTER c SET INVISIBLE", + "ALTER TABLE t ALTER INDEX i INVISIBLE", + "ALTER TABLE t ALTER INDEX i VISIBLE", + "ALTER TABLE t ALTER COLUMN c SET VISIBLE, ALTER INDEX i VISIBLE", + "ALTER TABLE t FORCE", + "ALTER TABLE t FORCE, ALGORITHM = INPLACE", + "CREATE TABLE t (a JSON DEFAULT (JSON_ARRAY()))", + "ALTER TABLE t ADD COLUMN j JSON DEFAULT (JSON_OBJECT())"}) void rendersSingleSpaces(String sql) throws JSQLParserException { - Statement statement = - CCJSqlParserUtil.parse(sql, parser -> parser.withDialect(Dialect.MYSQL)); + assertRendersExactly(sql, Dialect.MYSQL); + } + + @ParameterizedTest + @ValueSource(strings = {"ALTER TABLE t ENABLE ROW LEVEL SECURITY", + "ALTER TABLE t DISABLE ROW LEVEL SECURITY", + "ALTER TABLE t FORCE ROW LEVEL SECURITY", + "ALTER TABLE t NO FORCE ROW LEVEL SECURITY", + "ALTER TABLE t ENABLE ROW LEVEL SECURITY, FORCE ROW LEVEL SECURITY", + "CREATE INDEX ON t (a)"}) + void rendersSingleSpacesForPostgreSql(String sql) throws JSQLParserException { + assertRendersExactly(sql, Dialect.POSTGRESQL); + } + + @ParameterizedTest + @ValueSource(strings = {"ALTER TABLE t FORCE", + "ALTER TABLE t MODIFY (c INT)", + "ALTER TABLE t MODIFY COLUMN (c INT NOT NULL)", + "ALTER TABLE t ADD (c INT), ADD d INT", + "ALTER TABLE t ADD CONSTRAINT UNIQUE KEY uk (a)", + "SELECT JSON_ARRAY() FROM t", + "SELECT JSON_OBJECT() FROM t"}) + void rendersSingleSpacesWithoutDialect(String sql) throws JSQLParserException { + assertRendersExactly(sql, null); + } + + @ParameterizedTest + @ValueSource(strings = {"SELECT JSON_ARRAY(1, 2) AS a FROM t", + "SELECT JSON_ARRAY(NULL ON NULL) AS a FROM t", + "SELECT JSON_OBJECT(KEY 'a' VALUE 1) AS o FROM t", + "SELECT JSON_OBJECT(ABSENT ON NULL) AS o FROM t"}) + void jsonConstructorsAddNoDoubleOrTrailingSpace(String sql) throws JSQLParserException { + Statement statement = CCJSqlParserUtil.parse(sql); + String rendered = statement.toString(); + assertFalse(rendered.contains(" "), rendered); + assertEquals(rendered, deparse(statement)); + } + + @Test + void columnVisibilityHasSingleSpaces() { + assertEquals("c SET INVISIBLE", + new AlterExpression.ColumnSetVisibility("c", false).toString()); + assertEquals("c SET VISIBLE", + new AlterExpression.ColumnSetVisibility("c", true).toString()); + } + + @Test + void exchangePartitionHasNoTrailingSpace() { + AlterExpression expression = new AlterExpression() + .withOperation(AlterOperation.EXCHANGE_PARTITION); + expression.setPartitions(List.of("p0")); + expression.setExchangePartitionTableName("t2"); + expression.setExchangePartitionWithValidation(true); + assertEquals("EXCHANGE PARTITION p0 WITH TABLE t2 WITH VALIDATION", + expression.toString()); + } + + @Test + void indexWithoutDeclarationHasNoLeadingSpace() throws JSQLParserException { + CreateIndex createIndex = (CreateIndex) CCJSqlParserUtil.parse("CREATE INDEX ON t (a)", + parser -> parser.withDialect(Dialect.POSTGRESQL)); + assertEquals("(a)", createIndex.getIndex().toString()); + assertEquals("(a)", new Index().withColumnsNames(List.of("a")).toString()); + assertEquals("INDEX i INVISIBLE", new Index().withIndexKeyword("INDEX").withName("i") + .withIndexSpec(List.of("INVISIBLE")).toString()); + } + + private static void assertRendersExactly(String sql, Dialect dialect) + throws JSQLParserException { + Statement statement = dialect == null ? CCJSqlParserUtil.parse(sql) + : CCJSqlParserUtil.parse(sql, parser -> parser.withDialect(dialect)); assertEquals(sql, statement.toString()); + assertEquals(sql, deparse(statement)); + } + private static String deparse(Statement statement) { StringBuilder deparsed = new StringBuilder(); statement.accept(new StatementDeParser(deparsed)); - assertEquals(sql, deparsed.toString()); + return deparsed.toString(); } @Test