Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public enum Dialect {
AdjacentStringLiterals.WHITESPACE,
Feature.allowDoubleQuotedStrings,
Feature.allowBackslashEscapeCharacter), SNOWFLAKE(
Feature.allowBackslashEscapeCharacter), INFORMIX, SPANNER, DORIS, COCKROACHDB;
Feature.allowBackslashEscapeCharacter), INFORMIX, SPANNER, DORIS, COCKROACHDB, TERADATA;

private final Set<Feature> lexerFeatures;
private final AdjacentStringLiterals adjacentStringLiterals;
Expand Down
61 changes: 50 additions & 11 deletions src/main/java/net/sf/jsqlparser/statement/update/Update.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
private PreferringClause preferringClause;
private List<UpdateSet> updateSets;
private FromItem fromItem;
private boolean fromBeforeSet;
private List<Join> joins;
private List<Join> startJoins;
private OracleHint oracleHint = null;
Expand Down Expand Up @@ -190,6 +191,50 @@
this.fromItem = fromItem;
}

/** Whether the FROM clause precedes SET, as in Teradata joined updates. */
public boolean isFromBeforeSet() {
return fromBeforeSet;
}

public void setFromBeforeSet(boolean fromBeforeSet) {
this.fromBeforeSet = fromBeforeSet;
}

public Update withFromBeforeSet(boolean fromBeforeSet) {
setFromBeforeSet(fromBeforeSet);
return this;
}

/** Whether the Teradata target names an alias declared in this UPDATE's FROM clause. */
public boolean isTargetTableAlias() {
if (!fromBeforeSet || table == null) {
return false;
}
if (matchesTargetAlias(fromItem)) {
return true;
}
return joins != null
&& joins.stream().anyMatch(join -> matchesTargetAlias(join.getRightItem()));

Check warning on line 217 in src/main/java/net/sf/jsqlparser/statement/update/Update.java

View workflow job for this annotation

GitHub Actions / Maven Verify (ubuntu-latest)

[deprecation] getRightItem() in Join has been deprecated

Check warning on line 217 in src/main/java/net/sf/jsqlparser/statement/update/Update.java

View workflow job for this annotation

GitHub Actions / Maven Verify (windows-latest)

[deprecation] getRightItem() in Join has been deprecated

Check warning on line 217 in src/main/java/net/sf/jsqlparser/statement/update/Update.java

View workflow job for this annotation

GitHub Actions / Maven Verify (macos-latest)

[deprecation] getRightItem() in Join has been deprecated
}

private boolean matchesTargetAlias(FromItem source) {
return source != null && source.getAlias() != null
&& table.getFullyQualifiedName().equalsIgnoreCase(source.getAlias().getName());
}

/** Shared FROM rendering for both supported clause positions. */
public StringBuilder appendFromTo(StringBuilder builder) {
if (fromItem != null) {
builder.append(" FROM ").append(fromItem);
if (joins != null) {
for (Join join : joins) {
builder.append(join.isSimple() ? ", " : " ").append(join);
}
}
}
return builder;
}

public List<Join> getJoins() {
return joins;
}
Expand Down Expand Up @@ -337,24 +382,18 @@
}
}

if (fromBeforeSet) {
appendFromTo(b);
}
b.append(" SET ");
UpdateSet.appendUpdateSetsTo(b, updateSets);

if (outputClause != null) {
outputClause.appendTo(b);
}

