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

xiangfu0 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pinot.git


The following commit(s) were added to refs/heads/master by this push:
     new 081a013e4f1 Add safe SQL quoting for typed table names (#19327)
081a013e4f1 is described below

commit 081a013e4f1d3745b3f725f8f3e269cd50408986
Author: Xiang Fu <[email protected]>
AuthorDate: Sat Aug 22 20:50:48 2026 -0700

    Add safe SQL quoting for typed table names (#19327)
---
 .../pinot/sql/parsers/CalciteSqlCompilerTest.java  | 14 ++++++++
 .../java/org/apache/pinot/spi/utils/SqlUtils.java} | 31 +++++++---------
 .../pinot/spi/utils/TimestampIndexUtils.java       |  2 +-
 .../pinot/spi/utils/builder/TableNameBuilder.java  | 42 ++++++++++++++++++++++
 ...TableNameBuilderTest.java => SqlUtilsTest.java} | 24 ++++++-------
 .../pinot/spi/utils/TimestampIndexUtilsTest.java   | 17 +++++++++
 .../spi/utils/builder/TableNameBuilderTest.java    | 41 +++++++++++++++++++++
 .../pinot/sql/ddl/reverse/SqlIdentifiers.java      |  3 +-
 8 files changed, 139 insertions(+), 35 deletions(-)

diff --git 
a/pinot-common/src/test/java/org/apache/pinot/sql/parsers/CalciteSqlCompilerTest.java
 
b/pinot-common/src/test/java/org/apache/pinot/sql/parsers/CalciteSqlCompilerTest.java
index dcc242e8e31..f3ee5d141b8 100644
--- 
a/pinot-common/src/test/java/org/apache/pinot/sql/parsers/CalciteSqlCompilerTest.java
+++ 
b/pinot-common/src/test/java/org/apache/pinot/sql/parsers/CalciteSqlCompilerTest.java
@@ -34,6 +34,7 @@ import org.apache.pinot.common.request.JoinType;
 import org.apache.pinot.common.request.Literal;
 import org.apache.pinot.common.request.PinotQuery;
 import org.apache.pinot.segment.spi.AggregationFunctionType;
+import org.apache.pinot.spi.utils.builder.TableNameBuilder;
 import org.apache.pinot.sql.FilterKind;
 import org.apache.pinot.sql.parsers.parser.ParseException;
 import org.apache.pinot.sql.parsers.parser.SqlInsertFromFile;
@@ -3326,6 +3327,19 @@ public class CalciteSqlCompilerTest {
     Assert.assertEquals(defaultCatalogQuery.getDataSource().getTableName(), 
"default.foo");
   }
 
