From 01bd0901c940be334bf03f54380a407a400db0eb Mon Sep 17 00:00:00 2001 From: minleejae Date: Fri, 11 Sep 2026 14:24:16 +0900 Subject: [PATCH] Support Teradata UPDATE FROM before SET with shared clause rendering --- .../jsqlparser/parser/AbstractJSqlParser.java | 2 +- .../jsqlparser/statement/update/Update.java | 61 +++++++++--- .../sf/jsqlparser/util/TablesNamesFinder.java | 4 +- .../util/deparser/UpdateDeParser.java | 16 +--- .../validation/validator/UpdateValidator.java | 4 +- .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 22 +++-- src/site/sphinx/usage.rst | 8 ++ .../statement/update/TeradataUpdateTest.java | 96 +++++++++++++++++++ 8 files changed, 181 insertions(+), 32 deletions(-) create mode 100644 src/test/java/net/sf/jsqlparser/statement/update/TeradataUpdateTest.java diff --git a/src/main/java/net/sf/jsqlparser/parser/AbstractJSqlParser.java b/src/main/java/net/sf/jsqlparser/parser/AbstractJSqlParser.java index 8ce5c93e86..f5d030a56a 100644 --- a/src/main/java/net/sf/jsqlparser/parser/AbstractJSqlParser.java +++ b/src/main/java/net/sf/jsqlparser/parser/AbstractJSqlParser.java @@ -46,7 +46,7 @@ public enum Dialect { AdjacentStringLiterals.WHITESPACE, Feature.allowDoubleQuotedStrings, Feature.allowBackslashEscapeCharacter), SNOWFLAKE( - Feature.allowBackslashEscapeCharacter), INFORMIX; + Feature.allowBackslashEscapeCharacter), INFORMIX, TERADATA; private final Set lexerFeatures; private final AdjacentStringLiterals adjacentStringLiterals; diff --git a/src/main/java/net/sf/jsqlparser/statement/update/Update.java b/src/main/java/net/sf/jsqlparser/statement/update/Update.java index 03d9d25800..3849940a54 100644 --- a/src/main/java/net/sf/jsqlparser/statement/update/Update.java +++ b/src/main/java/net/sf/jsqlparser/statement/update/Update.java @@ -44,6 +44,7 @@ public class Update implements Statement { private PreferringClause preferringClause; private List updateSets; private FromItem fromItem; + private boolean fromBeforeSet; private List joins; private List startJoins; private OracleHint oracleHint = null; @@ -190,6 +191,50 @@ public void setFromItem(FromItem fromItem) { 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())); + } + + 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 getJoins() { return joins; } @@ -337,6 +382,9 @@ public String toString() { } } + if (fromBeforeSet) { + appendFromTo(b); + } b.append(" SET "); UpdateSet.appendUpdateSetsTo(b, updateSets); @@ -344,17 +392,8 @@ public String toString() { 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) { diff --git a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java index 2838566809..3ecac2e4c2 100644 --- a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java +++ b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java @@ -1382,7 +1382,9 @@ public 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()) { diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/UpdateDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/UpdateDeParser.java index b18d58090b..a6d9c2670c 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/UpdateDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/UpdateDeParser.java @@ -70,6 +70,9 @@ public void deParse(Update update) { } } } + if (update.isFromBeforeSet()) { + update.appendFromTo(builder); + } builder.append(" SET "); deparseUpdateSetsClause(update); @@ -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); diff --git a/src/main/java/net/sf/jsqlparser/util/validation/validator/UpdateValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/UpdateValidator.java index 5701b4b7c4..7d27e94b3e 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/validator/UpdateValidator.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/UpdateValidator.java @@ -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)); diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index e89a86094c..5d60f3edcf 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -4435,8 +4435,6 @@ Update Update(): List updateSets; Expression where = null; PreferringClause preferringClause = null; - FromItem fromItem = null; - List joins = null; Limit limit = null; OptionClause optionClause = null; List orderByElements; @@ -4453,13 +4451,14 @@ Update Update(): [ LOOKAHEAD(2) { modifierPriority = UpdateModifierPriority.LOW_PRIORITY; }] [ LOOKAHEAD(2) { modifierIgnore = true; }] table=TableWithAliasAndMysqlIndexHint() [ startJoins=JoinsList() ] + [ LOOKAHEAD(, { Dialect.TERADATA.name().equals(getAsString(Feature.dialect)) }) + UpdateFromClause(update) { update.setFromBeforeSet(true); } ] updateSets = UpdateSets() { update.setUpdateSets(updateSets); } [ outputClause = OutputClause() {update.setOutputClause(outputClause); } ] - [ LOOKAHEAD(2) - 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); } ] @@ -4473,13 +4472,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 joins = null; +} +{ + fromItem=FromItem() + [ LOOKAHEAD(2) joins=JoinsList() ] + { update.setFromItem(fromItem); update.setJoins(joins); } +} + List UpdateSets(): { ArrayList updateSets = new ArrayList(); diff --git a/src/site/sphinx/usage.rst b/src/site/sphinx/usage.rst index 6d3995c0c8..db285209f8 100644 --- a/src/site/sphinx/usage.rst +++ b/src/site/sphinx/usage.rst @@ -713,9 +713,17 @@ One grammar covers every supported RDBMS, but a few pieces of syntax mean differ - ``withBackslashEscapeCharacter`` only, double quotes stay quoted identifiers * - ``INFORMIX`` - Informix ``ALTER TABLE ... ADD CONSTRAINT`` definitions with optional trailing constraint names + * - ``TERADATA`` + - ``UPDATE target FROM sources SET ...`` with the FROM clause before SET Features set explicitly *after* the preset win over it. +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 diff --git a/src/test/java/net/sf/jsqlparser/statement/update/TeradataUpdateTest.java b/src/test/java/net/sf/jsqlparser/statement/update/TeradataUpdateTest.java new file mode 100644 index 0000000000..7c033fc057 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/update/TeradataUpdateTest.java @@ -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 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()); + } +}