Skip to content

Commit bee671d

Browse files
authored
Add support for IN list padding in SQL queries (#1254)
2 parents 0f1d1f4 + 3555c9b commit bee671d

File tree

19 files changed

+856
-20
lines changed

19 files changed

+856
-20
lines changed

doma-core/src/main/java/org/seasar/doma/internal/jdbc/sql/NodePreparedSqlBuilder.java

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import java.util.ArrayList;
77
import java.util.Arrays;
8+
import java.util.Collection;
89
import java.util.Iterator;
910
import java.util.List;
1011
import java.util.ListIterator;
@@ -64,6 +65,8 @@
6465
import org.seasar.doma.internal.jdbc.sql.node.WhereClauseNode;
6566
import org.seasar.doma.internal.jdbc.sql.node.WhitespaceNode;
6667
import org.seasar.doma.internal.jdbc.sql.node.WordNode;
68+
import org.seasar.doma.internal.util.IntegerUtil;
69+
import org.seasar.doma.internal.util.PaddingIterator;
6770
import org.seasar.doma.internal.util.SqlTokenUtil;
6871
import org.seasar.doma.internal.util.StringUtil;
6972
import org.seasar.doma.jdbc.Config;
@@ -364,7 +367,7 @@ protected void handleIterableValueNode(
364367
Class<?> valueClass,
365368
Consumer<Scalar<?, ?>> consumer) {
366369
int index = 0;
367-
for (Object v : values) {
370+
for (Object v : applyInListPadding(node, values)) {
368371
if (v == null) {
369372
SqlLocation location = node.getLocation();
370373
throw new JdbcException(
@@ -391,6 +394,29 @@ protected void handleIterableValueNode(
391394
}
392395
}
393396

397+
private <E> Iterable<E> applyInListPadding(ValueNode node, Iterable<E> values) {
398+
if (node.getInNode() == null || !config.getSqlBuilderSettings().requiresInListPadding()) {
399+
return values;
400+
}
401+
Collection<E> valueCollection;
402+
if (values instanceof Collection<E> collection) {
403+
valueCollection = collection;
404+
} else {
405+
valueCollection = new ArrayList<>();
406+
values.forEach(valueCollection::add);
407+
}
408+
if (valueCollection.isEmpty()) {
409+
return valueCollection;
410+
}
411+
int size = valueCollection.size();
412+
int maxSize = IntegerUtil.nextPowerOfTwo(size);
413+
int paddingSize = maxSize - size;
414+
if (paddingSize <= 0) {
415+
return valueCollection;
416+
}
417+
return () -> new PaddingIterator<>(valueCollection.iterator(), paddingSize);
418+
}
419+
394420
@Override
395421
public Void visitIfBlockNode(IfBlockNode node, Context p) {
396422
if (!handleIfNode(node, p)) {

doma-core/src/main/java/org/seasar/doma/internal/jdbc/sql/SqlParser.java

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
import java.util.Deque;
66
import java.util.Iterator;
77
import java.util.LinkedList;
8+
import java.util.List;
9+
import java.util.ListIterator;
810
import java.util.regex.Matcher;
911
import java.util.regex.Pattern;
1012
import org.seasar.doma.internal.jdbc.sql.node.AnonymousNode;
@@ -28,6 +30,7 @@
2830
import org.seasar.doma.internal.jdbc.sql.node.HavingClauseNode;
2931
import org.seasar.doma.internal.jdbc.sql.node.IfBlockNode;
3032
import org.seasar.doma.internal.jdbc.sql.node.IfNode;
33+
import org.seasar.doma.internal.jdbc.sql.node.InNode;
3134
import org.seasar.doma.internal.jdbc.sql.node.LiteralVariableNode;
3235
import org.seasar.doma.internal.jdbc.sql.node.LogicalOperatorNode;
3336
import org.seasar.doma.internal.jdbc.sql.node.OptionClauseNode;
@@ -138,6 +141,11 @@ public SqlNode parse() {
138141
parseDistinctWord();
139142
break;
140143
}
144+
case IN_WORD:
145+
{
146+
parseInWord();
147+
break;
148+
}
141149
case UPDATE_WORD:
142150
{
143151
parseUpdateWord();
@@ -417,6 +425,11 @@ protected void parseDistinctWord() {
417425
appendNode(node);
418426
}
419427

428+
protected void parseInWord() {
429+
InNode node = new InNode(token);
430+
appendNode(node);
431+
}
432+
420433
protected void parseBlockComment() {
421434
CommentNode node = new CommentNode(token, CommentType.BLOCK);
422435
appendNode(node);
@@ -457,6 +470,8 @@ protected void parseBindVariableBlockComment() {
457470
Message.DOMA2120, sql, tokenizer.getLineNumber(), tokenizer.getPosition(), token);
458471
}
459472
BindVariableNode node = new BindVariableNode(getLocation(), variableName, token);
473+
InNode inNode = findInNode();
474+
node.setInNode(inNode);
460475
appendNode(node);
461476
push(node);
462477
}
@@ -468,6 +483,8 @@ protected void parseLiteralVariableBlockComment() {
468483
Message.DOMA2228, sql, tokenizer.getLineNumber(), tokenizer.getPosition(), token);
469484
}
470485
LiteralVariableNode node = new LiteralVariableNode(getLocation(), variableName, token);
486+
InNode inNode = findInNode();
487+
node.setInNode(inNode);
471488
appendNode(node);
472489
push(node);
473490
}
@@ -596,6 +613,7 @@ protected void parseDelimiter() {
596613
rootNode.appendNode(OtherNode.of(token));
597614
}
598615

616+
@Deprecated(forRemoval = true)
599617
protected boolean containsOnlyWhitespaces(SqlNode node) {
600618
for (SqlNode child : node.getChildren()) {
601619
if (!(child instanceof WhitespaceNode)) {
@@ -708,10 +726,34 @@ protected boolean isAfterExpandNode() {
708726
return peek() instanceof ExpandNode;
709727
}
710728

729+
@Deprecated(forRemoval = true)
711730
protected boolean isAfterOrderByClauseNode() {
712731
return peek() instanceof OrderByClauseNode;
713732
}
714733

734+
protected InNode findInNode() {
735+
AppendableSqlNode node = peek();
736+
List<SqlNode> children = node.getChildren();
737+
ListIterator<SqlNode> iterator = children.listIterator(children.size());
738+
while (iterator.hasPrevious()) {
739+
SqlNode child = iterator.previous();
740+
if (child instanceof WhitespaceNode) {
741+
continue;
742+
}
743+
if (child instanceof EolNode) {
744+
continue;
745+
}
746+
if (child instanceof CommentNode) {
747+
continue;
748+
}
749+
if (child instanceof InNode inNode) {
750+
return inNode;
751+
}
752+
return null;
753+
}
754+
return null;
755+
}
756+
715757
protected void appendNode(SqlNode node) {
716758
if (isAfterValueNode()) {
717759
ValueNode valueNode = pop();

doma-core/src/main/java/org/seasar/doma/internal/jdbc/sql/SqlTokenType.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,8 @@ public String extract(String token) {
143143

144144
DISTINCT_WORD,
145145

146+
IN_WORD,
147+
146148
OTHER,
147149

148150
WHITESPACE,

doma-core/src/main/java/org/seasar/doma/internal/jdbc/sql/SqlTokenizer.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import static org.seasar.doma.internal.jdbc.sql.SqlTokenType.HAVING_WORD;
2222
import static org.seasar.doma.internal.jdbc.sql.SqlTokenType.IF_BLOCK_COMMENT;
2323
import static org.seasar.doma.internal.jdbc.sql.SqlTokenType.INTERSECT_WORD;
24+
import static org.seasar.doma.internal.jdbc.sql.SqlTokenType.IN_WORD;
2425
import static org.seasar.doma.internal.jdbc.sql.SqlTokenType.LINE_COMMENT;
2526
import static org.seasar.doma.internal.jdbc.sql.SqlTokenType.LITERAL_VARIABLE_BLOCK_COMMENT;
2627
import static org.seasar.doma.internal.jdbc.sql.SqlTokenType.MINUS_WORD;
@@ -381,6 +382,11 @@ protected void peekTwoChars(char c, char c2) {
381382
if (isWordTerminated()) {
382383
return;
383384
}
385+
} else if ((c == 'i' || c == 'I') && (c2 == 'n' || c2 == 'N')) {
386+
type = IN_WORD;
387+
if (isWordTerminated()) {
388+
return;
389+
}
384390
} else if (c == '/' && c2 == '*') {
385391
type = BLOCK_COMMENT;
386392
if (buf.hasRemaining()) {
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package org.seasar.doma.internal.jdbc.sql.node;
2+
3+
import org.seasar.doma.DomaNullPointerException;
4+
import org.seasar.doma.jdbc.JdbcUnsupportedOperationException;
5+
import org.seasar.doma.jdbc.SqlNode;
6+
import org.seasar.doma.jdbc.SqlNodeVisitor;
7+
8+
public class InNode extends AbstractSqlNode {
9+
10+
protected final WordNode wordNode;
11+
12+
public InNode(String word) {
13+
wordNode = new WordNode(word, false);
14+
}
15+
16+
public WordNode getWordNode() {
17+
return wordNode;
18+
}
19+
20+
@Override
21+
public void appendNode(SqlNode child) {
22+
throw new JdbcUnsupportedOperationException(getClass().getName(), "addNode");
23+
}
24+
25+
@Override
26+
public <R, P> R accept(SqlNodeVisitor<R, P> visitor, P p) {
27+
if (visitor == null) {
28+
throw new DomaNullPointerException("visitor");
29+
}
30+
return visitor.visitInNode(this, p);
31+
}
32+
}

doma-core/src/main/java/org/seasar/doma/internal/jdbc/sql/node/ValueNode.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ public abstract class ValueNode extends AbstractSqlNode {
1313

1414
protected final String text;
1515

16+
protected InNode inNode;
17+
1618
protected WordNode wordNode;
1719

1820
protected ParensNode parensNode;
@@ -41,6 +43,14 @@ public void appendNode(SqlNode child) {
4143
throw new JdbcUnsupportedOperationException(getClass().getName(), "addNode");
4244
}
4345

46+
public InNode getInNode() {
47+
return inNode;
48+
}
49+
50+
public void setInNode(InNode inNode) {
51+
this.inNode = inNode;
52+
}
53+
4454
public WordNode getWordNode() {
4555
return wordNode;
4656
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package org.seasar.doma.internal.util;
2+
3+
public final class IntegerUtil {
4+
5+
/**
6+
* Computes the next power of two greater than or equal to the given integer. If the input is
7+
* already a power of two, the method returns the same value. If the computation results in an
8+
* overflow, the input value is returned instead.
9+
*
10+
* @param n the integer input, must be greater than or equal to 0
11+
* @return the next power of two greater than or equal to the input value
12+
* @throws IllegalArgumentException if the input value is less than 0
13+
*/
14+
public static int nextPowerOfTwo(int n) {
15+
if (n < 0) {
16+
throw new IllegalArgumentException("n must be greater than or equal to 0");
17+
}
18+
if (n == 0) {
19+
return 1;
20+
}
21+
// The input is already a power of two
22+
if ((n & (n - 1)) == 0) {
23+
return n;
24+
}
25+
int result = Integer.highestOneBit(n) << 1;
26+
// The calculation overflowed
27+
if (result < 0) {
28+
return n;
29+
}
30+
return result;
31+
}
32+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package org.seasar.doma.internal.util;
2+
3+
import java.util.Iterator;
4+
import java.util.NoSuchElementException;
5+
import java.util.Objects;
6+
7+
/**
8+
* An iterator that extends an existing {@link Iterator} with padding behavior.
9+
*
10+
* <p>The {@code PaddingIterator} guarantees that additional elements will be yielded even after the
11+
* original iterator has been exhausted. These additional elements will repeat the last element
12+
* produced by the original iterator until the specified padding size is reached.
13+
*
14+
* @param <E> the type of elements returned by this iterator
15+
*/
16+
public class PaddingIterator<E> implements Iterator<E> {
17+
18+
private final Iterator<E> iterator;
19+
private E lastElement;
20+
private int paddingSize;
21+
22+
/**
23+
* Constructs a {@code PaddingIterator} that decorates the specified {@link Iterator} with
24+
* additional padding behavior.
25+
*
26+
* @param iterator the original iterator to be decorated; must not be {@code null}
27+
* @param paddingSize the number of additional elements to produce after the original iterator is
28+
* exhausted; must be {@code >= 0}
29+
* @throws NullPointerException if {@code iterator} is {@code null}
30+
* @throws IllegalArgumentException if {@code paddingSize} is less than 0
31+
*/
32+
public PaddingIterator(Iterator<E> iterator, int paddingSize) {
33+
Objects.requireNonNull(iterator);
34+
if (paddingSize < 0) {
35+
throw new IllegalArgumentException("paddingSize must be greater than or equal to 0");
36+
}
37+
this.iterator = iterator;
38+
this.lastElement = null;
39+
this.paddingSize = paddingSize;
40+
}
41+
42+
@Override
43+
public boolean hasNext() {
44+
return iterator.hasNext() || paddingSize > 0;
45+
}
46+
47+
@Override
48+
public E next() {
49+
if (iterator.hasNext()) {
50+
E element = iterator.next();
51+
lastElement = element;
52+
return element;
53+
} else if (paddingSize > 0) {
54+
paddingSize--;
55+
return lastElement;
56+
}
57+
throw new NoSuchElementException();
58+
}
59+
}

doma-core/src/main/java/org/seasar/doma/jdbc/SqlBuilderSettings.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,13 @@ default boolean shouldRemoveLineComment(String comment) {
3333
default boolean shouldRemoveBlankLines() {
3434
return false;
3535
}
36+
37+
/**
38+
* Indicates whether padding is required for the "IN" list clauses in SQL generation.
39+
*
40+
* @return {@code true} if padding is required; {@code false} otherwise
41+
*/
42+
default boolean requiresInListPadding() {
43+
return false;
44+
}
3645
}

doma-core/src/main/java/org/seasar/doma/jdbc/SqlNodeVisitor.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ public interface SqlNodeVisitor<R, P> {
5050

5151
R visitIfNode(IfNode node, P p);
5252

53+
default R visitInNode(InNode node, P p) {
54+
return visitWordNode(node.getWordNode(), p);
55+
}
56+
5357
R visitLiteralVariableNode(LiteralVariableNode node, P p);
5458

5559
R visitLogicalOperatorNode(LogicalOperatorNode node, P p);

0 commit comments

Comments
 (0)