if (fromItem != null) {
b.append(" FROM ").append(fromItem);
if (joins != null) {
for (Join join : joins) {
if (join.isSimple()) {
b.append(", ").append(join);
} else {
b.append(" ").append(join);
}
}
}
if (!fromBeforeSet) {
appendFromTo(b);
}

if (where != null) {
Expand Down
4 changes: 3 additions & 1 deletion src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java
Original file line number Diff line number Diff line change
Expand Up @@ -1386,7 +1386,9 @@ public <S> Void visit(Update update, S context) {
}
}

visit(update.getTable(), context);
if (!update.isTargetTableAlias()) {
visit(update.getTable(), context);
}

if (update.getStartJoins() != null) {
for (Join join : update.getStartJoins()) {
Expand Down
16 changes: 5 additions & 11 deletions src/main/java/net/sf/jsqlparser/util/deparser/UpdateDeParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ public void deParse(Update update) {
}
}
}
if (update.isFromBeforeSet()) {
update.appendFromTo(builder);
}
builder.append(" SET ");

deparseUpdateSetsClause(update);
Expand All @@ -78,17 +81,8 @@ public void deParse(Update update) {
update.getOutputClause().appendTo(builder);
}

if (update.getFromItem() != null) {
builder.append(" FROM ").append(update.getFromItem());
if (update.getJoins() != null) {
for (Join join : update.getJoins()) {
if (join.isSimple()) {
builder.append(", ").append(join);
} else {
builder.append(" ").append(join);
}
}
}
if (!update.isFromBeforeSet()) {
update.appendFromTo(builder);
}

deparseWhereClause(update);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ public void validate(Update update) {
Feature.updateReturning);
}

validateOptionalFromItem(update.getTable());
if (!update.isTargetTableAlias()) {
validateOptionalFromItem(update.getTable());
}

validateOptional(update.getStartJoins(),
j -> getValidator(SelectValidator.class).validateOptionalJoins(j));
Expand Down
22 changes: 15 additions & 7 deletions src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt
Original file line number Diff line number Diff line change
Expand Up @@ -4574,8 +4574,6 @@ Update Update():
List<UpdateSet> updateSets;
Expression where = null;
PreferringClause preferringClause = null;
FromItem fromItem = null;
List<Join> joins = null;
Limit limit = null;
OptionClause optionClause = null;
List<OrderByElement> orderByElements;
Expand All @@ -4592,13 +4590,14 @@ Update Update():
[ LOOKAHEAD(2) <K_LOW_PRIORITY> { modifierPriority = UpdateModifierPriority.LOW_PRIORITY; }]
[ LOOKAHEAD(2) <K_IGNORE> { modifierIgnore = true; }]
table=TableWithAliasAndMysqlIndexHint() [ startJoins=JoinsList() ]
[ LOOKAHEAD(<K_FROM>, { Dialect.TERADATA.name().equals(getAsString(Feature.dialect)) })
UpdateFromClause(update) { update.setFromBeforeSet(true); } ]
<K_SET> updateSets = UpdateSets() { update.setUpdateSets(updateSets); }

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

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

[ where=WhereClause() { update.setWhere(where); } ]
[ preferringClause=PreferringClause() { update.setPreferringClause(preferringClause); } ]
Expand All @@ -4612,13 +4611,22 @@ Update Update():
return update.withWithItemsList(with)
.withTable(table)
.withStartJoins(startJoins)
.withFromItem(fromItem)
.withJoins(joins)
.withModifierPriority(modifierPriority)
.withModifierIgnore(modifierIgnore);
}
}

void UpdateFromClause(Update update):
{
FromItem fromItem;
List<Join> joins = null;
}
{
<K_FROM> fromItem=FromItem()
[ LOOKAHEAD(2) joins=JoinsList() ]
{ update.setFromItem(fromItem); update.setJoins(joins); }
}

List<UpdateSet> UpdateSets():
{
ArrayList<UpdateSet> updateSets = new ArrayList<UpdateSet>();
Expand Down
8 changes: 8 additions & 0 deletions src/site/sphinx/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -738,6 +738,8 @@ One grammar covers every supported RDBMS, but a few pieces of syntax mean differ
- ``JOIN [shuffle]`` and ``JOIN [broadcast]`` distribution hints
* - ``COCKROACHDB``
- ``ALTER TABLE ... ALTER PRIMARY KEY USING COLUMNS (...)`` with optional hash sharding and storage parameters
* - ``TERADATA``
- ``UPDATE target FROM sources SET ...`` with the FROM clause before SET

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

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

With ``Dialect.TERADATA``, ``UPDATE a FROM target a, source b SET a.id = b.id``
uses the existing ``Update`` model's ``fromItem`` and ``joins`` properties.
``isFromBeforeSet()`` preserves the clause position in both SQL renderers.
Table discovery and metadata validation recognize a target alias declared in
that FROM clause. Other dialects retain the existing FROM-after-SET syntax.

With ``Dialect.SQLSERVER``, ``PRIMARY KEY NONCLUSTERED (id)`` and
``UNIQUE CLUSTERED (id)`` store their clustering option in ``Index.getClustering()``
for both ``CREATE TABLE`` and ``ALTER TABLE``. Without that dialect, these words
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*-
* #%L
* JSQLParser library
* %%
* Copyright (C) 2004 - 2026 JSQLParser
* %%
* Dual licensed under GNU LGPL 2.1 or Apache License 2.0
* #L%
*/
package net.sf.jsqlparser.statement.update;

import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed;
import static org.junit.jupiter.api.Assertions.*;

import java.util.Set;
import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.expression.LongValue;
import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.statement.Statement;
import net.sf.jsqlparser.util.TablesNamesFinder;
import net.sf.jsqlparser.util.deparser.ExpressionDeParser;
import net.sf.jsqlparser.util.deparser.SelectDeParser;
import net.sf.jsqlparser.util.deparser.StatementDeParser;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

class TeradataUpdateTest {
private static Update parse(String sql) throws JSQLParserException {
return (Update) CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.TERADATA));
}

