Skip to content

Commit a206b86

Browse files
minleejaemj
andauthored
Support Teradata UPDATE FROM before SET with shared clause rendering (#2587)
Co-authored-by: mj <mj@mjui-MacBookPro.local>
1 parent df984dc commit a206b86

8 files changed

Lines changed: 181 additions & 32 deletions

File tree

src/main/java/net/sf/jsqlparser/parser/AbstractJSqlParser.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ public enum Dialect {
4646
AdjacentStringLiterals.WHITESPACE,
4747
Feature.allowDoubleQuotedStrings,
4848
Feature.allowBackslashEscapeCharacter), SNOWFLAKE(
49-
Feature.allowBackslashEscapeCharacter), INFORMIX, SPANNER, DORIS, COCKROACHDB;
49+
Feature.allowBackslashEscapeCharacter), INFORMIX, SPANNER, DORIS, COCKROACHDB, TERADATA;
5050

5151
private final Set<Feature> lexerFeatures;
5252
private final AdjacentStringLiterals adjacentStringLiterals;

src/main/java/net/sf/jsqlparser/statement/update/Update.java

Lines changed: 50 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ public class Update implements Statement {
4444
private PreferringClause preferringClause;
4545
private List<UpdateSet> updateSets;
4646
private FromItem fromItem;
47+
private boolean fromBeforeSet;
4748
private List<Join> joins;
4849
private List<Join> startJoins;
4950
private OracleHint oracleHint = null;
@@ -190,6 +191,50 @@ public void setFromItem(FromItem fromItem) {
190191
this.fromItem = fromItem;
191192
}
192193

194+
/** Whether the FROM clause precedes SET, as in Teradata joined updates. */
195+
public boolean isFromBeforeSet() {
196+
return fromBeforeSet;
197+
}
198+
199+
public void setFromBeforeSet(boolean fromBeforeSet) {
200+
this.fromBeforeSet = fromBeforeSet;
201+
}
202+
203+
public Update withFromBeforeSet(boolean fromBeforeSet) {
204+
setFromBeforeSet(fromBeforeSet);
205+
return this;
206+
}
207+
208+
/** Whether the Teradata target names an alias declared in this UPDATE's FROM clause. */
209+
public boolean isTargetTableAlias() {
210+
if (!fromBeforeSet || table == null) {
211+
return false;
212+
}
213+
if (matchesTargetAlias(fromItem)) {
214+
return true;
215+
}
216+
return joins != null
217+
&& joins.stream().anyMatch(join -> matchesTargetAlias(join.getRightItem()));
218+
}
219+
220+
private boolean matchesTargetAlias(FromItem source) {
221+
return source != null && source.getAlias() != null
222+
&& table.getFullyQualifiedName().equalsIgnoreCase(source.getAlias().getName());
223+
}
224+
225+
/** Shared FROM rendering for both supported clause positions. */
226+
public StringBuilder appendFromTo(StringBuilder builder) {
227+
if (fromItem != null) {
228+
builder.append(" FROM ").append(fromItem);
229+
if (joins != null) {
230+
for (Join join : joins) {
231+
builder.append(join.isSimple() ? ", " : " ").append(join);
232+
}
233+
}
234+
}
235+
return builder;
236+
}
237+
193238
public List<Join> getJoins() {
194239
return joins;
195240
}
@@ -337,24 +382,18 @@ public String toString() {
337382
}
338383
}
339384

385+
if (fromBeforeSet) {
386+
appendFromTo(b);
387+
}
340388
b.append(" SET ");
341389
UpdateSet.appendUpdateSetsTo(b, updateSets);
342390

343391
if (outputClause != null) {
344392
outputClause.appendTo(b);
345393
}
346394

