Skip to content

Commit f33f83c

Browse files
committed
feat: parse MySQL index hint scopes, row and table insert sources and DELETE USING joins
MySQL 8.4 index hints with FOR JOIN/ORDER BY/GROUP BY, several hints on one table, USE INDEX () and IGNORE KEY failed, as did WEIGHT_STRING(x AS CHAR(n)), INSERT ... VALUES ROW(...), INSERT ... TABLE t, INSERT INTO t () VALUES () and DELETE FROM t1 USING t1 JOIN t2. INSERT ... PARTITION (p) (cols) rendered PARTITION after the column list, and toString() and the deparsers disagreed on several INSERT and REPLACE forms. Keep every index hint in Table.getIndexHints() with a typed MySQLIndexHint.Scope, parsed by one list production and rendered by one helper; getIndexHint() returns the first. Store the WEIGHT_STRING cast in Function.getCastType(), a ColDataType built as in CAST from exactly CHAR(n) or BINARY(n) after the single argument. Let VALUES ROW( and TABLE name reach the existing Values and TableStatement sources, and share an InsertColumnList production that accepts () between INSERT and REPLACE. Hold the joins of a DELETE USING clause in Delete.getUsingJoins() and render the target's joins before USING. The cast type and the REPLACE column list carry nodes spanning their source. Render INSERT through one ordered Insert.appendTo with renderer callbacks, used by toString() and InsertDeParser, with callback appendTo methods on InsertDuplicateAction, InsertConflictAction and OracleMultiInsertClause. The deparser now keeps ON DUPLICATE KEY UPDATE ... WHERE and visits the ON CONFLICT action; the unused Insert.duplicateUpdateSets field is gone. Signed-off-by: minleejae <mmj9808@gmail.com>
1 parent a30a679 commit f33f83c

28 files changed

Lines changed: 1647 additions & 366 deletions

‎src/main/java/net/sf/jsqlparser/expression/Function.java‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import net.sf.jsqlparser.statement.select.Limit;
1818
import net.sf.jsqlparser.statement.select.OrderByElement;
1919

20+
import net.sf.jsqlparser.statement.create.table.ColDataType;
2021
import net.sf.jsqlparser.statement.create.table.ColumnDefinition;
2122
import net.sf.jsqlparser.statement.select.PlainSelect;
2223

@@ -59,6 +60,12 @@ public class Function extends ASTNodeAccessImpl implements Expression {
5960
*/
6061
private List<KeywordArgument> keywordArguments = null;
6162

63+
/**
64+
* The {@code AS type} cast of the argument, as in MySQL's {@code WEIGHT_STRING(str AS CHAR(3))}
65+
* or {@code WEIGHT_STRING(str AS BINARY(4))}.
66+
*/
67+
private ColDataType castType;
68+
6269
/** Column definitions supplied for a record-returning function inside ROWS FROM. */
6370
public List<ColumnDefinition> getResultColumnDefinitions() {
6471
return resultColumnDefinitions;
@@ -83,6 +90,31 @@ public Function(String name, Expression... parameters) {
8390
this.parameters = new ExpressionList<>(parameters);
8491
}
8592

93+
/**
94+
* Returns the {@code AS type} cast written after the argument, as in MySQL's
95+
* {@code WEIGHT_STRING(str AS CHAR(3))}, or {@code null}.
96+
*/
97+
public ColDataType getCastType() {
98+
return castType;
99+
}
100+
101+
public void setCastType(ColDataType castType) {
102+
this.castType = castType;
103+
}
104+
105+
public Function withCastType(ColDataType castType) {
106+
setCastType(castType);
107+
return this;
108+
}
109+
110+
/** Appends {@code " AS type"} when the argument has a {@link #getCastType() cast type}. */
111+
public StringBuilder appendCastTypeTo(StringBuilder builder) {
112+
if (castType != null) {
113+
builder.append(" AS ").append(castType);
114+
}
115+
return builder;
116+
}
117+
86118
@Override
87119
public <T, S> T accept(ExpressionVisitor<T> expressionVisitor, S context) {
88120
return expressionVisitor.visit(this, context);
@@ -385,6 +417,7 @@ public String toString() {
385417
}
386418

387419
b.append(parameters);
420+
appendCastTypeTo(b);
388421

389422
if (havingClause != null) {
390423
havingClause.appendTo(b);

‎src/main/java/net/sf/jsqlparser/expression/MySQLIndexHint.java‎

Lines changed: 60 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,35 @@
1212
import java.io.Serializable;
1313
import java.util.List;
1414

15+
/**
16+
* A MySQL index hint after a table reference:
17+
* {@code {USE|IGNORE|FORCE} {INDEX|KEY} [FOR {JOIN|ORDER BY|GROUP BY}] (index_list)}.
18+
* <p>
19+
* A table may carry several hints, see {@code Table.getIndexHints()}. {@code USE INDEX ()} has an
20+
* empty index list.
21+
*/
1522
public class MySQLIndexHint implements Serializable {
1623

1724
private final String action;
1825
private final String indexQualifier;
26+
private final Scope scope;
1927
private final List<String> indexNames;
2028

2129
public MySQLIndexHint(String action, String indexQualifier, List<String> indexNames) {
30+
this(action, indexQualifier, null, indexNames);
31+
}
32+
33+
/**
34+
* @param action {@code USE}, {@code IGNORE} or {@code FORCE}
35+
* @param indexQualifier {@code INDEX} or {@code KEY}
36+
* @param scope the {@code FOR JOIN|ORDER BY|GROUP BY} scope, or {@code null} for every use
37+
* @param indexNames the index names, empty for {@code USE INDEX ()}
38+
*/
39+
public MySQLIndexHint(String action, String indexQualifier, Scope scope,
40+
List<String> indexNames) {
2241
this.action = action;
2342
this.indexQualifier = indexQualifier;
43+
this.scope = scope;
2444
this.indexNames = indexNames;
2545
}
2646

@@ -32,22 +52,53 @@ public String getIndexQualifier() {
3252
return indexQualifier;
3353
}
3454

55+
/**
56+
* Returns the part of the query the hint applies to, or {@code null} when no {@code FOR} clause
57+
* was given.
58+
*/
59+
public Scope getScope() {
60+
return scope;
61+
}
62+
3563
public List<String> getIndexNames() {
3664
return indexNames;
3765
}
3866

39-
@Override
40-
public String toString() {
41-
// use|ignore|force key|index (index1,...,indexN)
42-
StringBuilder buffer = new StringBuilder();
43-
buffer.append(" ").append(action).append(" ").append(indexQualifier).append(" (");
67+
/** Appends {@code " USE INDEX [FOR scope] (i1,i2)"}, with a leading space. */
68+
public StringBuilder appendTo(StringBuilder builder) {
69+
// use|ignore|force key|index [for join|order by|group by] (index1,...,indexN)
70+
builder.append(" ").append(action).append(" ").append(indexQualifier);
71+
if (scope != null) {
72+
builder.append(" FOR ").append(scope.getKeywords());
73+
}
74+
builder.append(" (");
4475
for (int i = 0; i < indexNames.size(); i++) {
4576
if (i > 0) {
46-
buffer.append(",");
77+
builder.append(",");
4778
}
48-
buffer.append(indexNames.get(i));
79+
builder.append(indexNames.get(i));
80+
}
81+
return builder.append(")");
82+
}
83+
84+
@Override
85+
public String toString() {
86+
return appendTo(new StringBuilder()).toString();
87+
}
88+
89+
/** The {@code FOR} clause of an index hint. */
90+
public enum Scope {
91+
JOIN("JOIN"), ORDER_BY("ORDER BY"), GROUP_BY("GROUP BY");
92+
93+
private final String keywords;
94+
95+
Scope(String keywords) {
96+
this.keywords = keywords;
97+
}
98+
99+
/** Returns the keywords after {@code FOR}, for example {@code ORDER BY}. */
100+
public String getKeywords() {
101+
return keywords;
49102
}
50-
buffer.append(")");
51-
return buffer.toString();
52103
}
53104
}

‎src/main/java/net/sf/jsqlparser/schema/Table.java‎

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ public class Table extends ASTNodeAccessImpl
6060

6161
private UnPivot unpivot;
6262

63-
private MySQLIndexHint mysqlHints;
63+
private List<MySQLIndexHint> mysqlIndexHints;
6464

6565
private SQLServerHints sqlServerHints;
6666

@@ -411,17 +411,60 @@ public void setUnPivot(UnPivot unpivot) {
411411
this.unpivot = unpivot;
412412
}
413413

414+
/**
415+
* Returns the first MySQL index hint, or {@code null}. Use {@link #getIndexHints()} for a table
416+
* with several hints.
417+
*/
414418
public MySQLIndexHint getIndexHint() {
415-
return mysqlHints;
419+
return mysqlIndexHints == null || mysqlIndexHints.isEmpty() ? null
420+
: mysqlIndexHints.get(0);
416421
}
417422

418423
public Table withHint(MySQLIndexHint hint) {
419424
setHint(hint);
420425
return this;
421426
}
422427

428+
/**
429+
* Replaces the MySQL index hints with the given hint, or removes them for {@code null}.
430+
*/
423431
public void setHint(MySQLIndexHint hint) {
424-
this.mysqlHints = hint;
432+
this.mysqlIndexHints = hint == null ? null : new ArrayList<>(Collections.singleton(hint));
433+
}
434+
435+
/**
436+
* Returns the MySQL index hints in their order, for example
437+
* {@code USE INDEX (i1) IGNORE INDEX FOR ORDER BY (i2)}, or {@code null} when there are none.
438+
*/
439+
public List<MySQLIndexHint> getIndexHints() {
440+
return mysqlIndexHints;
441+
}
442+
443+
public void setIndexHints(List<MySQLIndexHint> indexHints) {
444+
this.mysqlIndexHints = indexHints;
445+
}
446+
447+
public Table withIndexHints(List<MySQLIndexHint> indexHints) {
448+
setIndexHints(indexHints);
449+
return this;
450+
}
451+
452+
public Table addIndexHint(MySQLIndexHint hint) {
453+
if (mysqlIndexHints == null) {
454+
mysqlIndexHints = new ArrayList<>();
455+
}
456+
mysqlIndexHints.add(hint);
457+
return this;
458+
}
459+
460+
/** Appends the MySQL index hints, each with a leading space, in their order. */
461+
public StringBuilder appendIndexHintsTo(StringBuilder builder) {
462+
if (mysqlIndexHints != null) {
463+
for (MySQLIndexHint hint : mysqlIndexHints) {
464+
hint.appendTo(builder);
465+
}
466+
}
467+
return builder;
425468
}
426469

427470
public SQLServerHints getSqlServerHints() {
@@ -468,9 +511,7 @@ public StringBuilder appendTo(StringBuilder builder) {
468511
builder.append(" ").append(unpivot);
469512
}
470513

471-
if (mysqlHints != null) {
472-
builder.append(mysqlHints);
473-
}
514+
appendIndexHintsTo(builder);
474515

475516
if (sqlServerHints != null) {
476517
builder.append(sqlServerHints);

‎src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@
105105
import net.sf.jsqlparser.statement.refresh.RefreshMaterializedViewStatement;
106106
import net.sf.jsqlparser.statement.reindex.Reindex;
107107
import net.sf.jsqlparser.statement.securitylabel.SecurityLabel;
108+
import net.sf.jsqlparser.statement.select.FromItem;
108109
import net.sf.jsqlparser.statement.select.FromItemVisitor;
109110
import net.sf.jsqlparser.statement.select.FromItemVisitorAdapter;
110111
import net.sf.jsqlparser.statement.select.PivotVisitor;
@@ -267,8 +268,14 @@ public <S> T visit(Delete delete, S context) {
267268
fromItemVisitor.visitTables(delete.getTables(), context);
268269
selectVisitor.visitOutputClause(delete.getOutputClause(), context);
269270
fromItemVisitor.visitFromItem(delete.getTable(), context);
270-
fromItemVisitor.visitTables(delete.getUsingList(), context);
271271
fromItemVisitor.visitJoins(delete.getJoins(), context);
272+
// USING may name subqueries, so visit the items rather than getUsingList()'s tables
273+
if (delete.getUsingFromItemList() != null) {
274+
for (FromItem usingFromItem : delete.getUsingFromItemList()) {
275+
fromItemVisitor.visitFromItem(usingFromItem, context);
276+
}
277+
}
278+
fromItemVisitor.visitJoins(delete.getUsingJoins(), context);
272279
expressionVisitor.visitExpression(delete.getWhere(), context);
273280

274281
expressionVisitor.visitPreferringClause(delete.getPreferringClause(), context);

‎src/main/java/net/sf/jsqlparser/statement/delete/Delete.java‎

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ public class Delete extends ASTNodeAccessImpl implements Statement {
4545
private OracleHint oracleHint = null;
4646
private List<Table> tables;
4747
private List<FromItem> usingFromItemList;
48+
private List<Join> usingJoins;
4849
private List<Join> joins;
4950
private Expression where;
5051
private PreferringClause preferringClause;
@@ -281,21 +282,14 @@ public String toString() {
281282
b.append(' ').append(forPortionClause);
282283
}
283284

285+
appendJoinsTo(b, joins);
286+
284287
if (usingFromItemList != null && !usingFromItemList.isEmpty()) {
285288
b.append(" USING ");
286289
b.append(usingFromItemList.stream()
287290
.map(Object::toString)
288291
.collect(joining(", ")));
289-
}
290-
291-
if (joins != null) {
292-
for (Join join : joins) {
293-
if (join.isSimple()) {
294-
b.append(", ").append(join);
295-
} else {
296-
b.append(" ").append(join);
297-
}
298-
}
292+
appendJoinsTo(b, usingJoins);
299293
}
300294

301295
if (where != null) {
@@ -542,4 +536,42 @@ public List<FromItem> getUsingFromItemList() {
542536
public void setUsingFromItemList(List<FromItem> usingFromItemList) {
543537
this.usingFromItemList = usingFromItemList;
544538
}
539+
540+
/**
541+
* Returns the joins after the USING items, as in MySQL's
542+
* {@code DELETE FROM t1 USING t1 JOIN t2 ON t1.id = t2.id} or PostgreSQL's
543+
* {@code DELETE FROM t USING s JOIN u ON s.id = u.id WHERE ...}, or {@code null}. They are
544+
* rendered only when {@link #getUsingFromItemList()} is not empty. {@link #getJoins()} holds
545+
* the joins of the target table, written before USING.
546+
*/
547+
public List<Join> getUsingJoins() {
548+
return usingJoins;
549+
}
550+
551+
public void setUsingJoins(List<Join> usingJoins) {
552+
this.usingJoins = usingJoins;
553+
}
554+
555+
public Delete withUsingJoins(List<Join> usingJoins) {
556+
setUsingJoins(usingJoins);
557+
return this;
558+
}
559+
560+
public Delete addUsingJoins(Join... usingJoins) {
561+
List<Join> collection = Optional.ofNullable(getUsingJoins()).orElseGet(ArrayList::new);
562+
Collections.addAll(collection, usingJoins);
563+
return this.withUsingJoins(collection);
564+
}
565+
566+
private static void appendJoinsTo(StringBuilder b, List<Join> joins) {
567+
if (joins != null) {
568+
for (Join join : joins) {
569+
if (join.isSimple()) {
570+
b.append(", ").append(join);
571+
} else {
572+
b.append(" ").append(join);
573+
}
574+
}
575+
}
576+
}
545577
}

0 commit comments

Comments
 (0)