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

pchenxi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git


The following commit(s) were added to refs/heads/main by this push:
     new 7e982fa069 [#10165] improvement: Escape partition/statistic values in 
Lance delete filters to prevent malformed SQL (#10324)
7e982fa069 is described below

commit 7e982fa06918b36eec26cfcda186d695386b6323
Author: Lucas <[email protected]>
AuthorDate: Tue Mar 10 14:04:54 2026 +0800

    [#10165] improvement: Escape partition/statistic values in Lance delete 
filters to prevent malformed SQL (#10324)
    
    <!--
    1. Title: [#<issue>] <type>(<scope>): <subject>
       Examples:
         - "[#123] feat(operator): support xxx"
         - "[#233] fix: check null before access result in xxx"
         - "[MINOR] refactor: fix typo in variable name"
         - "[MINOR] docs: fix typo in README"
         - "[#255] test: fix flaky test NameOfTheTest"
       Reference: https://www.conventionalcommits.org/en/v1.0.0/
    2. If the PR is unfinished, please mark this PR as draft.
    -->
    
    
    ### What changes were proposed in this pull request?
    - Added a static escapeSqlLiteral method in
    LancePartitionStatisticStorage class that escapes single quotes in SQL
    literals by replacing ' with ''
    - Used this method to escape partition names and statistic names in
    dropStatisticsImpl method
    - Also applied the same fix to getPartitionFilter method to handle
    partition names with single quotes
    
    ### Why are the changes needed?
    
    Fix: #10165
    
    ### Does this PR introduce _any_ user-facing change?
    
    
    ### How was this patch tested?
    
    - Added a new test case
    testDropStatisticsWithQuoteInPartitionAndStatisticName that specifically
    tests partition names with single quotes
---
 .../storage/LancePartitionStatisticStorage.java    |  14 ++-
 .../TestLancePartitionStatisticStorage.java        | 101 +++++++++++++++++++++
 2 files changed, 111 insertions(+), 4 deletions(-)

diff --git 
a/core/src/main/java/org/apache/gravitino/stats/storage/LancePartitionStatisticStorage.java
 
b/core/src/main/java/org/apache/gravitino/stats/storage/LancePartitionStatisticStorage.java
index a2bfb0c720..918c94218c 100644
--- 
a/core/src/main/java/org/apache/gravitino/stats/storage/LancePartitionStatisticStorage.java
+++ 
b/core/src/main/java/org/apache/gravitino/stats/storage/LancePartitionStatisticStorage.java
@@ -326,9 +326,11 @@ public class LancePartitionStatisticStorage implements 
PartitionStatisticStorage
             "table_id = "
                 + tableId
                 + " AND partition_name = '"
-                + partition
+                + escapeSqlLiteral(partition)
                 + "' AND statistic_name IN ("
-                + statistics.stream().map(str -> "'" + str + 
"'").collect(Collectors.joining(", "))
+                + statistics.stream()
+                    .map(str -> "'" + escapeSqlLiteral(str) + "'")
+                    .collect(Collectors.joining(", "))
                 + ")");
       }
 
@@ -449,7 +451,7 @@ public class LancePartitionStatisticStorage implements 
PartitionStatisticStorage
                                 "AND partition_name "
                                     + (type == PartitionRange.BoundType.CLOSED 
? ">= " : "> ")
                                     + "'"
-                                    + name
+                                    + escapeSqlLiteral(name)
                                     + "'"))
             .orElse("");
     String toPartitionNameFilter =
@@ -464,13 +466,17 @@ public class LancePartitionStatisticStorage implements 
PartitionStatisticStorage
                                 "AND partition_name "
                                     + (type == PartitionRange.BoundType.CLOSED 
? "<= " : "< ")
                                     + "'"
-                                    + name
+                                    + escapeSqlLiteral(name)
                                     + "'"))
             .orElse("");
 
     return fromPartitionNameFilter + toPartitionNameFilter;
   }
 
+  private static String escapeSqlLiteral(String value) {
+    return value.replace("'", "''");
+  }
+
   private List<PersistedPartitionStatistics> listStatisticsImpl(
       Long tableId, String partitionFilter) {
 
diff --git 
a/core/src/test/java/org/apache/gravitino/stats/storage/TestLancePartitionStatisticStorage.java
 
b/core/src/test/java/org/apache/gravitino/stats/storage/TestLancePartitionStatisticStorage.java
index 6f2527a24b..7a6755870f 100644
--- 
a/core/src/test/java/org/apache/gravitino/stats/storage/TestLancePartitionStatisticStorage.java
+++ 
b/core/src/test/java/org/apache/gravitino/stats/storage/TestLancePartitionStatisticStorage.java
@@ -493,4 +493,105 @@ public class TestLancePartitionStatisticStorage {
     }
     return newData;
   }