+  @Test
+  public void testQuotedTypedTableNameRoundTrip() {
+    List<String> tableNamesWithType = List.of(
+        "events_OFFLINE",
+        "analytics.events_REALTIME",
+        "db\"name.events\";DROP_TABLE--_OFFLINE");
+    for (String tableNameWithType : tableNamesWithType) {
+      PinotQuery pinotQuery = compileToPinotQuery(
+          "SELECT * FROM " + 
TableNameBuilder.quoteTableNameWithType(tableNameWithType));
+      Assert.assertEquals(pinotQuery.getDataSource().getTableName(), 
tableNameWithType);
+    }
+  }
+
   @Test
   public void testInvalidQueryWithSemicolon() {
     Assert.expectThrows(SqlCompilationException.class, () -> 
compileToPinotQuery(";"));
diff --git 
a/pinot-spi/src/test/java/org/apache/pinot/spi/utils/builder/TableNameBuilderTest.java
 b/pinot-spi/src/main/java/org/apache/pinot/spi/utils/SqlUtils.java
similarity index 52%
copy from 
pinot-spi/src/test/java/org/apache/pinot/spi/utils/builder/TableNameBuilderTest.java
copy to pinot-spi/src/main/java/org/apache/pinot/spi/utils/SqlUtils.java
index c3204cce84e..4fb0f2c40d3 100644
--- 
a/pinot-spi/src/test/java/org/apache/pinot/spi/utils/builder/TableNameBuilderTest.java
+++ b/pinot-spi/src/main/java/org/apache/pinot/spi/utils/SqlUtils.java
@@ -16,27 +16,20 @@
  * specific language governing permissions and limitations
  * under the License.
  */
+package org.apache.pinot.spi.utils;
 
-package org.apache.pinot.spi.utils.builder;
 
-import com.google.common.collect.ImmutableSet;
-import org.testng.annotations.Test;
-
-import static org.testng.Assert.assertEquals;
-
-
-public class TableNameBuilderTest {
-
-  @Test
-  public void testGetTableNameVariations() {
-
-    assertEquals(TableNameBuilder.getTableNameVariations("tableAbc"),
-        ImmutableSet.of("tableAbc", "tableAbc_REALTIME", "tableAbc_OFFLINE"));
-
-    assertEquals(TableNameBuilder.getTableNameVariations("tableAbc_REALTIME"),
-        ImmutableSet.of("tableAbc", "tableAbc_REALTIME", "tableAbc_OFFLINE"));
+/// Utilities for constructing SQL fragments. This stateless utility is 
thread-safe.
+public class SqlUtils {
+  private SqlUtils() {
+  }
 
-    assertEquals(TableNameBuilder.getTableNameVariations("tableAbc_OFFLINE"),
-        ImmutableSet.of("tableAbc", "tableAbc_REALTIME", "tableAbc_OFFLINE"));
+  /// Quotes one SQL identifier component with double quotes, escaping 
embedded double quotes by doubling them.
+  /// Qualified names must be split by the caller so that each component is 
quoted separately.
+  ///
+  /// @param identifier SQL identifier component
+  /// @return Identifier quoted for use in a SQL statement
+  public static String quoteIdentifier(String identifier) {
+    return "\"" + identifier.replace("\"", "\"\"") + "\"";
   }
 }
diff --git 
a/pinot-spi/src/main/java/org/apache/pinot/spi/utils/TimestampIndexUtils.java 
b/pinot-spi/src/main/java/org/apache/pinot/spi/utils/TimestampIndexUtils.java
index 66e221a933b..d095d02b6f5 100644
--- 
a/pinot-spi/src/main/java/org/apache/pinot/spi/utils/TimestampIndexUtils.java
+++ 
b/pinot-spi/src/main/java/org/apache/pinot/spi/utils/TimestampIndexUtils.java
@@ -190,6 +190,6 @@ public class TimestampIndexUtils {
   }
 
   private static String getTransformExpression(String timestampColumn, 
TimestampIndexGranularity granularity) {
-    return "dateTrunc('" + granularity + "',\"" + timestampColumn + "\")";
+    return "dateTrunc('" + granularity + "'," + 
SqlUtils.quoteIdentifier(timestampColumn) + ")";
   }
 }
diff --git 
a/pinot-spi/src/main/java/org/apache/pinot/spi/utils/builder/TableNameBuilder.java
 
b/pinot-spi/src/main/java/org/apache/pinot/spi/utils/builder/TableNameBuilder.java
index e2f44ca00d2..a9dd0fd5ff6 100644
--- 
a/pinot-spi/src/main/java/org/apache/pinot/spi/utils/builder/TableNameBuilder.java
+++ 
b/pinot-spi/src/main/java/org/apache/pinot/spi/utils/builder/TableNameBuilder.java
@@ -22,6 +22,7 @@ import com.google.common.collect.ImmutableSet;
 import java.util.Set;
 import javax.annotation.Nullable;
 import org.apache.pinot.spi.config.table.TableType;
+import org.apache.pinot.spi.utils.SqlUtils;
 
 
 public class TableNameBuilder {
@@ -117,6 +118,47 @@ public class TableNameBuilder {
     return REALTIME.tableHasTypeSuffix(resourceName);
   }
 
+  /// Quotes a typed table name for use as a SQL identifier. When the table 
name is database-qualified, the database
+  /// and table components are quoted separately. Embedded double quotes are 
escaped by doubling them.
+  ///
+  /// @param tableNameWithType Table name ending in `_OFFLINE` or `_REALTIME`, 
optionally prefixed with a database name
+  /// @return Table name quoted for use in a SQL statement
+  /// @throws IllegalArgumentException If the table name is not a valid typed 
table resource
+  public static String quoteTableNameWithType(String tableNameWithType) {
+    if (tableNameWithType == null || containsWhitespace(tableNameWithType)) {
+      throw new IllegalArgumentException("Invalid table name with type");
+    }
+
+    int separatorIndex = tableNameWithType.indexOf('.');
+    if (separatorIndex < 0) {
+      validateTableNameWithType(tableNameWithType);
+      return SqlUtils.quoteIdentifier(tableNameWithType);
+    }
+    if (separatorIndex == 0 || separatorIndex != 
tableNameWithType.lastIndexOf('.')) {
+      throw new IllegalArgumentException("Invalid table name with type");
+    }
+
+    String tableNameWithTypeWithoutDatabase = 
tableNameWithType.substring(separatorIndex + 1);
+    validateTableNameWithType(tableNameWithTypeWithoutDatabase);
+    return SqlUtils.quoteIdentifier(tableNameWithType.substring(0, 
separatorIndex)) + "."
+        + SqlUtils.quoteIdentifier(tableNameWithTypeWithoutDatabase);
+  }
+
+  private static void validateTableNameWithType(String tableNameWithType) {
+    if (!isTableResource(tableNameWithType)) {
+      throw new IllegalArgumentException("Invalid table name with type");
+    }
+  }
+
+  private static boolean containsWhitespace(String value) {
+    for (int i = 0; i < value.length(); i++) {
+      if (Character.isWhitespace(value.charAt(i))) {
+        return true;
+      }
+    }
+    return false;
+  }
+
   public static Set<String> getTableNameVariations(String tableName) {
     String rawTableName = extractRawTableName(tableName);
     String offlineTableName = OFFLINE.tableNameWithType(rawTableName);
diff --git 
a/pinot-spi/src/test/java/org/apache/pinot/spi/utils/builder/TableNameBuilderTest.java
 b/pinot-spi/src/test/java/org/apache/pinot/spi/utils/SqlUtilsTest.java
similarity index 58%
copy from 
pinot-spi/src/test/java/org/apache/pinot/spi/utils/builder/TableNameBuilderTest.java
copy to pinot-spi/src/test/java/org/apache/pinot/spi/utils/SqlUtilsTest.java
index c3204cce84e..9af3cf51fd3 100644
--- 
a/pinot-spi/src/test/java/org/apache/pinot/spi/utils/builder/TableNameBuilderTest.java
+++ b/pinot-spi/src/test/java/org/apache/pinot/spi/utils/SqlUtilsTest.java
@@ -16,27 +16,23 @@
  * specific language governing permissions and limitations
  * under the License.
  */
+package org.apache.pinot.spi.utils;
 
-package org.apache.pinot.spi.utils.builder;
-
-import com.google.common.collect.ImmutableSet;
 import org.testng.annotations.Test;
 
 import static org.testng.Assert.assertEquals;
 
 
-public class TableNameBuilderTest {
+/// Tests for [SqlUtils].
+public class SqlUtilsTest {
 
   @Test
-  public void testGetTableNameVariations() {
-
-    assertEquals(TableNameBuilder.getTableNameVariations("tableAbc"),
-        ImmutableSet.of("tableAbc", "tableAbc_REALTIME", "tableAbc_OFFLINE"));
-
-    assertEquals(TableNameBuilder.getTableNameVariations("tableAbc_REALTIME"),
-        ImmutableSet.of("tableAbc", "tableAbc_REALTIME", "tableAbc_OFFLINE"));
-
-    assertEquals(TableNameBuilder.getTableNameVariations("tableAbc_OFFLINE"),
-        ImmutableSet.of("tableAbc", "tableAbc_REALTIME", "tableAbc_OFFLINE"));
+  public void testQuoteIdentifier() {
+    assertEquals(SqlUtils.quoteIdentifier("column"), "\"column\"");
+    assertEquals(SqlUtils.quoteIdentifier("column with space"), "\"column with 
space\"");
+    assertEquals(SqlUtils.quoteIdentifier("database.table"), 
"\"database.table\"");
+    assertEquals(SqlUtils.quoteIdentifier("column\"name"), 
"\"column\"\"name\"");
+    assertEquals(SqlUtils.quoteIdentifier("column\"; DROP TABLE events; --"),
+        "\"column\"\"; DROP TABLE events; --\"");
   }
 }
diff --git 
a/pinot-spi/src/test/java/org/apache/pinot/spi/utils/TimestampIndexUtilsTest.java
 
b/pinot-spi/src/test/java/org/apache/pinot/spi/utils/TimestampIndexUtilsTest.java
index c7f4ec4fef3..2adb2ea2e29 100644
--- 
a/pinot-spi/src/test/java/org/apache/pinot/spi/utils/TimestampIndexUtilsTest.java
+++ 
b/pinot-spi/src/test/java/org/apache/pinot/spi/utils/TimestampIndexUtilsTest.java
@@ -127,4 +127,21 @@ public class TimestampIndexUtilsTest {
       assertEquals(new HashSet<>(rangeIndexColumns), transformColumns);
     }
   }
+
+  @Test
+  public void testApplyTimestampIndexEscapesQuotedColumn() {
+    String timestampColumn = "ts\"name";
+    TableConfig tableConfig = new 
TableConfigBuilder(TableType.OFFLINE).setTableName("testTable").setFieldConfigList(
+        List.of(new FieldConfig.Builder(timestampColumn)
+            .withTimestampConfig(new 
TimestampConfig(List.of(TimestampIndexGranularity.DAY))).build())).build();
+    Schema schema = new Schema.SchemaBuilder().setSchemaName("testTable")
+        .addDateTime(timestampColumn, DataType.TIMESTAMP, "TIMESTAMP", 
"1:MILLISECONDS").build();
+
+    TimestampIndexUtils.applyTimestampIndex(tableConfig, schema);
+
+    List<TransformConfig> transformConfigs = 
tableConfig.getIngestionConfig().getTransformConfigs();
+    assertEquals(transformConfigs.size(), 1);
+    assertEquals(transformConfigs.get(0).getColumnName(), "$ts\"name$DAY");
+    assertEquals(transformConfigs.get(0).getTransformFunction(), 
"dateTrunc('DAY',\"ts\"\"name\")");
+  }
 }
diff --git 
a/pinot-spi/src/test/java/org/apache/pinot/spi/utils/builder/TableNameBuilderTest.java
 
b/pinot-spi/src/test/java/org/apache/pinot/spi/utils/builder/TableNameBuilderTest.java
index c3204cce84e..dcb0dee00d6 100644
--- 
a/pinot-spi/src/test/java/org/apache/pinot/spi/utils/builder/TableNameBuilderTest.java
+++ 
b/pinot-spi/src/test/java/org/apache/pinot/spi/utils/builder/TableNameBuilderTest.java
@@ -23,6 +23,7 @@ import com.google.common.collect.ImmutableSet;
 import org.testng.annotations.Test;
 
 import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertThrows;
 
 
 public class TableNameBuilderTest {
@@ -39,4 +40,44 @@ public class TableNameBuilderTest {
     assertEquals(TableNameBuilder.getTableNameVariations("tableAbc_OFFLINE"),
         ImmutableSet.of("tableAbc", "tableAbc_REALTIME", "tableAbc_OFFLINE"));
   }
+
+  @Test
+  public void testQuoteTableNameWithType() {
+    assertEquals(TableNameBuilder.quoteTableNameWithType("events_OFFLINE"), 
"\"events_OFFLINE\"");
+    assertEquals(TableNameBuilder.quoteTableNameWithType("events_REALTIME"), 
"\"events_REALTIME\"");
+    
assertEquals(TableNameBuilder.quoteTableNameWithType("analytics.events_OFFLINE"),
+        "\"analytics\".\"events_OFFLINE\"");
+    
assertEquals(TableNameBuilder.quoteTableNameWithType("analytics.my-table$1_REALTIME"),
+        "\"analytics\".\"my-table$1_REALTIME\"");
+    
assertEquals(TableNameBuilder.quoteTableNameWithType("\u6570\u636E\u5E93.\u4E8B\u4EF6_REALTIME"),
+        "\"\u6570\u636E\u5E93\".\"\u4E8B\u4EF6_REALTIME\"");
+    assertEquals(TableNameBuilder.quoteTableNameWithType("_OFFLINE"), 
"\"_OFFLINE\"");
+    
assertEquals(TableNameBuilder.quoteTableNameWithType("analytics._REALTIME"),
+        "\"analytics\".\"_REALTIME\"");
+  }
+
+  @Test
+  public void testQuoteTableNameWithTypeEscapesEmbeddedQuotes() {
+    
assertEquals(TableNameBuilder.quoteTableNameWithType("db\"name.events\";DROP_TABLE--_OFFLINE"),
+        "\"db\"\"name\".\"events\"\";DROP_TABLE--_OFFLINE\"");
+  }
+
+  @Test
+  public void testQuoteTableNameWithTypeRejectsInvalidNames() {
+    String[] invalidTableNames = {
+        null,
+        "",
+        "events",
+        "events_offline",
+        ".events_OFFLINE",
+        "analytics.",
+        "analytics.events_OFFLINE.extra",
+        "analytics..events_REALTIME",
+        "events name_OFFLINE",
+        "events_OFFLINE;SELECT"
+    };
+    for (String invalidTableName : invalidTableNames) {
+      assertThrows(IllegalArgumentException.class, () -> 
TableNameBuilder.quoteTableNameWithType(invalidTableName));
+    }
+  }
 }
diff --git 
a/pinot-sql-ddl/src/main/java/org/apache/pinot/sql/ddl/reverse/SqlIdentifiers.java
 
b/pinot-sql-ddl/src/main/java/org/apache/pinot/sql/ddl/reverse/SqlIdentifiers.java
index e8cacf01062..4ccd1159608 100644
--- 
a/pinot-sql-ddl/src/main/java/org/apache/pinot/sql/ddl/reverse/SqlIdentifiers.java
+++ 
b/pinot-sql-ddl/src/main/java/org/apache/pinot/sql/ddl/reverse/SqlIdentifiers.java
@@ -22,6 +22,7 @@ import com.google.common.collect.ImmutableSet;
 import java.util.Locale;
 import java.util.Set;
 import java.util.regex.Pattern;
+import org.apache.pinot.spi.utils.SqlUtils;
 
 
 /// Quoting helpers used by the canonical DDL emitter.
@@ -62,7 +63,7 @@ final class SqlIdentifiers {
   /// bare otherwise. Embedded double quotes are escaped per SQL convention 
(`"` → `""`).
   static String quote(String identifier) {
     if (mustQuote(identifier)) {
-      return "\"" + identifier.replace("\"", "\"\"") + "\"";
+      return SqlUtils.quoteIdentifier(identifier);
     }
     return identifier;
   }


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

Reply via email to