347-
if (fromItem != null) {
348-
b.append(" FROM ").append(fromItem);
349-
if (joins != null) {
350-
for (Join join : joins) {
351-
if (join.isSimple()) {
352-
b.append(", ").append(join);
353-
} else {
354-
b.append(" ").append(join);
355-
}
356-
}
357-
}
395+
if (!fromBeforeSet) {
396+
appendFromTo(b);
358397
}
359398

360399
if (where != null) {

src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1386,7 +1386,9 @@ public <S> Void visit(Update update, S context) {
13861386
}
13871387
}
13881388

1389-
visit(update.getTable(), context);
1389+
if (!update.isTargetTableAlias()) {
1390+
visit(update.getTable(), context);
1391+
}
13901392

13911393
if (update.getStartJoins() != null) {
13921394
for (Join join : update.getStartJoins()) {

src/main/java/net/sf/jsqlparser/util/deparser/UpdateDeParser.java

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,9 @@ public void deParse(Update update) {
7070
}
7171
}
7272
}
73+
if (update.isFromBeforeSet()) {
74+
update.appendFromTo(builder);
75+
}
7376
builder.append(" SET ");
7477

7578
deparseUpdateSetsClause(update);
@@ -78,17 +81,8 @@ public void deParse(Update update) {
7881
update.getOutputClause().appendTo(builder);
7982
}
8083

81-
if (update.getFromItem() != null) {
82-
builder.append(" FROM ").append(update.getFromItem());
83-
if (update.getJoins() != null) {
84-
for (Join join : update.getJoins()) {
85-
if (join.isSimple()) {
86-
builder.append(", ").append(join);
87-
} else {
88-
builder.append(" ").append(join);
89-
}
90-
}
91-
}
84+
if (!update.isFromBeforeSet()) {
85+
update.appendFromTo(builder);
9286
}
9387

9488
deparseWhereClause(update);

src/main/java/net/sf/jsqlparser/util/validation/validator/UpdateValidator.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@ public void validate(Update update) {
3434
Feature.updateReturning);
3535
}
3636

37-
validateOptionalFromItem(update.getTable());
37+
if (!update.isTargetTableAlias()) {
38+
validateOptionalFromItem(update.getTable());
39+
}
3840

3941
validateOptional(update.getStartJoins(),
4042
j -> getValidator(SelectValidator.class).validateOptionalJoins(j));

src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4574,8 +4574,6 @@ Update Update():
45744574
List<UpdateSet> updateSets;
45754575
Expression where = null;
45764576
PreferringClause preferringClause = null;
4577-
FromItem fromItem = null;
4578-
List<Join> joins = null;
45794577
Limit limit = null;
45804578
OptionClause optionClause = null;
45814579
List<OrderByElement> orderByElements;
@@ -4592,13 +4590,14 @@ Update Update():
45924590
[ LOOKAHEAD(2) <K_LOW_PRIORITY> { modifierPriority = UpdateModifierPriority.LOW_PRIORITY; }]
45934591
[ LOOKAHEAD(2) <K_IGNORE> { modifierIgnore = true; }]
45944592
table=TableWithAliasAndMysqlIndexHint() [ startJoins=JoinsList() ]
4593+
[ LOOKAHEAD(<K_FROM>, { Dialect.TERADATA.name().equals(getAsString(Feature.dialect)) })
4594+
UpdateFromClause(update) { update.setFromBeforeSet(true); } ]
45954595
<K_SET> updateSets = UpdateSets() { update.setUpdateSets(updateSets); }
45964596

45974597
[ outputClause = OutputClause() {update.setOutputClause(outputClause); } ]
45984598

4599-
[ LOOKAHEAD(2) <K_FROM>
4600-
fromItem=FromItem()
4601-
[ LOOKAHEAD(2) joins=JoinsList() ] ]
4599+
[ LOOKAHEAD(2, { !Dialect.TERADATA.name().equals(getAsString(Feature.dialect)) })
4600+
UpdateFromClause(update) ]
46024601

46034602
[ where=WhereClause() { update.setWhere(where); } ]
46044603
[ preferringClause=PreferringClause() { update.setPreferringClause(preferringClause); } ]
@@ -4612,13 +4611,22 @@ Update Update():
46124611
return update.withWithItemsList(with)
46134612
.withTable(table)
46144613
.withStartJoins(startJoins)
4615-
.withFromItem(fromItem)
4616-
.withJoins(joins)
46174614
.withModifierPriority(modifierPriority)
46184615
.withModifierIgnore(modifierIgnore);
46194616
}
46204617
}
46214618

