diff --git a/src/main/java/net/sf/jsqlparser/schema/Column.java b/src/main/java/net/sf/jsqlparser/schema/Column.java index 1c1427c86..33240ac52 100644 --- a/src/main/java/net/sf/jsqlparser/schema/Column.java +++ b/src/main/java/net/sf/jsqlparser/schema/Column.java @@ -12,12 +12,12 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; - import net.sf.jsqlparser.expression.ArrayConstructor; import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.expression.ExpressionVisitor; import net.sf.jsqlparser.expression.operators.relational.SupportsOldOracleJoinSyntax; import net.sf.jsqlparser.parser.ASTNodeAccessImpl; +import net.sf.jsqlparser.statement.ReturningReferenceType; /** * A column. It can have the table name it belongs to. @@ -30,6 +30,8 @@ public class Column extends ASTNodeAccessImpl implements Expression, MultiPartNa private ArrayConstructor arrayConstructor; private String tableDelimiter = "."; private int oldOracleJoinSyntax = SupportsOldOracleJoinSyntax.NO_ORACLE_JOIN; + private ReturningReferenceType returningReferenceType = null; + private String returningQualifier = null; // holds the physical table when resolved against an actual schema information private Table resolvedTable = null; @@ -215,7 +217,9 @@ public String getUnquotedName() { public String getFullyQualifiedName(boolean aliases) { StringBuilder fqn = new StringBuilder(); - if (table != null) { + if (returningQualifier != null) { + fqn.append(returningQualifier); + } else if (table != null) { if (table.getAlias() != null && aliases) { fqn.append(table.getAlias().getName()); } else { @@ -284,6 +288,31 @@ public Column withOldOracleJoinSyntax(int oldOracleJoinSyntax) { return this; } + public ReturningReferenceType getReturningReferenceType() { + return returningReferenceType; + } + + public Column setReturningReferenceType(ReturningReferenceType returningReferenceType) { + this.returningReferenceType = returningReferenceType; + return this; + } + + public String getReturningQualifier() { + return returningQualifier; + } + + public Column setReturningQualifier(String returningQualifier) { + this.returningQualifier = returningQualifier; + return this; + } + + public Column withReturningReference(ReturningReferenceType returningReferenceType, + String returningQualifier) { + this.returningReferenceType = returningReferenceType; + this.returningQualifier = returningQualifier; + return this; + } + public String getCommentText() { return commentText; } diff --git a/src/main/java/net/sf/jsqlparser/statement/ReturningClause.java b/src/main/java/net/sf/jsqlparser/statement/ReturningClause.java index c84a07903..4c55daba8 100644 --- a/src/main/java/net/sf/jsqlparser/statement/ReturningClause.java +++ b/src/main/java/net/sf/jsqlparser/statement/ReturningClause.java @@ -9,10 +9,18 @@ */ package net.sf.jsqlparser.statement; -import net.sf.jsqlparser.statement.select.SelectItem; - import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import net.sf.jsqlparser.expression.ExpressionVisitorAdapter; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.schema.MultiPartName; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.select.AllTableColumns; +import net.sf.jsqlparser.statement.select.SelectItem; /** * RETURNING clause according to > { * List of output targets like Table or UserVariable */ private final List dataItems; + private final List outputAliases; private Keyword keyword; public ReturningClause(Keyword keyword, List> selectItems, List dataItems) { + this(keyword, selectItems, null, dataItems); + } + + public ReturningClause(Keyword keyword, List> selectItems, + List outputAliases, List dataItems) { this.keyword = keyword; this.addAll(selectItems); + this.outputAliases = outputAliases; this.dataItems = dataItems; + normalizeReturningReferences(); } public ReturningClause(String keyword, List> selectItems, @@ -39,12 +55,17 @@ public ReturningClause(String keyword, List> selectItems, this(Keyword.from(keyword), selectItems, dataItems); } + public ReturningClause(String keyword, List> selectItems, + List outputAliases, List dataItems) { + this(Keyword.from(keyword), selectItems, outputAliases, dataItems); + } + public ReturningClause(Keyword keyword, List> selectItems) { - this(keyword, selectItems, null); + this(keyword, selectItems, null, null); } public ReturningClause(String keyword, List> selectItems) { - this(Keyword.valueOf(keyword), selectItems, null); + this(Keyword.from(keyword), selectItems, null, null); } public Keyword getKeyword() { @@ -60,8 +81,22 @@ public List getDataItems() { return dataItems; } + public List getOutputAliases() { + return outputAliases; + } + public StringBuilder appendTo(StringBuilder builder) { builder.append(" ").append(keyword).append(" "); + if (outputAliases != null && !outputAliases.isEmpty()) { + builder.append("WITH ("); + for (int i = 0; i < outputAliases.size(); i++) { + if (i > 0) { + builder.append(", "); + } + builder.append(outputAliases.get(i)); + } + builder.append(") "); + } for (int i = 0; i < size(); i++) { if (i > 0) { builder.append(", "); @@ -86,6 +121,126 @@ public String toString() { return appendTo(new StringBuilder()).toString(); } + private void normalizeReturningReferences() { + Map qualifierMap = buildQualifierMap(); + if (qualifierMap.isEmpty()) { + return; + } + + ReturningReferenceNormalizer normalizer = new ReturningReferenceNormalizer(qualifierMap); + forEach(selectItem -> { + if (selectItem != null && selectItem.getExpression() != null) { + selectItem.getExpression().accept(normalizer, null); + } + }); + } + + private Map buildQualifierMap() { + LinkedHashMap qualifierMap = new LinkedHashMap<>(); + + if (outputAliases == null || outputAliases.isEmpty()) { + qualifierMap.put(QualifierKey.from("OLD"), ReturningReferenceType.OLD); + qualifierMap.put(QualifierKey.from("NEW"), ReturningReferenceType.NEW); + return qualifierMap; + } + + for (ReturningOutputAlias outputAlias : outputAliases) { + if (outputAlias == null || outputAlias.getAlias() == null + || outputAlias.getReferenceType() == null) { + continue; + } + qualifierMap.put(QualifierKey.from(outputAlias.getAlias()), + outputAlias.getReferenceType()); + } + return qualifierMap; + } + + private static class ReturningReferenceNormalizer extends ExpressionVisitorAdapter { + private final Map qualifierMap; + + ReturningReferenceNormalizer(Map qualifierMap) { + this.qualifierMap = qualifierMap; + } + + @Override + public Void visit(Column column, S context) { + Table table = column.getTable(); + String qualifier = extractSimpleQualifier(table); + if (qualifier == null) { + return null; + } + ReturningReferenceType referenceType = qualifierMap.get(QualifierKey.from(qualifier)); + if (referenceType != null) { + column.withReturningReference(referenceType, qualifier); + column.setTable(null); + } + return null; + } + + @Override + public Void visit(AllTableColumns allTableColumns, S context) { + Table table = allTableColumns.getTable(); + String qualifier = extractSimpleQualifier(table); + if (qualifier == null) { + return null; + } + ReturningReferenceType referenceType = qualifierMap.get(QualifierKey.from(qualifier)); + if (referenceType != null) { + allTableColumns.withReturningReference(referenceType, qualifier); + allTableColumns.setTable(null); + } + return null; + } + + private String extractSimpleQualifier(Table table) { + if (table == null || table.getSchemaName() != null || table.getDatabaseName() != null) { + return null; + } + String qualifier = table.getName(); + if (qualifier == null || qualifier.contains("@")) { + return null; + } + return qualifier; + } + } + + private static class QualifierKey { + private final boolean quoted; + private final String normalizedIdentifier; + + private QualifierKey(boolean quoted, String normalizedIdentifier) { + this.quoted = quoted; + this.normalizedIdentifier = normalizedIdentifier; + } + + static QualifierKey from(String identifier) { + boolean quoted = MultiPartName.isQuoted(identifier); + String unquoted = MultiPartName.unquote(identifier); + if (!quoted && unquoted != null) { + unquoted = unquoted.toUpperCase(Locale.ROOT); + } + return new QualifierKey(quoted, unquoted); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof QualifierKey)) { + return false; + } + QualifierKey that = (QualifierKey) o; + return quoted == that.quoted + && Objects.equals(normalizedIdentifier, that.normalizedIdentifier); + } + + @Override + public int hashCode() { + return Objects.hash(quoted, normalizedIdentifier); + } + } + public enum Keyword { RETURN, RETURNING; diff --git a/src/main/java/net/sf/jsqlparser/statement/ReturningOutputAlias.java b/src/main/java/net/sf/jsqlparser/statement/ReturningOutputAlias.java new file mode 100644 index 000000000..d0c42de34 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/ReturningOutputAlias.java @@ -0,0 +1,66 @@ +/*- + * #%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; + +import java.util.Objects; + +public class ReturningOutputAlias { + private ReturningReferenceType referenceType; + private String alias; + + public ReturningOutputAlias(ReturningReferenceType referenceType, String alias) { + this.referenceType = referenceType; + this.alias = alias; + } + + public ReturningReferenceType getReferenceType() { + return referenceType; + } + + public ReturningOutputAlias setReferenceType(ReturningReferenceType referenceType) { + this.referenceType = referenceType; + return this; + } + + public String getAlias() { + return alias; + } + + public ReturningOutputAlias setAlias(String alias) { + this.alias = alias; + return this; + } + + public StringBuilder appendTo(StringBuilder builder) { + return builder.append(referenceType).append(" AS ").append(alias); + } + + @Override + public String toString() { + return appendTo(new StringBuilder()).toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof ReturningOutputAlias)) { + return false; + } + ReturningOutputAlias that = (ReturningOutputAlias) o; + return referenceType == that.referenceType && Objects.equals(alias, that.alias); + } + + @Override + public int hashCode() { + return Objects.hash(referenceType, alias); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/ReturningReferenceType.java b/src/main/java/net/sf/jsqlparser/statement/ReturningReferenceType.java new file mode 100644 index 000000000..06079904a --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/ReturningReferenceType.java @@ -0,0 +1,30 @@ +/*- + * #%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; + +import net.sf.jsqlparser.schema.MultiPartName; + +public enum ReturningReferenceType { + OLD, NEW; + + public static ReturningReferenceType from(String name) { + String unquoted = MultiPartName.unquote(name); + if (unquoted == null) { + return null; + } + if ("OLD".equalsIgnoreCase(unquoted)) { + return OLD; + } + if ("NEW".equalsIgnoreCase(unquoted)) { + return NEW; + } + return null; + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/select/AllTableColumns.java b/src/main/java/net/sf/jsqlparser/statement/select/AllTableColumns.java index 85e517aa1..0030da89b 100644 --- a/src/main/java/net/sf/jsqlparser/statement/select/AllTableColumns.java +++ b/src/main/java/net/sf/jsqlparser/statement/select/AllTableColumns.java @@ -9,16 +9,18 @@ */ package net.sf.jsqlparser.statement.select; +import java.util.List; import net.sf.jsqlparser.expression.ExpressionVisitor; import net.sf.jsqlparser.expression.operators.relational.ExpressionList; import net.sf.jsqlparser.schema.Column; import net.sf.jsqlparser.schema.Table; - -import java.util.List; +import net.sf.jsqlparser.statement.ReturningReferenceType; public class AllTableColumns extends AllColumns { private Table table; + private ReturningReferenceType returningReferenceType = null; + private String returningQualifier = null; public AllTableColumns(Table table, ExpressionList exceptColumns, List> replaceExpressions, String exceptKeyword) { @@ -55,11 +57,43 @@ public AllTableColumns withTable(Table table) { @Override public StringBuilder appendTo(StringBuilder builder) { - return super.appendTo(table.appendTo(builder).append(".")); + if (returningQualifier != null) { + return super.appendTo(builder.append(returningQualifier).append(".")); + } + if (table != null) { + return super.appendTo(table.appendTo(builder).append(".")); + } + return super.appendTo(builder); } @Override public T accept(ExpressionVisitor expressionVisitor, S context) { return expressionVisitor.visit(this, context); } + + public ReturningReferenceType getReturningReferenceType() { + return returningReferenceType; + } + + public AllTableColumns setReturningReferenceType( + ReturningReferenceType returningReferenceType) { + this.returningReferenceType = returningReferenceType; + return this; + } + + public String getReturningQualifier() { + return returningQualifier; + } + + public AllTableColumns setReturningQualifier(String returningQualifier) { + this.returningQualifier = returningQualifier; + return this; + } + + public AllTableColumns withReturningReference(ReturningReferenceType returningReferenceType, + String returningQualifier) { + this.returningReferenceType = returningReferenceType; + this.returningQualifier = returningQualifier; + return this; + } } diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 7a78f84e4..45a2d3fe5 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -2626,12 +2626,14 @@ Values Values(): { ReturningClause ReturningClause(): { Token keyword; + List outputAliases = null; List> selectItems; Object dataItem; List dataItems = null; } { ( keyword= | keyword= ) + [ outputAliases = ReturningOutputAliasList() ] selectItems = SelectItemsList() [ @@ -2646,7 +2648,57 @@ ReturningClause ReturningClause(): ] { - return new ReturningClause(keyword.image, selectItems, dataItems); + return new ReturningClause(keyword.image, selectItems, outputAliases, dataItems); + } +} + +ReturningReferenceType ReturningReferenceKind(): +{ + String refName; + ReturningReferenceType refType; +} +{ + refName = RelObjectNameWithoutValue() + { + refType = ReturningReferenceType.from(refName); + if (refType == ReturningReferenceType.OLD) { + return ReturningReferenceType.OLD; + } else if (refType == ReturningReferenceType.NEW) { + return ReturningReferenceType.NEW; + } + throw new ParseException("Expected OLD or NEW but found: " + refName); + } +} + +ReturningOutputAlias ReturningOutputAliasDefinition(): +{ + ReturningReferenceType refType; + String aliasName; +} +{ + refType = ReturningReferenceKind() + + aliasName = RelObjectNameWithoutStart() + { + return new ReturningOutputAlias(refType, aliasName); + } +} + +List ReturningOutputAliasList(): +{ + List outputAliases = new ArrayList(); + ReturningOutputAlias outputAlias; +} +{ + "(" + outputAlias = ReturningOutputAliasDefinition() { outputAliases.add(outputAlias); } + ( + "," + outputAlias = ReturningOutputAliasDefinition() { outputAliases.add(outputAlias); } + )* + ")" + { + return outputAliases; } } diff --git a/src/test/java/net/sf/jsqlparser/statement/ReturningClauseTest.java b/src/test/java/net/sf/jsqlparser/statement/ReturningClauseTest.java index 331f7263b..6c29f5e41 100644 --- a/src/test/java/net/sf/jsqlparser/statement/ReturningClauseTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/ReturningClauseTest.java @@ -9,7 +9,14 @@ */ package net.sf.jsqlparser.statement; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.statement.insert.Insert; +import net.sf.jsqlparser.statement.select.AllTableColumns; +import net.sf.jsqlparser.statement.update.Update; import net.sf.jsqlparser.test.TestUtils; import org.junit.jupiter.api.Test; @@ -25,4 +32,60 @@ void returnIntoTest() throws JSQLParserException { TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); } + @Test + void returningOldNewDefaultReferencesTest() throws JSQLParserException { + String sqlStr = "UPDATE products SET price = price * 1.10 " + + "RETURNING old.price AS old_price, new.price AS new_price, new.*"; + Update update = (Update) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + ReturningClause returningClause = update.getReturningClause(); + assertNull(returningClause.getOutputAliases()); + + Column oldPrice = returningClause.get(0).getExpression(Column.class); + assertNull(oldPrice.getTable()); + assertEquals(ReturningReferenceType.OLD, oldPrice.getReturningReferenceType()); + assertEquals("old", oldPrice.getReturningQualifier()); + + Column newPrice = returningClause.get(1).getExpression(Column.class); + assertNull(newPrice.getTable()); + assertEquals(ReturningReferenceType.NEW, newPrice.getReturningReferenceType()); + assertEquals("new", newPrice.getReturningQualifier()); + + AllTableColumns allNew = returningClause.get(2).getExpression(AllTableColumns.class); + assertNull(allNew.getTable()); + assertEquals(ReturningReferenceType.NEW, allNew.getReturningReferenceType()); + assertEquals("new", allNew.getReturningQualifier()); + } + + @Test + void returningWithOutputAliasesTest() throws JSQLParserException { + String sqlStr = "INSERT INTO products (price) VALUES (99.99) " + + "RETURNING WITH (OLD AS o, NEW AS n) o.price AS old_price, n.price AS new_price, n.*"; + Insert insert = (Insert) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + ReturningClause returningClause = insert.getReturningClause(); + assertEquals(2, returningClause.getOutputAliases().size()); + assertEquals(ReturningReferenceType.OLD, + returningClause.getOutputAliases().get(0).getReferenceType()); + assertEquals("o", returningClause.getOutputAliases().get(0).getAlias()); + assertEquals(ReturningReferenceType.NEW, + returningClause.getOutputAliases().get(1).getReferenceType()); + assertEquals("n", returningClause.getOutputAliases().get(1).getAlias()); + + Column oldPrice = returningClause.get(0).getExpression(Column.class); + assertNull(oldPrice.getTable()); + assertEquals(ReturningReferenceType.OLD, oldPrice.getReturningReferenceType()); + assertEquals("o", oldPrice.getReturningQualifier()); + + Column newPrice = returningClause.get(1).getExpression(Column.class); + assertNull(newPrice.getTable()); + assertEquals(ReturningReferenceType.NEW, newPrice.getReturningReferenceType()); + assertEquals("n", newPrice.getReturningQualifier()); + + AllTableColumns allNew = returningClause.get(2).getExpression(AllTableColumns.class); + assertNull(allNew.getTable()); + assertEquals(ReturningReferenceType.NEW, allNew.getReturningReferenceType()); + assertEquals("n", allNew.getReturningQualifier()); + } + }