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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6678,10 +6678,9 @@ public LogicalPlan visitShowTypeCast(DorisParser.ShowTypeCastContext ctx) {
@Override
public LogicalPlan visitShowTrash(ShowTrashContext ctx) {
if (ctx.ON() != null) {
String backend = stripQuotes(ctx.STRING_LITERAL().getText());
new ShowTrashCommand(backend);
} else {
return new ShowTrashCommand();
List<String> backendsQuery = Lists.newArrayList();
ctx.backends.forEach(backend -> backendsQuery.add(stripQuotes(backend.getText())));
return new ShowTrashCommand(backendsQuery);
}
return new ShowTrashCommand();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,30 +36,32 @@
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
* show trash command
*/
public class ShowTrashCommand extends ShowCommand {
private List<Backend> backends = Lists.newArrayList();
private String backendQuery;
private List<String> backendsQuery;

public ShowTrashCommand() {
super(PlanType.SHOW_TRASH_COMMAND);
}

public ShowTrashCommand(String backendQuery) {
public ShowTrashCommand(List<String> backendsQuery) {
super(PlanType.SHOW_TRASH_COMMAND);
this.backendQuery = backendQuery;
this.backendsQuery = backendsQuery;
}

public List<Backend> getBackends() {
return backends;
}

public String getBackend() {
return backendQuery;
public List<String> getBackendsQuery() {
return backendsQuery;
}

public ShowResultSetMetaData getMetaData() {
Expand All @@ -70,24 +72,28 @@ public ShowResultSetMetaData getMetaData() {
return builder.build();
}

private ShowResultSet handleShowTrash(String backendQuery) throws Exception {
private ShowResultSet handleShowTrash(List<String> backendsQuery) throws Exception {
if (!Env.getCurrentEnv().getAccessManager().checkGlobalPriv(ConnectContext.get(), PrivPredicate.ADMIN)
&& !Env.getCurrentEnv().getAccessManager().checkGlobalPriv(ConnectContext.get(),
PrivPredicate.OPERATOR)) {
ErrorReport.reportAnalysisException(ErrorCode.ERR_SPECIFIC_ACCESS_DENIED_ERROR, "ADMIN/OPERATOR");
}
ImmutableMap<Long, Backend> backendsInfo = Env.getCurrentSystemInfo().getAllBackendsByAllCluster();
if (backendQuery == null || backendQuery.isEmpty()) {
if (backendsQuery == null) {
for (Backend backend : backendsInfo.values()) {
this.backends.add(backend);
}
} else {
Map<String, Long> backendsID = new HashMap<>();
for (Backend backend : backendsInfo.values()) {
String backendStr = NetUtils.getHostPortInAccessibleFormat(backend.getHost(),
backend.getHeartbeatPort());
if (backendQuery.equals(backendStr)) {
this.backends.add(backend);
break;
backendsID.put(
NetUtils.getHostPortInAccessibleFormat(backend.getHost(), backend.getHeartbeatPort()),
backend.getId());
}
for (String backendQuery : backendsQuery) {
if (backendsID.containsKey(backendQuery)) {
this.backends.add(backendsInfo.get(backendsID.get(backendQuery)));
backendsID.remove(backendQuery);
}
}
}
Expand All @@ -103,7 +109,7 @@ public <R, C> R accept(PlanVisitor<R, C> visitor, C context) {

@Override
public ShowResultSet doRun(ConnectContext ctx, StmtExecutor executor) throws Exception {
return handleShowTrash(backendQuery);
return handleShowTrash(backendsQuery);
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
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.ShowTrashCommand;
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;
Expand Down Expand Up @@ -1769,4 +1770,41 @@ public void testIsTrueAndIsFalseExpression() {
Assertions.assertInstanceOf(Not.class, expression);
Assertions.assertInstanceOf(IsFalse.class, expression.child(0));
}

@Test
public void testShowTrashCommand() {
NereidsParser nereidsParser = new NereidsParser();

// Case 1: SHOW TRASH without ON clause (backendsQuery should be null)
String sql1 = "SHOW TRASH";
Plan plan1 = nereidsParser.parseSingle(sql1);
Assertions.assertInstanceOf(ShowTrashCommand.class, plan1);
ShowTrashCommand cmd1 = (ShowTrashCommand) plan1;
Assertions.assertNull(cmd1.getBackendsQuery());

// Case 2: SHOW TRASH ON with single backend (new parenthesized syntax)
String sql2 = "SHOW TRASH ON (\"be1:9050\")";
Plan plan2 = nereidsParser.parseSingle(sql2);
Assertions.assertInstanceOf(ShowTrashCommand.class, plan2);
ShowTrashCommand cmd2 = (ShowTrashCommand) plan2;

Assertions.assertNotNull(cmd2.getBackendsQuery());
Assertions.assertEquals(1, cmd2.getBackendsQuery().size());
Assertions.assertEquals("be1:9050", cmd2.getBackendsQuery().get(0));

// Case 3: SHOW TRASH ON with multiple backends
String sql3 = "SHOW TRASH ON (\"be1:9050\", \"be2:9050\", \"be3:9050\")";
Plan plan3 = nereidsParser.parseSingle(sql3);
Assertions.assertInstanceOf(ShowTrashCommand.class, plan3);
ShowTrashCommand cmd3 = (ShowTrashCommand) plan3;

Assertions.assertNotNull(cmd3.getBackendsQuery());
Assertions.assertEquals(3, cmd3.getBackendsQuery().size());
Assertions.assertEquals(Lists.newArrayList("be1:9050", "be2:9050", "be3:9050"), cmd3.getBackendsQuery());

// Case 4: Verify old syntax (without parentheses) is rejected
String sql4 = "SHOW TRASH ON \"be1:9050\"";
Assertions.assertThrows(ParseException.class, () -> nereidsParser.parseSingle(sql4));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// 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.trees.plans.commands;

import org.apache.doris.catalog.Env;
import org.apache.doris.common.proc.TrashProcDir;
import org.apache.doris.mysql.privilege.AccessControllerManager;
import org.apache.doris.qe.ConnectContext;
import org.apache.doris.qe.StmtExecutor;
import org.apache.doris.system.Backend;
import org.apache.doris.system.SystemInfoService;

import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.MockedStatic;
import org.mockito.Mockito;

import java.util.List;

public class ShowTrashCommandTest {

private MockedStatic<Env> mockedEnvStatic;
private MockedStatic<ConnectContext> mockedConnectContextStatic;
private MockedStatic<TrashProcDir> mockedTrashProcDirStatic;

private SystemInfoService systemInfoService;
private ConnectContext ctx;
private StmtExecutor executor;
private AccessControllerManager accessManager;

@BeforeEach
public void setUp() {
systemInfoService = Mockito.mock(SystemInfoService.class);
ctx = Mockito.mock(ConnectContext.class);
executor = Mockito.mock(StmtExecutor.class);
accessManager = Mockito.mock(AccessControllerManager.class);
mockedConnectContextStatic = Mockito.mockStatic(ConnectContext.class);
mockedConnectContextStatic.when(ConnectContext::get).thenReturn(ctx);

Env fakeEnv = Mockito.mock(Env.class);
Mockito.when(fakeEnv.getAccessManager()).thenReturn(accessManager);

Mockito.when(accessManager.checkGlobalPriv(Mockito.eq(ctx), Mockito.any())).thenReturn(true);

mockedEnvStatic = Mockito.mockStatic(Env.class, Mockito.CALLS_REAL_METHODS);
mockedEnvStatic.when(Env::getCurrentEnv).thenReturn(fakeEnv);
mockedEnvStatic.when(Env::getCurrentSystemInfo).thenReturn(systemInfoService);

mockedTrashProcDirStatic = Mockito.mockStatic(TrashProcDir.class);
mockedTrashProcDirStatic.when(() -> TrashProcDir.getTrashInfo(
Mockito.anyList(),
Mockito.anyList()
)).thenAnswer(invocation -> null);
}

@AfterEach
public void tearDown() {
if (mockedEnvStatic != null) {
mockedEnvStatic.close();
}
if (mockedConnectContextStatic != null) {
mockedConnectContextStatic.close();
}
if (mockedTrashProcDirStatic != null) {
mockedTrashProcDirStatic.close();
}
}

@Test
public void testHandleShowTrashWithFilter() throws Exception {
Backend be1 = new Backend(1001L, "192.168.1.1", 9050);
Backend be2 = new Backend(1002L, "192.168.1.2", 9050);
Backend be3 = new Backend(1003L, "192.168.1.3", 9050);
ImmutableMap<Long, Backend> allBackendsMap = ImmutableMap.of(
1001L, be1,
1002L, be2,
1003L, be3
);
Mockito.when(systemInfoService.getAllBackendsByAllCluster()).thenReturn(allBackendsMap);
ShowTrashCommand command = new ShowTrashCommand(
Lists.newArrayList("192.168.1.1:9050", "192.168.1.3:9050")
);
command.doRun(ctx, executor);
List<Backend> resultBackends = command.getBackends();
Assertions.assertNotNull(resultBackends);
Assertions.assertEquals(2, resultBackends.size());
List<Long> resultIds = Lists.newArrayList(resultBackends.get(0).getId(), resultBackends.get(1).getId());
Assertions.assertTrue(resultIds.contains(1001L));
Assertions.assertTrue(resultIds.contains(1003L));
Assertions.assertFalse(resultIds.contains(1002L));
}

@Test
public void testHandleShowTrashWithNonExistentBackend() throws Exception {
Backend be1 = new Backend(1001L, "192.168.1.1", 9050);
ImmutableMap<Long, Backend> allBackendsMap = ImmutableMap.of(1001L, be1);
Mockito.when(systemInfoService.getAllBackendsByAllCluster()).thenReturn(allBackendsMap);
ShowTrashCommand command = new ShowTrashCommand(
Lists.newArrayList("192.168.1.1:9050", "192.168.99.99:9999")
);
command.doRun(ctx, executor);
List<Backend> resultBackends = command.getBackends();
Assertions.assertNotNull(resultBackends);
Assertions.assertEquals(1, resultBackends.size());
Assertions.assertEquals(1001L, resultBackends.get(0).getId());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -464,7 +464,8 @@ supportedShowStatement
| SHOW FULL? (COLUMNS | FIELDS) (FROM | IN) tableName=multipartIdentifier
((FROM | IN) database=multipartIdentifier)? wildWhere? #showColumns
| SHOW TABLE tableId=INTEGER_VALUE #showTableId
| SHOW TRASH (ON backend=STRING_LITERAL)? #showTrash
| SHOW TRASH (ON LEFT_PAREN backends+=STRING_LITERAL
(COMMA backends+=STRING_LITERAL)* RIGHT_PAREN)? #showTrash
| SHOW TYPECAST ((FROM | IN) database=identifier)? #showTypeCast
| SHOW (CLUSTERS | (COMPUTE GROUPS)) #showClusters
| SHOW statementScope? STATUS #showStatus
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,79 @@
// under the License.

suite("show_trash_nereids") {
// can not use qt command since the output change based on cluster and backend ip
checkNereidsExecute("""show trash;""")
checkNereidsExecute("""show trash on "127.0.0.1:9050";""")
// Retrieve backend info dynamically to adapt to any environment (1 node or N nodes)
def allBackends = sql """SHOW BACKENDS"""
assertTrue(allBackends.size() > 0, "Test requires at least 1 backend in the cluster")

//cloud-mode
// Extract HostPorts dynamically.
// Based on actual SHOW BACKENDS output:
// Index 1: Host (e.g., 172.17.0.1)
// Index 2: HeartbeatPort (e.g., 9050) <--- This is the port used by SHOW TRASH!
String firstBackendHost = allBackends[0][1].toString()
String firstBackendPort = allBackends[0][2].toString()
String firstBackendHostPort = "${firstBackendHost}:${firstBackendPort}"

String nonExistentBackend = "1.2.3.4:9999" // A non-existent backend address

// ========================================
// Test 1: Verify backward compatibility (No filter)
// ========================================
def allTrashResults = sql """SHOW TRASH"""
assertTrue(allTrashResults.size() == allBackends.size(),
"Without filter, should return trash info for all backends. Expected: ${allBackends.size()}, Got: ${allTrashResults.size()}")

// ========================================
// Test 2: Verify single backend filter
// ========================================
def singleResult = sql """SHOW TRASH ON ("${firstBackendHostPort}")"""
assertTrue(singleResult.size() == 1,
"With single backend filter, should return exactly 1 row. Expected: 1, Got: ${singleResult.size()}")

// Verify the returned backend matches the requested one
// FIX: Column 1 is the 'Backend' string (IP:Port), not Column 0 (BackendId)
assertTrue(singleResult[0][1].toString() == firstBackendHostPort,
"The returned backend should match the requested one. Expected: ${firstBackendHostPort}, Got: ${singleResult[0][1]}")

// ========================================
// Test 3: Verify filter with non-existent backend
// ========================================
def mixedResults = sql """SHOW TRASH ON ("${firstBackendHostPort}", "${nonExistentBackend}")"""
assertTrue(mixedResults.size() == 1,
"With one existent and one non-existent backend, should return only 1 row. Expected: 1, Got: ${mixedResults.size()}")

// ========================================
// Test 4: Verify result with all non-existent backends
// ========================================
def emptyResults = sql """SHOW TRASH ON ("${nonExistentBackend}", "5.6.7.8:9999")"""
assertTrue(emptyResults.size() == 0,
"With all non-existent backends, should return empty result. Expected: 0, Got: ${emptyResults.size()}")

// ========================================
// Test 5: Verify filter for multiple backends (Only if >= 2 backends exist)
// ========================================
if (allBackends.size() >= 2) {
// FIX: Use the same indices as Test 2 for consistency
String secondBackendHost = allBackends[1][1].toString()
String secondBackendPort = allBackends[1][2].toString()
String secondBackendHostPort = "${secondBackendHost}:${secondBackendPort}"

def multiResults = sql """SHOW TRASH ON ("${firstBackendHostPort}", "${secondBackendHostPort}")"""
assertTrue(multiResults.size() == 2,
"With filter for 2 backends, should return exactly 2 rows. Expected: 2, Got: ${multiResults.size()}")

// Verify content
// FIX: Column 1 is the 'Backend' string, not Column 0
def r1 = multiResults[0][1].toString()
def r2 = multiResults[1][1].toString()
assertTrue(
(r1 == firstBackendHostPort && r2 == secondBackendHostPort) ||
(r1 == secondBackendHostPort && r2 == firstBackendHostPort),
"Returned backends should match the requested ones. Expected: [${firstBackendHostPort}, ${secondBackendHostPort}] in any order, Got: [${r1}, ${r2}]"
)
}

// cloud-mode checks
if (isCloudMode()) {
return
// Add cloud mode specific logic here
}
checkNereidsExecute("""ADMIN CLEAN TRASH;""")
checkNereidsExecute("""ADMIN CLEAN TRASH ON ("127.0.0.1:9050");""")
checkNereidsExecute("""ADMIN CLEAN TRASH ON ("192.168.0.1:9050", "192.168.0.2:9050", "192.168.0.3:9050");""")
}
Loading