4619+
void UpdateFromClause(Update update):
4620+
{
4621+
FromItem fromItem;
4622+
List<Join> joins = null;
4623+
}
4624+
{
4625+
<K_FROM> fromItem=FromItem()
4626+
[ LOOKAHEAD(2) joins=JoinsList() ]
4627+
{ update.setFromItem(fromItem); update.setJoins(joins); }
4628+
}
4629+
46224630
List<UpdateSet> UpdateSets():
46234631
{
46244632
ArrayList<UpdateSet> updateSets = new ArrayList<UpdateSet>();

src/site/sphinx/usage.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -738,6 +738,8 @@ One grammar covers every supported RDBMS, but a few pieces of syntax mean differ
738738
- ``JOIN [shuffle]`` and ``JOIN [broadcast]`` distribution hints
739739
* - ``COCKROACHDB``
740740
- ``ALTER TABLE ... ALTER PRIMARY KEY USING COLUMNS (...)`` with optional hash sharding and storage parameters
741+
* - ``TERADATA``
742+
- ``UPDATE target FROM sources SET ...`` with the FROM clause before SET
741743

742744
Features set explicitly *after* the preset win over it.
743745

@@ -758,6 +760,12 @@ parameters in ``getIndex()``. ``isUsingHash()`` preserves ``USING HASH``, while
758760
``getBucketCount()`` holds the legacy ``WITH BUCKET_COUNT = expression`` value.
759761
The newer ``WITH (bucket_count = expression)`` form uses the index storage parameters.
760762

763+
With ``Dialect.TERADATA``, ``UPDATE a FROM target a, source b SET a.id = b.id``
764+
uses the existing ``Update`` model's ``fromItem`` and ``joins`` properties.
765+
``isFromBeforeSet()`` preserves the clause position in both SQL renderers.
766+
Table discovery and metadata validation recognize a target alias declared in
767+
that FROM clause. Other dialects retain the existing FROM-after-SET syntax.
768+
761769
With ``Dialect.SQLSERVER``, ``PRIMARY KEY NONCLUSTERED (id)`` and
762770
``UNIQUE CLUSTERED (id)`` store their clustering option in ``Index.getClustering()``
763771
for both ``CREATE TABLE`` and ``ALTER TABLE``. Without that dialect, these words
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/*-
2+
* #%L
3+
* JSQLParser library
4+
* %%
5+
* Copyright (C) 2004 - 2026 JSQLParser
6+
* %%
7+
* Dual licensed under GNU LGPL 2.1 or Apache License 2.0
8+
* #L%
9+
*/
10+
package net.sf.jsqlparser.statement.update;
11+
12+
import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed;
13+
import static org.junit.jupiter.api.Assertions.*;
14+
15+
import java.util.Set;
16+
import net.sf.jsqlparser.JSQLParserException;
17+
import net.sf.jsqlparser.expression.LongValue;
18+
import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect;
19+
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
20+
import net.sf.jsqlparser.statement.Statement;
21+
import net.sf.jsqlparser.util.TablesNamesFinder;
22+
import net.sf.jsqlparser.util.deparser.ExpressionDeParser;
23+
import net.sf.jsqlparser.util.deparser.SelectDeParser;
24+
import net.sf.jsqlparser.util.deparser.StatementDeParser;
25+
import org.junit.jupiter.api.Test;
26+
import org.junit.jupiter.params.ParameterizedTest;
27+
import org.junit.jupiter.params.provider.ValueSource;
28+
29+
class TeradataUpdateTest {
30+
private static Update parse(String sql) throws JSQLParserException {
31+
return (Update) CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.TERADATA));
32+
}
33+
34+
@Test
35+
void parsesIssue891AndFindsSourceTables() throws Exception {
36+
String sql = "UPDATE a FROM db1.table1 a, db2.tabl2 b SET a.column1 = b.column1 "
37+
+ "WHERE a.column2 = b.column2";
38+
Update update = (Update) assertSqlCanBeParsedAndDeparsed(sql, true,
39+
p -> p.withDialect(Dialect.TERADATA));
40+
assertTrue(update.isFromBeforeSet());
41+
assertTrue(update.isTargetTableAlias());
42+
assertEquals("a", update.getTable().getName());
43+
assertEquals(1, update.getJoins().size());
44+
assertEquals(Set.of("db1.table1", "db2.tabl2"), new TablesNamesFinder().getTables(update));
45+
assertEquals(sql, parse(update.toString()).toString());
46+
}
47+
48+
@ParameterizedTest
49+
@ValueSource(strings = {
50+
"UPDATE t FROM source s SET id = s.id",
51+
"UPDATE b FROM source a, target b SET b.id = a.id WHERE a.id > 1",
52+
"UPDATE \"target\" FROM \"schema\".\"table\" AS \"target\" SET id = 1",
53+
"UPDATE t SET id = 1"})
54+
void roundTripsTargetsAndOptionalFrom(String sql) throws Exception {
55+
Statement statement = assertSqlCanBeParsedAndDeparsed(sql, true,
56+
p -> p.withDialect(Dialect.TERADATA));
57+
assertEquals(statement.toString(), parse(statement.toString()).toString());
58+
}
59+
60+
@Test
61+
void gatesClauseOrderAndRejectsDuplicates() throws Exception {
62+
String sql = "UPDATE t FROM s SET id = 1";
63+
assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql));
64+
for (Dialect dialect : Dialect.values()) {
65+
if (dialect != Dialect.TERADATA) {
66+
assertThrows(JSQLParserException.class,
67+
() -> CCJSqlParserUtil.parse(sql, p -> p.withDialect(dialect)));
68+
}
69+
}
70+
assertThrows(JSQLParserException.class, () -> parse(sql + " FROM other"));
71+
assertThrows(JSQLParserException.class, () -> parse("UPDATE t FROM SET id = 1"));
72+
Update legacy = (Update) assertSqlCanBeParsedAndDeparsed("UPDATE t SET id = 1 FROM s");
73+
assertFalse(legacy.isFromBeforeSet());
74+
legacy.setFromBeforeSet(true);
75+
assertEquals(sql, legacy.toString());
76+
assertEquals(2, CCJSqlParserUtil.parseStatements(sql + "; SELECT 1;",
77+
p -> p.withDialect(Dialect.TERADATA)).size());
78+
}
79+
80+
@Test
81+
void customDeparserKeepsFromPosition() throws Exception {
82+
Update update = parse("UPDATE t FROM s SET id = 1 WHERE s.id > 2");
83+
StringBuilder builder = new StringBuilder();
84+
ExpressionDeParser expressions = new ExpressionDeParser() {
85+
@Override
86+
public <S> StringBuilder visit(LongValue value, S context) {
87+
return getBuilder().append(value.getValue() + 10);
88+
}
89+
};
90+
update.accept(new StatementDeParser(expressions, new SelectDeParser(), builder));
91+
assertEquals("UPDATE t FROM s SET id = 11 WHERE s.id > 12", builder.toString());
92+
assertEquals(builder.toString(), parse(builder.toString()).toString());
93+
update.withFromBeforeSet(false);
94+
assertEquals("UPDATE t SET id = 1 FROM s WHERE s.id > 2", update.toString());
95+
}
96+
}

0 commit comments

Comments
 (0)