+
+  @Test
+  public void testDropStatisticsWithQuoteInPartitionAndStatisticName() throws 
Exception {
+    PartitionStatisticStorageFactory factory = new 
LancePartitionStatisticStorageFactory();
+    String metalakeName = "metalake";
+    MetadataObject metadataObject =
+        MetadataObjects.of(
+            Lists.newArrayList("catalog", "schema", "table"), 
MetadataObject.Type.TABLE);
+
+    EntityStore entityStore = mock(EntityStore.class);
+    TableEntity tableEntity = mock(TableEntity.class);
+    when(entityStore.get(any(), any(), any())).thenReturn(tableEntity);
+    when(tableEntity.id()).thenReturn(1L);
+    FieldUtils.writeField(GravitinoEnv.getInstance(), "entityStore", 
entityStore, true);
+
+    String location = 
Files.createTempDirectory("lance_stats_test_quote").toString();
+    Map<String, String> properties = Maps.newHashMap();
+    properties.put("location", location);
+
+    LancePartitionStatisticStorage storage =
+        (LancePartitionStatisticStorage) factory.create(properties);
+    try {
+      String quotedPartition = "partition'01";
+      String quotedStatistic = "statistic'0";
+      String normalStatistic = "statistic1";
+
+      Map<String, StatisticValue<?>> stats = Maps.newHashMap();
+      stats.put(quotedStatistic, StatisticValues.stringValue("value0"));
+      stats.put(normalStatistic, StatisticValues.stringValue("value1"));
+
+      storage.updateStatistics(
+          metalakeName,
+          Lists.newArrayList(
+              MetadataObjectStatisticsUpdate.of(
+                  metadataObject,
+                  Lists.newArrayList(
+                      PartitionStatisticsModification.update(quotedPartition, 
stats)))));
+
+      List<PersistedPartitionStatistics> listedStats =
+          storage.listStatistics(
+              metalakeName,
+              metadataObject,
+              PartitionRange.between(
+                  quotedPartition,
+                  PartitionRange.BoundType.CLOSED,
+                  quotedPartition,
+                  PartitionRange.BoundType.CLOSED));
+
+      Assertions.assertEquals(1, listedStats.size());
+      Assertions.assertEquals(quotedPartition, 
listedStats.get(0).partitionName());
+      Assertions.assertEquals(2, listedStats.get(0).statistics().size());
+
+      storage.dropStatistics(
+          metalakeName,
+          Lists.newArrayList(
+              MetadataObjectStatisticsDrop.of(
+                  metadataObject,
+                  Lists.newArrayList(
+                      PartitionStatisticsModification.drop(
+                          quotedPartition, 
Lists.newArrayList(quotedStatistic))))));
+
+      listedStats =
+          storage.listStatistics(
+              metalakeName,
+              metadataObject,
+              PartitionRange.between(
+                  quotedPartition,
+                  PartitionRange.BoundType.CLOSED,
+                  quotedPartition,
+                  PartitionRange.BoundType.CLOSED));
+
+      Assertions.assertEquals(1, listedStats.size());
+      Assertions.assertEquals(quotedPartition, 
listedStats.get(0).partitionName());
+      Assertions.assertEquals(1, listedStats.get(0).statistics().size());
+      Assertions.assertEquals(normalStatistic, 
listedStats.get(0).statistics().get(0).name());
+
+      storage.dropStatistics(
+          metalakeName,
+          Lists.newArrayList(
+              MetadataObjectStatisticsDrop.of(
+                  metadataObject,
+                  Lists.newArrayList(
+                      PartitionStatisticsModification.drop(
+                          quotedPartition, 
Lists.newArrayList(normalStatistic))))));
+
+      listedStats =
+          storage.listStatistics(
+              metalakeName,
+              metadataObject,
+              PartitionRange.between(
+                  quotedPartition,
+                  PartitionRange.BoundType.CLOSED,
+                  quotedPartition,
+                  PartitionRange.BoundType.CLOSED));
+
+      Assertions.assertTrue(listedStats.isEmpty());
+    } finally {
+      FileUtils.deleteDirectory(new File(location + "/" + tableEntity.id() + 
".lance"));
+      storage.close();
+    }
+  }
 }

Reply via email to