@Test
void parsesIssue891AndFindsSourceTables() throws Exception {
String sql = "UPDATE a FROM db1.table1 a, db2.tabl2 b SET a.column1 = b.column1 "
+ "WHERE a.column2 = b.column2";
Update update = (Update) assertSqlCanBeParsedAndDeparsed(sql, true,
p -> p.withDialect(Dialect.TERADATA));
assertTrue(update.isFromBeforeSet());
assertTrue(update.isTargetTableAlias());
assertEquals("a", update.getTable().getName());
assertEquals(1, update.getJoins().size());
assertEquals(Set.of("db1.table1", "db2.tabl2"), new TablesNamesFinder().getTables(update));
assertEquals(sql, parse(update.toString()).toString());
}

@ParameterizedTest
@ValueSource(strings = {
"UPDATE t FROM source s SET id = s.id",
"UPDATE b FROM source a, target b SET b.id = a.id WHERE a.id > 1",
"UPDATE \"target\" FROM \"schema\".\"table\" AS \"target\" SET id = 1",
"UPDATE t SET id = 1"})
void roundTripsTargetsAndOptionalFrom(String sql) throws Exception {
Statement statement = assertSqlCanBeParsedAndDeparsed(sql, true,
p -> p.withDialect(Dialect.TERADATA));
assertEquals(statement.toString(), parse(statement.toString()).toString());
}

@Test
void gatesClauseOrderAndRejectsDuplicates() throws Exception {
String sql = "UPDATE t FROM s SET id = 1";
assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql));
for (Dialect dialect : Dialect.values()) {
if (dialect != Dialect.TERADATA) {
assertThrows(JSQLParserException.class,
() -> CCJSqlParserUtil.parse(sql, p -> p.withDialect(dialect)));
}
}
assertThrows(JSQLParserException.class, () -> parse(sql + " FROM other"));
assertThrows(JSQLParserException.class, () -> parse("UPDATE t FROM SET id = 1"));
Update legacy = (Update) assertSqlCanBeParsedAndDeparsed("UPDATE t SET id = 1 FROM s");
assertFalse(legacy.isFromBeforeSet());
legacy.setFromBeforeSet(true);
assertEquals(sql, legacy.toString());
assertEquals(2, CCJSqlParserUtil.parseStatements(sql + "; SELECT 1;",
p -> p.withDialect(Dialect.TERADATA)).size());
}

@Test
void customDeparserKeepsFromPosition() throws Exception {
Update update = parse("UPDATE t FROM s SET id = 1 WHERE s.id > 2");
StringBuilder builder = new StringBuilder();
ExpressionDeParser expressions = new ExpressionDeParser() {
@Override
public <S> StringBuilder visit(LongValue value, S context) {
return getBuilder().append(value.getValue() + 10);
}
};
update.accept(new StatementDeParser(expressions, new SelectDeParser(), builder));
assertEquals("UPDATE t FROM s SET id = 11 WHERE s.id > 12", builder.toString());
assertEquals(builder.toString(), parse(builder.toString()).toString());
update.withFromBeforeSet(false);
assertEquals("UPDATE t SET id = 1 FROM s WHERE s.id > 2", update.toString());
}
}
Loading