@VisibleForTesting
    public boolean syncTableMetaInternal(Database db, OlapTable table, boolean forceDeleteData) throws DdlException {
        StarOSAgent starOSAgent = GlobalStateMgr.getCurrentState().getStarOSAgent();
        HashMap<Long, Set<Long>> redundantGroupToShards = new HashMap<>();
        List<PhysicalPartition> physicalPartitions = new ArrayList<>();
        Locker locker = new Locker();
        locker.lockDatabase(db, LockType.READ);
        try {
            if (db.getTable(table.getId()) == null) {
                return false; // table might be dropped
            }
            GlobalStateMgr.getCurrentState().getLocalMetastore()
                    .getAllPartitionsIncludeRecycleBin(table)
                    .stream()
                    .map(Partition::getSubPartitions)
                    .forEach(physicalPartitions::addAll);
            table.setShardGroupChanged(false);
        } finally {
            locker.unLockDatabase(db, LockType.READ);
        }

        for (PhysicalPartition physicalPartition : physicalPartitions) {
            locker.lockDatabase(db, LockType.READ);
            try {
                // schema change might replace the shards in the original shard group
                if (table.getState() != OlapTable.OlapTableState.NORMAL) {
                    return false;
                }
                // automatic bucketing will create new shards in the original shard group
                if (table.isAutomaticBucketing()) {
                    return false;
                }
                // automatic bucketing will change physicalPartitions make shard group changed even after it's done
                if (table.hasShardGroupChanged()) {
                    return false;
                }
                // no need to check db/table/partition again, everything still works
                long groupId = physicalPartition.getShardGroupId();
                Set<Long> starmgrShardIdsSet = null;
                if (redundantGroupToShards.get(groupId) != null) {
                    starmgrShardIdsSet = redundantGroupToShards.get(groupId);
                } else {
                    List<Long> starmgrShardIds = starOSAgent.listShard(groupId);
                    starmgrShardIdsSet = new HashSet<>(starmgrShardIds);
                }
                for (MaterializedIndex materializedIndex :
                        physicalPartition.getMaterializedIndices(MaterializedIndex.IndexExtState.ALL)) {
                    for (Tablet tablet : materializedIndex.getTablets()) {
                        starmgrShardIdsSet.remove(tablet.getId());
                    }
                }
                // collect shard in starmgr but not in fe
                redundantGroupToShards.put(groupId, starmgrShardIdsSet);
            } finally {
                locker.unLockDatabase(db, LockType.READ);
            }
        }

        // try to delete data, if fail, still delete redundant shard meta in starmgr
        Set<Long> shardToDelete = new HashSet<>();
        for (Map.Entry<Long, Set<Long>> entry : redundantGroupToShards.entrySet()) {
            if (forceDeleteData) {
                try {
                    List<Long> shardIds = new ArrayList<>();
                    shardIds.addAll(entry.getValue());
                    dropTabletAndDeleteShard(shardIds, starOSAgent);
                } catch (Exception e) {
                    // ignore exception
                    LOG.info(e.getMessage());
                }
            }
            shardToDelete.addAll(entry.getValue());
        }

        // do final meta delete, regardless whether above tablet deleted or not
        if (!shardToDelete.isEmpty()) {
            starOSAgent.deleteShards(shardToDelete);
        }
        return !shardToDelete.isEmpty();
    }
@Test
    public void testSyncTableMetaInternal() throws Exception {
        long dbId = 100;
        long tableId = 1000;
        List<Long> shards = new ArrayList<>();
        Database db = new Database(dbId, "db");

        new MockUp<GlobalStateMgr>() {
            @Mock
            public Database getDb(String dbName) {
                return db;
            }

            @Mock
            public Database getDb(long id) {
                return db;
            }

            @Mock
            public List<Long> getDbIds() {
                return Lists.newArrayList(dbId);
            }
        };

        List<Column> baseSchema = new ArrayList<>();
        KeysType keysType = KeysType.AGG_KEYS;
        PartitionInfo partitionInfo = new PartitionInfo(PartitionType.RANGE);
        DistributionInfo defaultDistributionInfo = new HashDistributionInfo();
        Table table = new LakeTable(tableId, "bbb", baseSchema, keysType, partitionInfo, defaultDistributionInfo);

        new MockUp<Database>() {
            @Mock
            public Table getTable(String tableName) {
                return table;
            }

            @Mock
            public Table getTable(long tableId) {
                return table;
            }

            @Mock
            public List<Table> getTables() {
                return Lists.newArrayList(table);
            }
        };

        new MockUp<MaterializedIndex>() {
            @Mock
            public List<Tablet> getTablets() {
                List<Tablet> tablets = new ArrayList<>();
                tablets.add(new LakeTablet(111));
                tablets.add(new LakeTablet(222));
                tablets.add(new LakeTablet(333));
                return tablets;
            }
        };

        new MockUp<PhysicalPartition>() {
            @Mock
            public long getShardGroupId() {
                return 444;
            }
        };

        new MockUp<StarOSAgent>() {
            @Mock
            public List<Long> listShard(long groupId) throws DdlException {
                return shards;
            }

            @Mock
            public void deleteShards(Set<Long> shardIds) throws DdlException {
                shards.removeAll(shardIds);
            }
        };

        new MockUp<ColocateTableIndex>() {
            @Mock
            public boolean isLakeColocateTable(long tableId) {
                return true;
            }

            @Mock
            public void updateLakeTableColocationInfo(OlapTable olapTable, boolean isJoin,
                                                      GroupId expectGroupId) throws DdlException {
                return;
            }
        };

        new MockUp<SystemInfoService>() {
            @Mock
            public ComputeNode getBackendOrComputeNode(long nodeId) {
                return null;
            }
        };

        shards.clear();
        shards.add(111L);
        shards.add(222L);
        shards.add(333L);
        starMgrMetaSyncer.syncTableMetaInternal(db, (OlapTable) table, true);
        Assert.assertEquals(3, shards.size());
    }