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 32cdb65a71ece2..eb11ce49cd1bfd 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 @@ -151,11 +151,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 a5acc3623657b8..13b4eac43082a0 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 @@ -600,6 +600,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; @@ -2037,6 +2038,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(); @@ -2067,8 +2070,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); @@ -2078,12 +2084,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()); @@ -4379,6 +4387,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 04e155b29fc42b..be06737831004f 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 @@ -181,7 +181,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); @@ -193,7 +193,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 48320525a9d7fa..977177161fd151 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; @@ -1524,4 +1526,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;""" +}