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();
+ }
+ }
}