From 5d7fb193c4e08ecfef80b919a00d085442696dd6 Mon Sep 17 00:00:00 2001 From: morrySnow Date: Tue, 24 Mar 2026 18:58:42 +0800 Subject: [PATCH] [feature](fe) Support ORDER BY and LIMIT clauses for UPDATE and DELETE commands ### What problem does this PR solve? Issue Number: close #xxx Problem Summary: MySQL supports ORDER BY and LIMIT clauses in single-table UPDATE and DELETE statements, but Doris does not. This PR adds support for these clauses so users can write statements like: DELETE FROM table1 ORDER BY c1 ASC NULLS FIRST LIMIT 10, 3; UPDATE table1 SET c1 = 10 ORDER BY c2 LIMIT 100, 20; The implementation reuses the existing queryOrganization grammar rule and withQueryOrganization builder method. For DELETE, when ORDER BY or LIMIT is present, the statement is routed through DeleteFromUsingCommand (INSERT INTO SELECT path) which naturally supports sort/limit semantics. For UPDATE, the queryOrganization is applied to the query plan after withFilter. ### Release note Support ORDER BY and LIMIT clauses in UPDATE and DELETE statements, consistent with MySQL single-table UPDATE/DELETE syntax. ### Check List (For Author) - Test: Regression test / Unit Test - Parser syntax tests in NereidsParserTest - Plan tree structure tests in LogicalPlanBuilderTest - DELETE regression tests in test_delete_order_by_limit.groovy - UPDATE regression tests in test_update_order_by_limit.groovy - Behavior changed: No - Does this need documentation: No Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../org/apache/doris/nereids/DorisParser.g4 | 6 +- .../nereids/parser/LogicalPlanBuilder.java | 43 ++- .../plans/commands/DeleteFromCommand.java | 4 +- .../commands/DeleteFromUsingCommand.java | 12 +- .../parser/LogicalPlanBuilderTest.java | 251 ++++++++++++++++++ .../nereids/parser/NereidsParserTest.java | 79 ++++++ .../delete_p0/test_delete_order_by_limit.out | 61 +++++ .../update/test_update_order_by_limit.out | 85 ++++++ .../test_delete_order_by_limit.groovy | 178 +++++++++++++ .../update/test_update_order_by_limit.groovy | 175 ++++++++++++ 10 files changed, 886 insertions(+), 8 deletions(-) create mode 100644 fe/fe-core/src/test/java/org/apache/doris/nereids/parser/LogicalPlanBuilderTest.java create mode 100644 regression-test/data/delete_p0/test_delete_order_by_limit.out create mode 100644 regression-test/data/update/test_update_order_by_limit.out create mode 100644 regression-test/suites/delete_p0/test_delete_order_by_limit.groovy create mode 100644 regression-test/suites/update/test_update_order_by_limit.groovy diff --git a/fe/fe-core/src/main/antlr4/org/apache/doris/nereids/DorisParser.g4 b/fe/fe-core/src/main/antlr4/org/apache/doris/nereids/DorisParser.g4 index e2e7a87d99f863..65916ff1082073 100644 --- a/fe/fe-core/src/main/antlr4/org/apache/doris/nereids/DorisParser.g4 +++ b/fe/fe-core/src/main/antlr4/org/apache/doris/nereids/DorisParser.g4 @@ -147,11 +147,13 @@ supportedDmlStatement | explain? cte? UPDATE tableName=multipartIdentifier tableAlias SET updateAssignmentSeq fromClause? - whereClause? #update + whereClause? + queryOrganization #update | explain? cte? DELETE FROM tableName=multipartIdentifier partitionSpec? tableAlias (USING relations)? - whereClause? #delete + whereClause? + queryOrganization #delete | explain? cte? MERGE INTO targetTable=multipartIdentifier (AS? identifier)? USING srcRelation=relationPrimary ON expression diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/LogicalPlanBuilder.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/LogicalPlanBuilder.java index 4a15f1736fed8d..7c5b51b6f6fd76 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/LogicalPlanBuilder.java +++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/LogicalPlanBuilder.java @@ -594,6 +594,7 @@ import org.apache.doris.nereids.trees.expressions.literal.DecimalLiteral; import org.apache.doris.nereids.trees.expressions.literal.DecimalV3Literal; import org.apache.doris.nereids.trees.expressions.literal.DoubleLiteral; +import org.apache.doris.nereids.trees.expressions.literal.IntegerLikeLiteral; import org.apache.doris.nereids.trees.expressions.literal.IntegerLiteral; import org.apache.doris.nereids.trees.expressions.literal.Interval; import org.apache.doris.nereids.trees.expressions.literal.LargeIntLiteral; @@ -2030,6 +2031,8 @@ public LogicalPlan visitUpdate(UpdateContext ctx) { query = withRelations(query, ((FromRelationsContext) ctx.fromClause()).relations().relation()); } query = withFilter(query, Optional.ofNullable(ctx.whereClause())); + query = withQueryOrganization(query, ctx.queryOrganization()); + query = convertSortOrdinalsToUnboundSlot(query); String tableAlias = null; if (ctx.tableAlias().strictIdentifier() != null) { tableAlias = ctx.tableAlias().strictIdentifier().getText(); @@ -2060,8 +2063,11 @@ public LogicalPlan visitDelete(DeleteContext ctx) { tableAlias = ctx.tableAlias().strictIdentifier().getText(); } + boolean hasQueryOrganization = ctx.queryOrganization() != null + && (ctx.queryOrganization().sortClause() != null + || ctx.queryOrganization().limitClause() != null); Command deleteCommand; - if (ctx.USING() == null && ctx.cte() == null) { + if (ctx.USING() == null && ctx.cte() == null && !hasQueryOrganization) { query = withFilter(query, Optional.ofNullable(ctx.whereClause())); deleteCommand = new DeleteFromCommand(tableName, tableAlias, partitionSpec.first, partitionSpec.second, query); @@ -2071,12 +2077,14 @@ public LogicalPlan visitDelete(DeleteContext ctx) { query = withRelations(query, ctx.relations().relation()); } query = withFilter(query, Optional.ofNullable(ctx.whereClause())); + query = withQueryOrganization(query, ctx.queryOrganization()); + query = convertSortOrdinalsToUnboundSlot(query); Optional cte = Optional.empty(); if (ctx.cte() != null) { cte = Optional.ofNullable(withCte(query, ctx.cte())); } deleteCommand = new DeleteFromUsingCommand(tableName, tableAlias, - partitionSpec.first, partitionSpec.second, query, cte); + partitionSpec.first, partitionSpec.second, query, cte, hasQueryOrganization); } if (ctx.explain() != null) { return withExplain(deleteCommand, ctx.explain()); @@ -4386,6 +4394,37 @@ private LogicalPlan withSort(LogicalPlan input, Optional sort }); } + /** + * Convert IntegerLikeLiteral expressions in ORDER BY keys to UnboundSlot. + * In SELECT queries, ORDER BY with an integer is treated as an ordinal (position reference). + * In DELETE/UPDATE commands, there is no user-specified SELECT list, so ordinal resolution + * would be meaningless. Convert integer literals to UnboundSlot to prevent the ordinal + * interpretation by BindExpression. + */ + private LogicalPlan convertSortOrdinalsToUnboundSlot(LogicalPlan plan) { + if (plan instanceof LogicalSort) { + LogicalSort sort = (LogicalSort) plan; + List newOrderKeys = sort.getOrderKeys().stream() + .map(key -> { + if (key.getExpr() instanceof IntegerLikeLiteral) { + return key.withExpression( + new UnboundSlot(String.valueOf( + ((IntegerLikeLiteral) key.getExpr()).getIntValue()))); + } + return key; + }) + .collect(ImmutableList.toImmutableList()); + return sort.withOrderKeys(newOrderKeys); + } else if (plan instanceof LogicalLimit) { + LogicalPlan child = (LogicalPlan) ((LogicalLimit) plan).child(); + LogicalPlan newChild = convertSortOrdinalsToUnboundSlot(child); + if (newChild != child) { + return (LogicalPlan) ((LogicalLimit) plan).withChildren(newChild); + } + } + return plan; + } + private LogicalPlan withLimit(LogicalPlan input, Optional limitCtx) { return input.optionalMap(limitCtx, () -> { long limit = Long.parseLong(limitCtx.get().limit.getText()); diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/DeleteFromCommand.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/DeleteFromCommand.java index fa451743733947..aeb586af93a772 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/DeleteFromCommand.java +++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/DeleteFromCommand.java @@ -183,7 +183,7 @@ public void run(ConnectContext ctx, StmtExecutor executor) throws Exception { } catch (Exception e) { try { new DeleteFromUsingCommand(nameParts, tableAlias, isTempPart, partitions, - logicalQuery, Optional.empty()).run(ctx, executor); + logicalQuery, Optional.empty(), false).run(ctx, executor); return; } catch (Exception e2) { LOG.warn("delete from command failed", e2); @@ -195,7 +195,7 @@ public void run(ConnectContext ctx, StmtExecutor executor) throws Exception { if (olapTable.getKeysType() == KeysType.UNIQUE_KEYS && olapTable.getEnableUniqueKeyMergeOnWrite() && !olapTable.getEnableMowLightDelete()) { new DeleteFromUsingCommand(nameParts, tableAlias, isTempPart, partitions, logicalQuery, - Optional.empty()).run(ctx, executor); + Optional.empty(), false).run(ctx, executor); return; } diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/DeleteFromUsingCommand.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/DeleteFromUsingCommand.java index 764ff05c00fc94..2364c690712af3 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/DeleteFromUsingCommand.java +++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/DeleteFromUsingCommand.java @@ -35,14 +35,17 @@ */ public class DeleteFromUsingCommand extends DeleteFromCommand { private final Optional cte; + private final boolean hasOrderByLimit; /** * constructor */ public DeleteFromUsingCommand(List nameParts, String tableAlias, - boolean isTempPart, List partitions, LogicalPlan logicalQuery, Optional cte) { + boolean isTempPart, List partitions, LogicalPlan logicalQuery, + Optional cte, boolean hasOrderByLimit) { super(nameParts, tableAlias, isTempPart, partitions, logicalQuery); this.cte = cte; + this.hasOrderByLimit = hasOrderByLimit; } @Override @@ -80,7 +83,12 @@ public R accept(PlanVisitor visitor, C context) { @Override protected void checkTargetTable(OlapTable targetTable) { if (targetTable.getKeysType() != KeysType.UNIQUE_KEYS) { - throw new AnalysisException("delete command on with using clause only supports unique key model"); + if (hasOrderByLimit) { + throw new AnalysisException( + "delete command with ORDER BY/LIMIT only supports unique key model"); + } + throw new AnalysisException( + "delete command on with using clause only supports unique key model"); } } diff --git a/fe/fe-core/src/test/java/org/apache/doris/nereids/parser/LogicalPlanBuilderTest.java b/fe/fe-core/src/test/java/org/apache/doris/nereids/parser/LogicalPlanBuilderTest.java new file mode 100644 index 00000000000000..9690e24ea85b2a --- /dev/null +++ b/fe/fe-core/src/test/java/org/apache/doris/nereids/parser/LogicalPlanBuilderTest.java @@ -0,0 +1,251 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.doris.nereids.parser; + +import org.apache.doris.nereids.analyzer.UnboundSlot; +import org.apache.doris.nereids.trees.expressions.literal.IntegerLikeLiteral; +import org.apache.doris.nereids.trees.plans.commands.DeleteFromCommand; +import org.apache.doris.nereids.trees.plans.commands.DeleteFromUsingCommand; +import org.apache.doris.nereids.trees.plans.commands.UpdateCommand; +import org.apache.doris.nereids.trees.plans.logical.LogicalFilter; +import org.apache.doris.nereids.trees.plans.logical.LogicalLimit; +import org.apache.doris.nereids.trees.plans.logical.LogicalPlan; +import org.apache.doris.nereids.trees.plans.logical.LogicalSort; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +/** + * Tests for LogicalPlanBuilder to verify DELETE/UPDATE with ORDER BY and LIMIT + * produce the correct logical plan tree structure. + */ +public class LogicalPlanBuilderTest { + + private final NereidsParser parser = new NereidsParser(); + + @Test + public void testDeleteWithOrderByLimitProducesCorrectPlanTree() { + String sql = "DELETE FROM t ORDER BY c1 LIMIT 10"; + LogicalPlan plan = parser.parseSingle(sql); + Assertions.assertInstanceOf(DeleteFromUsingCommand.class, plan); + LogicalPlan query = ((DeleteFromUsingCommand) plan).getLogicalQuery(); + // plan tree: LogicalLimit -> LogicalSort -> CheckPolicy(UnboundRelation) + Assertions.assertInstanceOf(LogicalLimit.class, query); + LogicalLimit limit = (LogicalLimit) query; + Assertions.assertEquals(10, limit.getLimit()); + Assertions.assertEquals(0, limit.getOffset()); + Assertions.assertInstanceOf(LogicalSort.class, limit.child()); + LogicalSort sort = (LogicalSort) limit.child(); + Assertions.assertEquals(1, sort.getOrderKeys().size()); + } + + @Test + public void testDeleteWithOrderByLimitOffset() { + String sql = "DELETE FROM t ORDER BY c1 ASC NULLS FIRST LIMIT 10, 3"; + LogicalPlan plan = parser.parseSingle(sql); + Assertions.assertInstanceOf(DeleteFromUsingCommand.class, plan); + LogicalPlan query = ((DeleteFromUsingCommand) plan).getLogicalQuery(); + Assertions.assertInstanceOf(LogicalLimit.class, query); + LogicalLimit limit = (LogicalLimit) query; + // LIMIT offset, count: offset=10, limit=3 + Assertions.assertEquals(3, limit.getLimit()); + Assertions.assertEquals(10, limit.getOffset()); + Assertions.assertInstanceOf(LogicalSort.class, limit.child()); + } + + @Test + public void testDeleteWithWhereOrderByLimit() { + String sql = "DELETE FROM t WHERE c1 > 0 ORDER BY c1 DESC NULLS LAST LIMIT 5"; + LogicalPlan plan = parser.parseSingle(sql); + Assertions.assertInstanceOf(DeleteFromUsingCommand.class, plan); + LogicalPlan query = ((DeleteFromUsingCommand) plan).getLogicalQuery(); + // plan tree: LogicalLimit -> LogicalSort -> LogicalFilter -> CheckPolicy(UnboundRelation) + Assertions.assertInstanceOf(LogicalLimit.class, query); + LogicalLimit limit = (LogicalLimit) query; + Assertions.assertEquals(5, limit.getLimit()); + Assertions.assertInstanceOf(LogicalSort.class, limit.child()); + LogicalSort sort = (LogicalSort) limit.child(); + Assertions.assertInstanceOf(LogicalFilter.class, sort.child()); + } + + @Test + public void testDeleteWithOrderByOnlyProducesDeleteFromUsingCommand() { + String sql = "DELETE FROM t ORDER BY c1"; + LogicalPlan plan = parser.parseSingle(sql); + Assertions.assertInstanceOf(DeleteFromUsingCommand.class, plan); + LogicalPlan query = ((DeleteFromUsingCommand) plan).getLogicalQuery(); + Assertions.assertInstanceOf(LogicalSort.class, query); + } + + @Test + public void testDeleteWithLimitOnlyProducesDeleteFromUsingCommand() { + String sql = "DELETE FROM t LIMIT 5"; + LogicalPlan plan = parser.parseSingle(sql); + Assertions.assertInstanceOf(DeleteFromUsingCommand.class, plan); + LogicalPlan query = ((DeleteFromUsingCommand) plan).getLogicalQuery(); + Assertions.assertInstanceOf(LogicalLimit.class, query); + LogicalLimit limit = (LogicalLimit) query; + Assertions.assertEquals(5, limit.getLimit()); + } + + @Test + public void testDeleteWithoutOrderByLimitProducesDeleteFromCommand() { + String sql = "DELETE FROM t WHERE c1 = 1"; + LogicalPlan plan = parser.parseSingle(sql); + Assertions.assertInstanceOf(DeleteFromCommand.class, plan); + Assertions.assertFalse(plan instanceof DeleteFromUsingCommand); + } + + @Test + public void testUpdateWithOrderByLimitProducesCorrectPlanTree() { + String sql = "UPDATE t SET c1 = 10 ORDER BY c2 LIMIT 100"; + LogicalPlan plan = parser.parseSingle(sql); + Assertions.assertInstanceOf(UpdateCommand.class, plan); + LogicalPlan query = ((UpdateCommand) plan).getLogicalQuery(); + // plan tree: LogicalLimit -> LogicalSort -> CheckPolicy(UnboundRelation) + Assertions.assertInstanceOf(LogicalLimit.class, query); + LogicalLimit limit = (LogicalLimit) query; + Assertions.assertEquals(100, limit.getLimit()); + Assertions.assertEquals(0, limit.getOffset()); + Assertions.assertInstanceOf(LogicalSort.class, limit.child()); + LogicalSort sort = (LogicalSort) limit.child(); + Assertions.assertEquals(1, sort.getOrderKeys().size()); + } + + @Test + public void testUpdateWithOrderByLimitOffset() { + String sql = "UPDATE t SET c1 = 10 ORDER BY c2 LIMIT 100, 20"; + LogicalPlan plan = parser.parseSingle(sql); + Assertions.assertInstanceOf(UpdateCommand.class, plan); + LogicalPlan query = ((UpdateCommand) plan).getLogicalQuery(); + Assertions.assertInstanceOf(LogicalLimit.class, query); + LogicalLimit limit = (LogicalLimit) query; + // LIMIT offset, count: offset=100, limit=20 + Assertions.assertEquals(20, limit.getLimit()); + Assertions.assertEquals(100, limit.getOffset()); + Assertions.assertInstanceOf(LogicalSort.class, limit.child()); + } + + @Test + public void testUpdateWithWhereOrderByLimit() { + String sql = "UPDATE t SET c1 = 10 WHERE c2 > 5 ORDER BY c2 DESC LIMIT 50"; + LogicalPlan plan = parser.parseSingle(sql); + Assertions.assertInstanceOf(UpdateCommand.class, plan); + LogicalPlan query = ((UpdateCommand) plan).getLogicalQuery(); + // plan tree: LogicalLimit -> LogicalSort -> LogicalFilter -> CheckPolicy(UnboundRelation) + Assertions.assertInstanceOf(LogicalLimit.class, query); + LogicalLimit limit = (LogicalLimit) query; + Assertions.assertInstanceOf(LogicalSort.class, limit.child()); + LogicalSort sort = (LogicalSort) limit.child(); + Assertions.assertInstanceOf(LogicalFilter.class, sort.child()); + } + + @Test + public void testUpdateWithOrderByOnlyProducesCorrectPlanTree() { + String sql = "UPDATE t SET c1 = 10 ORDER BY c2"; + LogicalPlan plan = parser.parseSingle(sql); + Assertions.assertInstanceOf(UpdateCommand.class, plan); + LogicalPlan query = ((UpdateCommand) plan).getLogicalQuery(); + Assertions.assertInstanceOf(LogicalSort.class, query); + } + + @Test + public void testUpdateWithLimitOnlyProducesCorrectPlanTree() { + String sql = "UPDATE t SET c1 = 10 LIMIT 50"; + LogicalPlan plan = parser.parseSingle(sql); + Assertions.assertInstanceOf(UpdateCommand.class, plan); + LogicalPlan query = ((UpdateCommand) plan).getLogicalQuery(); + Assertions.assertInstanceOf(LogicalLimit.class, query); + LogicalLimit limit = (LogicalLimit) query; + Assertions.assertEquals(50, limit.getLimit()); + } + + @Test + public void testUpdateWithoutOrderByLimitProducesUpdateCommand() { + String sql = "UPDATE t SET c1 = 10 WHERE c2 = 1"; + LogicalPlan plan = parser.parseSingle(sql); + Assertions.assertInstanceOf(UpdateCommand.class, plan); + LogicalPlan query = ((UpdateCommand) plan).getLogicalQuery(); + // No sort or limit in query + Assertions.assertInstanceOf(LogicalFilter.class, query); + } + + @Test + public void testDeleteWithMultipleOrderByColumns() { + String sql = "DELETE FROM t ORDER BY c1 ASC, c2 DESC NULLS LAST LIMIT 10"; + LogicalPlan plan = parser.parseSingle(sql); + Assertions.assertInstanceOf(DeleteFromUsingCommand.class, plan); + LogicalPlan query = ((DeleteFromUsingCommand) plan).getLogicalQuery(); + Assertions.assertInstanceOf(LogicalLimit.class, query); + LogicalSort sort = (LogicalSort) ((LogicalLimit) query).child(); + Assertions.assertEquals(2, sort.getOrderKeys().size()); + } + + @Test + public void testUpdateWithMultipleOrderByColumns() { + String sql = "UPDATE t SET c1 = 10 ORDER BY c2 ASC, c3 DESC NULLS FIRST LIMIT 5"; + LogicalPlan plan = parser.parseSingle(sql); + Assertions.assertInstanceOf(UpdateCommand.class, plan); + LogicalPlan query = ((UpdateCommand) plan).getLogicalQuery(); + Assertions.assertInstanceOf(LogicalLimit.class, query); + LogicalSort sort = (LogicalSort) ((LogicalLimit) query).child(); + Assertions.assertEquals(2, sort.getOrderKeys().size()); + } + + @Test + public void testDeleteOrderByIntegerOrdinalConvertedToUnboundSlot() { + String sql = "DELETE FROM t ORDER BY 1 LIMIT 10"; + LogicalPlan plan = parser.parseSingle(sql); + Assertions.assertInstanceOf(DeleteFromUsingCommand.class, plan); + LogicalPlan query = ((DeleteFromUsingCommand) plan).getLogicalQuery(); + Assertions.assertInstanceOf(LogicalLimit.class, query); + LogicalSort sort = (LogicalSort) ((LogicalLimit) query).child(); + Assertions.assertEquals(1, sort.getOrderKeys().size()); + // Integer ordinal should be converted to UnboundSlot, not remain as IntegerLikeLiteral + Assertions.assertInstanceOf(UnboundSlot.class, sort.getOrderKeys().get(0).getExpr()); + Assertions.assertFalse(sort.getOrderKeys().get(0).getExpr() instanceof IntegerLikeLiteral); + } + + @Test + public void testUpdateOrderByIntegerOrdinalConvertedToUnboundSlot() { + String sql = "UPDATE t SET c1 = 10 ORDER BY 1 LIMIT 100"; + LogicalPlan plan = parser.parseSingle(sql); + Assertions.assertInstanceOf(UpdateCommand.class, plan); + LogicalPlan query = ((UpdateCommand) plan).getLogicalQuery(); + Assertions.assertInstanceOf(LogicalLimit.class, query); + LogicalSort sort = (LogicalSort) ((LogicalLimit) query).child(); + Assertions.assertEquals(1, sort.getOrderKeys().size()); + Assertions.assertInstanceOf(UnboundSlot.class, sort.getOrderKeys().get(0).getExpr()); + Assertions.assertFalse(sort.getOrderKeys().get(0).getExpr() instanceof IntegerLikeLiteral); + } + + @Test + public void testDeleteOrderByMixedOrdinalAndColumn() { + String sql = "DELETE FROM t ORDER BY 1, c2 DESC LIMIT 5"; + LogicalPlan plan = parser.parseSingle(sql); + Assertions.assertInstanceOf(DeleteFromUsingCommand.class, plan); + LogicalPlan query = ((DeleteFromUsingCommand) plan).getLogicalQuery(); + Assertions.assertInstanceOf(LogicalLimit.class, query); + LogicalSort sort = (LogicalSort) ((LogicalLimit) query).child(); + Assertions.assertEquals(2, sort.getOrderKeys().size()); + // First key: integer ordinal converted to UnboundSlot + Assertions.assertInstanceOf(UnboundSlot.class, sort.getOrderKeys().get(0).getExpr()); + // Second key: column name remains as UnboundSlot + Assertions.assertInstanceOf(UnboundSlot.class, sort.getOrderKeys().get(1).getExpr()); + } +} diff --git a/fe/fe-core/src/test/java/org/apache/doris/nereids/parser/NereidsParserTest.java b/fe/fe-core/src/test/java/org/apache/doris/nereids/parser/NereidsParserTest.java index 9c94b9ea0870b5..a82e5b6a227a12 100644 --- a/fe/fe-core/src/test/java/org/apache/doris/nereids/parser/NereidsParserTest.java +++ b/fe/fe-core/src/test/java/org/apache/doris/nereids/parser/NereidsParserTest.java @@ -42,11 +42,13 @@ import org.apache.doris.nereids.trees.plans.commands.CreateMaterializedViewCommand; import org.apache.doris.nereids.trees.plans.commands.CreateTableCommand; import org.apache.doris.nereids.trees.plans.commands.CreateViewCommand; +import org.apache.doris.nereids.trees.plans.commands.DeleteFromUsingCommand; import org.apache.doris.nereids.trees.plans.commands.DropTableCommand; import org.apache.doris.nereids.trees.plans.commands.ExecuteActionCommand; import org.apache.doris.nereids.trees.plans.commands.ExplainCommand; import org.apache.doris.nereids.trees.plans.commands.ExplainCommand.ExplainLevel; import org.apache.doris.nereids.trees.plans.commands.ReplayCommand; +import org.apache.doris.nereids.trees.plans.commands.UpdateCommand; import org.apache.doris.nereids.trees.plans.commands.merge.MergeIntoCommand; import org.apache.doris.nereids.trees.plans.logical.LogicalAggregate; import org.apache.doris.nereids.trees.plans.logical.LogicalCTE; @@ -1530,4 +1532,81 @@ public void testUnnest() { String sql = "SELECT t.* FROM LATERAL unnest([1,2], ['hi','hello']) WITH ORDINALITY AS t(c1,c2);"; parsePlan(sql).matches(logicalGenerate().when(plan -> plan.getGenerators().get(0) instanceof Unnest)); } + + @Test + public void testDeleteWithOrderByAndLimit() { + NereidsParser nereidsParser = new NereidsParser(); + + // DELETE with ORDER BY and LIMIT + String sql = "DELETE FROM t ORDER BY c1 LIMIT 10"; + LogicalPlan plan = nereidsParser.parseSingle(sql); + Assertions.assertInstanceOf(DeleteFromUsingCommand.class, plan); + Assertions.assertEquals(StmtType.DELETE, plan.stmtType()); + + // DELETE with WHERE, ORDER BY DESC NULLS LAST, and LIMIT with offset + sql = "DELETE FROM t WHERE c1 > 0 ORDER BY c1 DESC NULLS LAST LIMIT 5, 10"; + plan = nereidsParser.parseSingle(sql); + Assertions.assertInstanceOf(DeleteFromUsingCommand.class, plan); + + // DELETE with ORDER BY ASC NULLS FIRST and LIMIT with offset + sql = "DELETE FROM t ORDER BY c1 ASC NULLS FIRST LIMIT 10, 3"; + plan = nereidsParser.parseSingle(sql); + Assertions.assertInstanceOf(DeleteFromUsingCommand.class, plan); + + // DELETE with LIMIT only + sql = "DELETE FROM t LIMIT 5"; + plan = nereidsParser.parseSingle(sql); + Assertions.assertInstanceOf(DeleteFromUsingCommand.class, plan); + + // DELETE with ORDER BY only + sql = "DELETE FROM t ORDER BY c1"; + plan = nereidsParser.parseSingle(sql); + Assertions.assertInstanceOf(DeleteFromUsingCommand.class, plan); + + // DELETE with LIMIT OFFSET syntax + sql = "DELETE FROM t ORDER BY c1 LIMIT 10 OFFSET 5"; + plan = nereidsParser.parseSingle(sql); + Assertions.assertInstanceOf(DeleteFromUsingCommand.class, plan); + + // DELETE with multiple ORDER BY columns + sql = "DELETE FROM t ORDER BY c1 ASC, c2 DESC LIMIT 10"; + plan = nereidsParser.parseSingle(sql); + Assertions.assertInstanceOf(DeleteFromUsingCommand.class, plan); + } + + @Test + public void testUpdateWithOrderByAndLimit() { + NereidsParser nereidsParser = new NereidsParser(); + + // UPDATE with ORDER BY and LIMIT + String sql = "UPDATE t SET c1 = 10 ORDER BY c2 LIMIT 100"; + LogicalPlan plan = nereidsParser.parseSingle(sql); + Assertions.assertInstanceOf(UpdateCommand.class, plan); + Assertions.assertEquals(StmtType.UPDATE, plan.stmtType()); + + // UPDATE with WHERE, ORDER BY DESC, and LIMIT with offset + sql = "UPDATE t SET c1 = 10 WHERE c2 > 5 ORDER BY c2 DESC LIMIT 100, 20"; + plan = nereidsParser.parseSingle(sql); + Assertions.assertInstanceOf(UpdateCommand.class, plan); + + // UPDATE with LIMIT only + sql = "UPDATE t SET c1 = 10 LIMIT 50"; + plan = nereidsParser.parseSingle(sql); + Assertions.assertInstanceOf(UpdateCommand.class, plan); + + // UPDATE with ORDER BY only + sql = "UPDATE t SET c1 = 10 ORDER BY c2 ASC NULLS FIRST"; + plan = nereidsParser.parseSingle(sql); + Assertions.assertInstanceOf(UpdateCommand.class, plan); + + // UPDATE with LIMIT OFFSET syntax + sql = "UPDATE t SET c1 = 10 ORDER BY c2 LIMIT 20 OFFSET 10"; + plan = nereidsParser.parseSingle(sql); + Assertions.assertInstanceOf(UpdateCommand.class, plan); + + // UPDATE with multiple ORDER BY columns + sql = "UPDATE t SET c1 = 10 ORDER BY c2 ASC, c3 DESC NULLS LAST LIMIT 5"; + plan = nereidsParser.parseSingle(sql); + Assertions.assertInstanceOf(UpdateCommand.class, plan); + } } diff --git a/regression-test/data/delete_p0/test_delete_order_by_limit.out b/regression-test/data/delete_p0/test_delete_order_by_limit.out new file mode 100644 index 00000000000000..d8474eb877fb99 --- /dev/null +++ b/regression-test/data/delete_p0/test_delete_order_by_limit.out @@ -0,0 +1,61 @@ +-- This file is automatically generated. You should know what you did if you want to edit this +-- !before_delete -- +1 100 a +10 10 j +2 90 b +3 80 c +4 70 d +5 60 e +6 50 f +7 40 g +8 30 h +9 20 i + +-- !delete_order_limit -- +1 100 a +2 90 b +3 80 c +4 70 d +5 60 e +6 50 f +7 40 g + +-- !delete_order_limit_offset -- +1 100 a +10 10 j +2 90 b +3 80 c +4 70 d +5 60 e +9 20 i + +-- !delete_where_order_limit -- +1 100 a +10 10 j +2 90 b +3 80 c +4 70 d +5 60 e +8 30 h +9 20 i + +-- !delete_order_desc_limit -- +10 10 j +3 80 c +4 70 d +5 60 e +6 50 f +7 40 g +8 30 h +9 20 i + +-- !delete_limit_offset_syntax -- +1 100 a +10 10 j +2 90 b +3 80 c +4 70 d +5 60 e +6 50 f +7 40 g + diff --git a/regression-test/data/update/test_update_order_by_limit.out b/regression-test/data/update/test_update_order_by_limit.out new file mode 100644 index 00000000000000..7eb546d8a23ee2 --- /dev/null +++ b/regression-test/data/update/test_update_order_by_limit.out @@ -0,0 +1,85 @@ +-- This file is automatically generated. You should know what you did if you want to edit this +-- !before_update -- +1 100 a +10 10 j +2 90 b +3 80 c +4 70 d +5 60 e +6 50 f +7 40 g +8 30 h +9 20 i + +-- !update_order_limit -- +1 100 a +10 10 updated +2 90 b +3 80 c +4 70 d +5 60 e +6 50 f +7 40 g +8 30 updated +9 20 updated + +-- !update_order_limit_offset -- +1 100 a +10 10 j +2 90 b +3 80 c +4 70 d +5 60 e +6 50 updated +7 40 updated +8 30 updated +9 20 i + +-- !update_where_order_limit -- +1 100 a +10 10 j +2 90 b +3 80 c +4 70 d +5 60 e +6 50 updated +7 40 updated +8 30 h +9 20 i + +-- !update_order_desc_limit -- +1 100 updated +10 10 j +2 90 updated +3 80 c +4 70 d +5 60 e +6 50 f +7 40 g +8 30 h +9 20 i + +-- !update_limit_offset_syntax -- +1 100 a +10 10 j +2 90 b +3 80 c +4 70 d +5 60 e +6 50 f +7 40 g +8 30 updated +9 20 updated + +-- !update_multi_set -- +1 999 top3 +10 10 j +2 999 top3 +3 999 top3 +4 70 d +5 60 e +6 50 f +7 40 g +8 30 h +9 20 i + diff --git a/regression-test/suites/delete_p0/test_delete_order_by_limit.groovy b/regression-test/suites/delete_p0/test_delete_order_by_limit.groovy new file mode 100644 index 00000000000000..b81e9b6cbb8bcf --- /dev/null +++ b/regression-test/suites/delete_p0/test_delete_order_by_limit.groovy @@ -0,0 +1,178 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +suite("test_delete_order_by_limit") { + sql "DROP TABLE IF EXISTS test_delete_obl" + sql """ + CREATE TABLE IF NOT EXISTS test_delete_obl ( + id int, + c1 int, + c2 varchar(32) + ) + UNIQUE KEY(id) + DISTRIBUTED BY HASH(id) BUCKETS 1 + PROPERTIES ( + 'replication_num' = '1' + ); + """ + + // insert test data: 10 rows with id 1..10 + sql """ + INSERT INTO test_delete_obl VALUES + (1, 100, 'a'), (2, 90, 'b'), (3, 80, 'c'), (4, 70, 'd'), (5, 60, 'e'), + (6, 50, 'f'), (7, 40, 'g'), (8, 30, 'h'), (9, 20, 'i'), (10, 10, 'j'); + """ + order_qt_before_delete """SELECT * FROM test_delete_obl ORDER BY id;""" + + // test DELETE with ORDER BY and LIMIT: delete 3 rows with smallest c1 values + // c1 ascending: 10(id=10), 20(id=9), 30(id=8), 40(id=7), 50(id=6), ... + // LIMIT 3 means delete the first 3 rows: id=10, id=9, id=8 + sql "DELETE FROM test_delete_obl ORDER BY c1 ASC LIMIT 3;" + order_qt_delete_order_limit """SELECT * FROM test_delete_obl ORDER BY id;""" + + // reset data + sql "DROP TABLE IF EXISTS test_delete_obl" + sql """ + CREATE TABLE IF NOT EXISTS test_delete_obl ( + id int, + c1 int, + c2 varchar(32) + ) + UNIQUE KEY(id) + DISTRIBUTED BY HASH(id) BUCKETS 1 + PROPERTIES ( + 'replication_num' = '1' + ); + """ + sql """ + INSERT INTO test_delete_obl VALUES + (1, 100, 'a'), (2, 90, 'b'), (3, 80, 'c'), (4, 70, 'd'), (5, 60, 'e'), + (6, 50, 'f'), (7, 40, 'g'), (8, 30, 'h'), (9, 20, 'i'), (10, 10, 'j'); + """ + + // test DELETE with ORDER BY, LIMIT and OFFSET + // c1 ascending: 10(id=10), 20(id=9), 30(id=8), 40(id=7), 50(id=6), ... + // LIMIT 2, 3 means offset=2, limit=3: skip 2 rows (id=10, id=9), delete next 3: id=8, id=7, id=6 + sql "DELETE FROM test_delete_obl ORDER BY c1 ASC LIMIT 2, 3;" + order_qt_delete_order_limit_offset """SELECT * FROM test_delete_obl ORDER BY id;""" + + // reset data + sql "DROP TABLE IF EXISTS test_delete_obl" + sql """ + CREATE TABLE IF NOT EXISTS test_delete_obl ( + id int, + c1 int, + c2 varchar(32) + ) + UNIQUE KEY(id) + DISTRIBUTED BY HASH(id) BUCKETS 1 + PROPERTIES ( + 'replication_num' = '1' + ); + """ + sql """ + INSERT INTO test_delete_obl VALUES + (1, 100, 'a'), (2, 90, 'b'), (3, 80, 'c'), (4, 70, 'd'), (5, 60, 'e'), + (6, 50, 'f'), (7, 40, 'g'), (8, 30, 'h'), (9, 20, 'i'), (10, 10, 'j'); + """ + + // test DELETE with WHERE, ORDER BY and LIMIT + // filter: c1 > 30 leaves: 100(id=1), 90(id=2), 80(id=3), 70(id=4), 60(id=5), 50(id=6), 40(id=7) + // order by c1 ASC: 40(id=7), 50(id=6), 60(id=5), 70(id=4), 80(id=3), 90(id=2), 100(id=1) + // LIMIT 2: delete id=7, id=6 + sql "DELETE FROM test_delete_obl WHERE c1 > 30 ORDER BY c1 ASC LIMIT 2;" + order_qt_delete_where_order_limit """SELECT * FROM test_delete_obl ORDER BY id;""" + + // reset data + sql "DROP TABLE IF EXISTS test_delete_obl" + sql """ + CREATE TABLE IF NOT EXISTS test_delete_obl ( + id int, + c1 int, + c2 varchar(32) + ) + UNIQUE KEY(id) + DISTRIBUTED BY HASH(id) BUCKETS 1 + PROPERTIES ( + 'replication_num' = '1' + ); + """ + sql """ + INSERT INTO test_delete_obl VALUES + (1, 100, 'a'), (2, 90, 'b'), (3, 80, 'c'), (4, 70, 'd'), (5, 60, 'e'), + (6, 50, 'f'), (7, 40, 'g'), (8, 30, 'h'), (9, 20, 'i'), (10, 10, 'j'); + """ + + // test DELETE with LIMIT only (no ORDER BY) + // delete any 3 rows (order is non-deterministic without ORDER BY) + sql "DELETE FROM test_delete_obl LIMIT 3;" + // just check the count + def result = sql "SELECT count(*) FROM test_delete_obl;" + assertEquals(7, result[0][0] as int) + + // reset data + sql "DROP TABLE IF EXISTS test_delete_obl" + sql """ + CREATE TABLE IF NOT EXISTS test_delete_obl ( + id int, + c1 int, + c2 varchar(32) + ) + UNIQUE KEY(id) + DISTRIBUTED BY HASH(id) BUCKETS 1 + PROPERTIES ( + 'replication_num' = '1' + ); + """ + sql """ + INSERT INTO test_delete_obl VALUES + (1, 100, 'a'), (2, 90, 'b'), (3, 80, 'c'), (4, 70, 'd'), (5, 60, 'e'), + (6, 50, 'f'), (7, 40, 'g'), (8, 30, 'h'), (9, 20, 'i'), (10, 10, 'j'); + """ + + // test DELETE with ORDER BY DESC and LIMIT + // c1 descending: 100(id=1), 90(id=2), 80(id=3), ... + // LIMIT 2: delete id=1, id=2 + sql "DELETE FROM test_delete_obl ORDER BY c1 DESC LIMIT 2;" + order_qt_delete_order_desc_limit """SELECT * FROM test_delete_obl ORDER BY id;""" + + // test DELETE with LIMIT OFFSET syntax + sql "DROP TABLE IF EXISTS test_delete_obl" + sql """ + CREATE TABLE IF NOT EXISTS test_delete_obl ( + id int, + c1 int, + c2 varchar(32) + ) + UNIQUE KEY(id) + DISTRIBUTED BY HASH(id) BUCKETS 1 + PROPERTIES ( + 'replication_num' = '1' + ); + """ + sql """ + INSERT INTO test_delete_obl VALUES + (1, 100, 'a'), (2, 90, 'b'), (3, 80, 'c'), (4, 70, 'd'), (5, 60, 'e'), + (6, 50, 'f'), (7, 40, 'g'), (8, 30, 'h'), (9, 20, 'i'), (10, 10, 'j'); + """ + + // LIMIT 2 OFFSET 1 means skip 1 row then delete next 2 rows + // c1 ascending: 10(id=10), 20(id=9), 30(id=8), ... + // skip 1 (id=10), delete 2 (id=9, id=8) + sql "DELETE FROM test_delete_obl ORDER BY c1 ASC LIMIT 2 OFFSET 1;" + order_qt_delete_limit_offset_syntax """SELECT * FROM test_delete_obl ORDER BY id;""" +} diff --git a/regression-test/suites/update/test_update_order_by_limit.groovy b/regression-test/suites/update/test_update_order_by_limit.groovy new file mode 100644 index 00000000000000..442b1857659494 --- /dev/null +++ b/regression-test/suites/update/test_update_order_by_limit.groovy @@ -0,0 +1,175 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +suite("test_update_order_by_limit") { + sql "DROP TABLE IF EXISTS test_update_obl" + sql """ + CREATE TABLE IF NOT EXISTS test_update_obl ( + id int, + c1 int, + c2 varchar(32) + ) + UNIQUE KEY(id) + DISTRIBUTED BY HASH(id) BUCKETS 1 + PROPERTIES ( + 'replication_num' = '1' + ); + """ + + // insert test data: 10 rows with id 1..10 + sql """ + INSERT INTO test_update_obl VALUES + (1, 100, 'a'), (2, 90, 'b'), (3, 80, 'c'), (4, 70, 'd'), (5, 60, 'e'), + (6, 50, 'f'), (7, 40, 'g'), (8, 30, 'h'), (9, 20, 'i'), (10, 10, 'j'); + """ + order_qt_before_update """SELECT * FROM test_update_obl ORDER BY id;""" + + // test UPDATE with ORDER BY and LIMIT + // c1 ascending: 10(id=10), 20(id=9), 30(id=8) + // LIMIT 3 means update the first 3 rows: set c2='updated' for id=10, id=9, id=8 + sql "UPDATE test_update_obl SET c2 = 'updated' ORDER BY c1 ASC LIMIT 3;" + order_qt_update_order_limit """SELECT * FROM test_update_obl ORDER BY id;""" + + // reset data + sql "DROP TABLE IF EXISTS test_update_obl" + sql """ + CREATE TABLE IF NOT EXISTS test_update_obl ( + id int, + c1 int, + c2 varchar(32) + ) + UNIQUE KEY(id) + DISTRIBUTED BY HASH(id) BUCKETS 1 + PROPERTIES ( + 'replication_num' = '1' + ); + """ + sql """ + INSERT INTO test_update_obl VALUES + (1, 100, 'a'), (2, 90, 'b'), (3, 80, 'c'), (4, 70, 'd'), (5, 60, 'e'), + (6, 50, 'f'), (7, 40, 'g'), (8, 30, 'h'), (9, 20, 'i'), (10, 10, 'j'); + """ + + // test UPDATE with ORDER BY, LIMIT and OFFSET + // c1 ascending: 10(id=10), 20(id=9), 30(id=8), 40(id=7), 50(id=6) + // LIMIT 2, 3 means offset=2, limit=3: skip 2 rows (id=10, id=9), update next 3: id=8, id=7, id=6 + sql "UPDATE test_update_obl SET c2 = 'updated' ORDER BY c1 ASC LIMIT 2, 3;" + order_qt_update_order_limit_offset """SELECT * FROM test_update_obl ORDER BY id;""" + + // reset data + sql "DROP TABLE IF EXISTS test_update_obl" + sql """ + CREATE TABLE IF NOT EXISTS test_update_obl ( + id int, + c1 int, + c2 varchar(32) + ) + UNIQUE KEY(id) + DISTRIBUTED BY HASH(id) BUCKETS 1 + PROPERTIES ( + 'replication_num' = '1' + ); + """ + sql """ + INSERT INTO test_update_obl VALUES + (1, 100, 'a'), (2, 90, 'b'), (3, 80, 'c'), (4, 70, 'd'), (5, 60, 'e'), + (6, 50, 'f'), (7, 40, 'g'), (8, 30, 'h'), (9, 20, 'i'), (10, 10, 'j'); + """ + + // test UPDATE with WHERE, ORDER BY and LIMIT + // filter: c1 > 30 leaves: 100(id=1), 90(id=2), 80(id=3), 70(id=4), 60(id=5), 50(id=6), 40(id=7) + // order by c1 ASC: 40(id=7), 50(id=6), 60(id=5), 70(id=4), 80(id=3), 90(id=2), 100(id=1) + // LIMIT 2: update id=7, id=6 + sql "UPDATE test_update_obl SET c2 = 'updated' WHERE c1 > 30 ORDER BY c1 ASC LIMIT 2;" + order_qt_update_where_order_limit """SELECT * FROM test_update_obl ORDER BY id;""" + + // reset data + sql "DROP TABLE IF EXISTS test_update_obl" + sql """ + CREATE TABLE IF NOT EXISTS test_update_obl ( + id int, + c1 int, + c2 varchar(32) + ) + UNIQUE KEY(id) + DISTRIBUTED BY HASH(id) BUCKETS 1 + PROPERTIES ( + 'replication_num' = '1' + ); + """ + sql """ + INSERT INTO test_update_obl VALUES + (1, 100, 'a'), (2, 90, 'b'), (3, 80, 'c'), (4, 70, 'd'), (5, 60, 'e'), + (6, 50, 'f'), (7, 40, 'g'), (8, 30, 'h'), (9, 20, 'i'), (10, 10, 'j'); + """ + + // test UPDATE with ORDER BY DESC and LIMIT + // c1 descending: 100(id=1), 90(id=2), 80(id=3) + // LIMIT 2: update id=1, id=2 + sql "UPDATE test_update_obl SET c2 = 'updated' ORDER BY c1 DESC LIMIT 2;" + order_qt_update_order_desc_limit """SELECT * FROM test_update_obl ORDER BY id;""" + + // test UPDATE with LIMIT OFFSET syntax + sql "DROP TABLE IF EXISTS test_update_obl" + sql """ + CREATE TABLE IF NOT EXISTS test_update_obl ( + id int, + c1 int, + c2 varchar(32) + ) + UNIQUE KEY(id) + DISTRIBUTED BY HASH(id) BUCKETS 1 + PROPERTIES ( + 'replication_num' = '1' + ); + """ + sql """ + INSERT INTO test_update_obl VALUES + (1, 100, 'a'), (2, 90, 'b'), (3, 80, 'c'), (4, 70, 'd'), (5, 60, 'e'), + (6, 50, 'f'), (7, 40, 'g'), (8, 30, 'h'), (9, 20, 'i'), (10, 10, 'j'); + """ + + // LIMIT 2 OFFSET 1 means skip 1 then update next 2 + // c1 ascending: 10(id=10), 20(id=9), 30(id=8) + // skip 1 (id=10), update 2 (id=9, id=8) + sql "UPDATE test_update_obl SET c2 = 'updated' ORDER BY c1 ASC LIMIT 2 OFFSET 1;" + order_qt_update_limit_offset_syntax """SELECT * FROM test_update_obl ORDER BY id;""" + + // test UPDATE with multiple SET assignments and ORDER BY LIMIT + sql "DROP TABLE IF EXISTS test_update_obl" + sql """ + CREATE TABLE IF NOT EXISTS test_update_obl ( + id int, + c1 int, + c2 varchar(32) + ) + UNIQUE KEY(id) + DISTRIBUTED BY HASH(id) BUCKETS 1 + PROPERTIES ( + 'replication_num' = '1' + ); + """ + sql """ + INSERT INTO test_update_obl VALUES + (1, 100, 'a'), (2, 90, 'b'), (3, 80, 'c'), (4, 70, 'd'), (5, 60, 'e'), + (6, 50, 'f'), (7, 40, 'g'), (8, 30, 'h'), (9, 20, 'i'), (10, 10, 'j'); + """ + + // update both c1 and c2 for the 3 rows with largest c1 + sql "UPDATE test_update_obl SET c1 = 999, c2 = 'top3' ORDER BY c1 DESC LIMIT 3;" + order_qt_update_multi_set """SELECT * FROM test_update_obl ORDER BY id;""" +}