This is an automated email from the ASF dual-hosted git repository.

shuke987 pushed a commit to branch codex/export-cases-branch41
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/codex/export-cases-branch41 by 
this push:
     new c086d459dc5 test(export): guard preflight before delete
c086d459dc5 is described below

commit c086d459dc506eaebb3c84afbc3bb9f57b11ef7c
Author: shuke <[email protected]>
AuthorDate: Wed Jul 22 18:53:44 2026 +0800

    test(export): guard preflight before delete
---
 .../test_export_preflight_before_delete.groovy     | 172 +++++++++++++++++++++
 1 file changed, 172 insertions(+)

diff --git 
a/regression-test/suites/export_p0/test_export_preflight_before_delete.groovy 
b/regression-test/suites/export_p0/test_export_preflight_before_delete.groovy
new file mode 100644
index 00000000000..cfcd1623ab1
--- /dev/null
+++ 
b/regression-test/suites/export_p0/test_export_preflight_before_delete.groovy
@@ -0,0 +1,172 @@
+// 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_export_preflight_before_delete", "p0") {
+    if (!getFeConfig("enable_delete_existing_files").equalsIgnoreCase("true")) 
{
+        logger.warn("Enable enable_delete_existing_files to run 
test_export_preflight_before_delete")
+        return
+    }
+
+    String bucket = getS3BucketName()
+    assertNotNull(bucket)
+    assertFalse(bucket.isEmpty())
+
+    String runId = UUID.randomUUID().toString()
+    String rootPrefix = "find-bugs/export/tc-f-006/${runId}/"
+    String targetPrefix = "${rootPrefix}target/"
+    String adjacentPrefix = "${rootPrefix}target-adjacent/"
+    String ownershipProbeKey = "${rootPrefix}OWNERSHIP_PROBE"
+    String sentinelKey = "${targetPrefix}sentinel.keep"
+    String oldResultKey = "${targetPrefix}old-success.csv"
+    String adjacentKey = "${adjacentPrefix}neighbor.keep"
+
+    def snapshotOwnedPrefix = {
+        return getS3Client().listObjects(bucket, 
rootPrefix).getObjectSummaries()
+                .sort { left, right -> left.getKey() <=> right.getKey() }
+                .collectEntries { summary ->
+                    [(summary.getKey()): 
"${summary.getETag()}:${summary.getSize()}"]
+                }
+    }
+
+    def cleanupOwnedPrefix = {
+        getS3Client().listObjects(bucket, 
rootPrefix).getObjectSummaries().each { summary ->
+            assertTrue(summary.getKey().startsWith(rootPrefix))
+            getS3Client().deleteObject(bucket, summary.getKey())
+        }
+        assertTrue(snapshotOwnedPrefix().isEmpty())
+    }
+
+    // Safety gate: a UUID root must be empty before first use, and exact-key 
cleanup
+    // must leave it empty. No destructive EXPORT is allowed before this gate 
passes.
+    assertTrue(snapshotOwnedPrefix().isEmpty())
+    getS3Client().putObject(bucket, ownershipProbeKey, 
"owned-by-tc-f-006-${runId}")
+    def probeSnapshot = snapshotOwnedPrefix()
+    assertEquals([ownershipProbeKey], probeSnapshot.keySet().toList())
+    getS3Client().deleteObject(bucket, ownershipProbeKey)
+    assertTrue(snapshotOwnedPrefix().isEmpty())
+
+    sql """DROP TABLE IF EXISTS test_export_preflight_before_delete"""
+    sql """
+        CREATE TABLE test_export_preflight_before_delete (
+            id INT,
+            epoch INT
+        )
+        DISTRIBUTED BY HASH(id) BUCKETS 3
+        PROPERTIES("replication_num" = "1")
+    """
+    sql """INSERT INTO test_export_preflight_before_delete VALUES (1, 100), 
(2, 100), (3, 100)"""
+
+    def restoreFixture = {
+        cleanupOwnedPrefix()
+        getS3Client().putObject(bucket, sentinelKey, "sentinel-${runId}")
+        getS3Client().putObject(bucket, oldResultKey, "old-result-${runId}")
+        getS3Client().putObject(bucket, adjacentKey, "adjacent-${runId}")
+        def fixture = snapshotOwnedPrefix()
+        assertEquals([adjacentKey, oldResultKey, sentinelKey].sort(), 
fixture.keySet().toList().sort())
+        return fixture
+    }
+
+    def waitForFinalJob = { label ->
+        def finalJob = null
+        for (int i = 0; i < 60; i++) {
+            def jobs = sql """SHOW EXPORT WHERE LABEL = "${label}" """
+            if (jobs.size() == 1 && (jobs[0][2] == "FINISHED" || jobs[0][2] == 
"CANCELLED")) {
+                finalJob = jobs[0]
+                break
+            }
+            sleep(1000)
+        }
+        return finalJob
+    }
+
+    String s3Endpoint = getS3Endpoint()
+    String s3Region = getS3Region()
+    String s3Provider = getS3Provider()
+    String s3Ak = getS3AK()
+    String s3Sk = getS3SK()
+    def contractViolations = []
+
+    def verifyPreflightBeforeDelete = { variant, Map<String, String> 
invalidProperties,
+            String expectedMessage ->
+        def before = restoreFixture()
+        String label = "${variant}_${runId}"
+        String propertySql = invalidProperties.collect { key, value ->
+            return "\"${key}\" = \"${value}\""
+        }.join(",\n")
+
+        try {
+            sql """
+                EXPORT TABLE test_export_preflight_before_delete
+                TO "s3://${bucket}/${targetPrefix}"
+                PROPERTIES(
+                    "label" = "${label}",
+                    "delete_existing_files" = "true",
+                    ${propertySql}
+                )
+                WITH S3(
+                    "s3.endpoint" = "${s3Endpoint}",
+                    "s3.region" = "${s3Region}",
+                    "s3.secret_key" = "${s3Sk}",
+                    "s3.access_key" = "${s3Ak}",
+                    "provider" = "${s3Provider}"
+                )
+            """
+            contractViolations.add("${variant}: submission succeeded")
+        } catch (Exception e) {
+            if 
(!e.getMessage()?.toLowerCase()?.contains(expectedMessage.toLowerCase())) {
+                contractViolations.add("${variant}: unexpected submission 
error: ${e.getMessage()}")
+            }
+        }
+
+        def immediatelyAfterSubmit = snapshotOwnedPrefix()
+        if (immediatelyAfterSubmit != before) {
+            contractViolations.add("${variant}: fixture changed before task 
completion; "
+                    + "before=${before.keySet()}, 
after=${immediatelyAfterSubmit.keySet()}")
+        }
+
+        def jobs = sql """SHOW EXPORT WHERE LABEL = "${label}" """
+        if (!jobs.isEmpty()) {
+            def finalJob = waitForFinalJob(label)
+            if (finalJob == null) {
+                contractViolations.add("${variant}: created job did not reach 
a final state")
+            } else {
+                contractViolations.add("${variant}: created job 
state=${finalJob[2]}, error=${finalJob[10]}")
+            }
+        }
+
+        def afterFinalState = snapshotOwnedPrefix()
+        if (afterFinalState != before) {
+            contractViolations.add("${variant}: fixture changed at final 
state; "
+                    + "before=${before.keySet()}, 
after=${afterFinalState.keySet()}")
+        }
+    }
+
+    try {
+        verifyPreflightBeforeDelete("invalid_format", ["format": 
"invalid_format"], "format")
+        verifyPreflightBeforeDelete("missing_column", ["columns": 
"id,missing_column"], "unknown column")
+        verifyPreflightBeforeDelete("small_max_file_size", ["max_file_size": 
"1MB"], "max file size")
+        verifyPreflightBeforeDelete("invalid_compression",
+                ["format": "csv", "compress_type": "invalid"], "unknown 
compression type")
+        verifyPreflightBeforeDelete("unknown_property", 
["unknown_export_property": "true"],
+                "invalid property key")
+
+        assertTrue(contractViolations.isEmpty(), contractViolations.join("\n"))
+    } finally {
+        cleanupOwnedPrefix()
+        try_sql("DROP TABLE IF EXISTS test_export_preflight_before_delete")
+    }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to