From 458031b76ca4f58f4aa57b60b2e92c49c7a39669 Mon Sep 17 00:00:00 2001 From: tanjiro Kamado Date: Tue, 27 Oct 2026 12:05:38 +0530 Subject: [PATCH] avoid quadratic positional access over linked-list column lists --- .../jsqlparser/statement/select/Select.java | 10 ++-- .../util/ConnectExpressionsVisitor.java | 10 ++-- .../select/JoinUsingDeParseTest.java | 48 +++++++++++++++++++ .../util/ConnectExpressionsVisitorTest.java | 26 ++++++++++ 4 files changed, 87 insertions(+), 7 deletions(-) create mode 100644 src/test/java/net/sf/jsqlparser/statement/select/JoinUsingDeParseTest.java diff --git a/src/main/java/net/sf/jsqlparser/statement/select/Select.java b/src/main/java/net/sf/jsqlparser/statement/select/Select.java index d3750c98ef..2dd82f93ea 100644 --- a/src/main/java/net/sf/jsqlparser/statement/select/Select.java +++ b/src/main/java/net/sf/jsqlparser/statement/select/Select.java @@ -131,9 +131,13 @@ public static StringBuilder appendStringListTo(StringBuilder builder, List li builder.append("("); } - int size = list.size(); - for (int i = 0; i < size; i++) { - builder.append(list.get(i)).append(i < size - 1 ? comma : ""); + boolean first = true; + for (Object element : list) { + if (!first) { + builder.append(comma); + } + builder.append(element); + first = false; } if (useBrackets) { diff --git a/src/main/java/net/sf/jsqlparser/util/ConnectExpressionsVisitor.java b/src/main/java/net/sf/jsqlparser/util/ConnectExpressionsVisitor.java index ca642b7a11..7c97f57972 100644 --- a/src/main/java/net/sf/jsqlparser/util/ConnectExpressionsVisitor.java +++ b/src/main/java/net/sf/jsqlparser/util/ConnectExpressionsVisitor.java @@ -9,6 +9,7 @@ */ package net.sf.jsqlparser.util; +import java.util.Iterator; import java.util.LinkedList; import java.util.List; @@ -70,15 +71,16 @@ public T visit(PlainSelect plainSelect, S context) { } if (itemsExpr.size() > 1) { + Iterator> it = itemsExpr.iterator(); BinaryExpression binExpr = createBinaryExpression(); - binExpr.setLeftExpression(itemsExpr.get(0).getExpression()); - for (int i = 1; i < itemsExpr.size() - 1; i++) { - binExpr.setRightExpression(itemsExpr.get(i).getExpression()); + binExpr.setLeftExpression(it.next().getExpression()); + binExpr.setRightExpression(it.next().getExpression()); + while (it.hasNext()) { BinaryExpression binExpr2 = createBinaryExpression(); binExpr2.setLeftExpression(binExpr); + binExpr2.setRightExpression(it.next().getExpression()); binExpr = binExpr2; } - binExpr.setRightExpression(itemsExpr.get(itemsExpr.size() - 1).getExpression()); SelectItem sei = new SelectItem<>(); sei.setExpression(binExpr); diff --git a/src/test/java/net/sf/jsqlparser/statement/select/JoinUsingDeParseTest.java b/src/test/java/net/sf/jsqlparser/statement/select/JoinUsingDeParseTest.java new file mode 100644 index 0000000000..4b5f7d33ce --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/select/JoinUsingDeParseTest.java @@ -0,0 +1,48 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2019 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.statement.select; + +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statement; +import org.junit.jupiter.api.Test; + +import java.time.Duration; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively; + +public class JoinUsingDeParseTest { + + private static String usingSelect(int columnCount) { + StringBuilder sb = new StringBuilder("SELECT * FROM a JOIN b USING ("); + for (int i = 0; i < columnCount; i++) { + if (i > 0) { + sb.append(", "); + } + sb.append("c").append(i); + } + return sb.append(")").toString(); + } + + @Test + void deParseOfLargeUsingColumnListIsLinear() throws Exception { + // usingColumns is a LinkedList, so deparsing the clause through + // Select.appendStringListTo used to read it by index (get(i)), which is O(n^2). + String sql = usingSelect(200000); + Statement statement = CCJSqlParserUtil.parse(sql); + assertEquals(sql, assertTimeoutPreemptively(Duration.ofSeconds(5), statement::toString)); + } + + @Test + void deParseOfUsingColumnListIsUnchanged() throws Exception { + String sql = "SELECT * FROM a JOIN b USING (c0, c1, c2)"; + assertEquals(sql, CCJSqlParserUtil.parse(sql).toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/util/ConnectExpressionsVisitorTest.java b/src/test/java/net/sf/jsqlparser/util/ConnectExpressionsVisitorTest.java index 712e1950f6..827dd56a28 100644 --- a/src/test/java/net/sf/jsqlparser/util/ConnectExpressionsVisitorTest.java +++ b/src/test/java/net/sf/jsqlparser/util/ConnectExpressionsVisitorTest.java @@ -18,8 +18,10 @@ import org.junit.jupiter.api.Test; import java.io.StringReader; +import java.time.Duration; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively; public class ConnectExpressionsVisitorTest { @@ -54,4 +56,28 @@ protected BinaryExpression createBinaryExpression() { assertEquals("SELECT a + b + c AS testexpr FROM test", select.toString()); } + + @Test + public void testVisit_PlainSelect_manyItemsIsLinear() throws JSQLParserException { + int itemCount = 200000; + StringBuilder sql = new StringBuilder("select "); + for (int i = 0; i < itemCount; i++) { + if (i > 0) { + sql.append(','); + } + sql.append('c').append(i); + } + sql.append(" from test"); + Select select = (Select) parserManager.parse(new StringReader(sql.toString())); + ConnectExpressionsVisitor instance = new ConnectExpressionsVisitor<>() { + @Override + protected BinaryExpression createBinaryExpression() { + return new Concat(); + } + }; + // itemsExpr is a LinkedList; connecting the items used to index it by position (get(i)), + // which is O(n^2) in the number of select items. + assertTimeoutPreemptively(Duration.ofSeconds(5), () -> select.accept(instance, null)); + assertEquals(1, select.getPlainSelect().getSelectItems().size()); + } }