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

yuqi1129 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 8b5bfb44d4 [#11933] feat(clickhouse): support ngrambf_v1 and 
tokenbf_v1 data skipping index types (#12026)
8b5bfb44d4 is described below

commit 8b5bfb44d4c6a5a0b01b48e7cba0159496f6c648
Author: StormSpirit <[email protected]>
AuthorDate: Sat Aug 29 10:02:44 2026 +0800

    [#11933] feat(clickhouse): support ngrambf_v1 and tokenbf_v1 data skipping 
index types (#12026)
    
    ### What changes were proposed in this pull request?
    
    Add support for `ngrambf_v1` and `tokenbf_v1` data skipping index types
    in the ClickHouse catalog.
    
    - Add `DATA_SKIPPING_NGRAMBFV1` and `DATA_SKIPPING_TOKENBFV1` to
    `Index.IndexType`.
    - Add ClickHouse type constants and the documented `Index.properties()`
    keys for bloom-filter parameters.
    - Read `type_full` from `system.data_skipping_indices` to restore the
    required parameters on table load, with a legacy query fallback when the
    column is unavailable.
    - Generate parameterized DDL for both CREATE TABLE and ALTER TABLE ADD
    INDEX, sharing the same validation and granularity handling.
    - Add single-node and ON CLUSTER integration coverage for CREATE, LOAD,
    ALTER, parameter round-trip, and SHOW CREATE behavior.
    
    ### Why are the changes needed?
    
    ClickHouse tables containing `ngrambf_v1` or `tokenbf_v1` indexes
    currently load without preserving their index type metadata, while
    attempts to create these index types through Gravitino fail because the
    catalog does not generate their required parameters. This change
    preserves the metadata when `type_full` is available and provides a
    defensive legacy-query fallback for older metadata schemas. The current
    Gravitino compatibility matrix continues to explicitly verify ClickHouse
    24.8.x; the fallback does not declare a new supported server-version
    baseline.
    
    Fix: #11933
    
    ### Does this PR introduce _any_ user-facing change?
    
    Yes. It adds two `Index.IndexType` values and the following
    `Index.properties()` keys: `ngram_size`, `bloom_filter_size`,
    `hash_functions`, and `random_seed`; `granularity` remains optional and
    defaults to `1`. `ngrambf_v1` requires all four bloom-filter parameters,
    while `tokenbf_v1` requires the latter three. On a server whose legacy
    `type` metadata is bare and does not include parameters, the catalog can
    preserve the index type and fields but cannot reconstruct the required
    parameter properties for a full recreate operation; callers must provide
    those properties explicitly.
    
    ### How was this patch tested?
    
    - `./gradlew :catalogs-contrib:catalog-jdbc-clickhouse:spotlessCheck`
    - `./gradlew :catalogs-contrib:catalog-jdbc-clickhouse:test -PskipITs
    -PskipDockerTests=true` — 61 tests passed.
    - `./gradlew compileJava -x test`
    - `python3 ~/GitHub/bin/gravitino-pr-precheck.py` — no errors.
    - ClickHouse 24.8.14 `CatalogClickHouseIT` — 46 tests passed with no
    skips, failures, or errors.
    - ClickHouse 24.8.14 `CatalogClickHouseClusterIT` — 18 tests passed with
    no skips, failures, or errors, including the new ngrambf/tokenbf cluster
    test.
    
    ---------
    
    Signed-off-by: jiangxt2 <[email protected]>
---
 .../org/apache/gravitino/rel/indexes/Index.java    |  16 +-
 .../catalog/clickhouse/ClickHouseConstants.java    |  18 +
 .../operations/ClickHouseTableOperations.java      | 393 ++++++++++++---
 .../test/CatalogClickHouseClusterIT.java           |  90 ++++
 .../integration/test/CatalogClickHouseIT.java      | 142 ++++++
 .../operations/TestClickHouseTableOperations.java  | 528 +++++++++++++++++++++
 .../TestClickHouseTableOperationsUnit.java         | 136 ++++++
 docs/jdbc-clickhouse-catalog.md                    |   8 +-
 docs/open-api/indexes.yaml                         |   2 +
 9 files changed, 1265 insertions(+), 68 deletions(-)

diff --git a/api/src/main/java/org/apache/gravitino/rel/indexes/Index.java 
b/api/src/main/java/org/apache/gravitino/rel/indexes/Index.java
index 9f725c19cc..ec3d6cbce7 100644
--- a/api/src/main/java/org/apache/gravitino/rel/indexes/Index.java
+++ b/api/src/main/java/org/apache/gravitino/rel/indexes/Index.java
@@ -122,8 +122,7 @@ public interface Index {
     /** IVF_HNSW_PQ */
     IVF_HNSW_PQ,
 
-    // The following index types are data skipping indexes in ClickHouse, 
ngrambf_v1 and tokenbf_v1
-    // Will be supported later.
+    // The following index types are data skipping indexes in ClickHouse.
     /** minmax data skipping index */
     DATA_SKIPPING_MINMAX,
 
@@ -132,5 +131,18 @@ public interface Index {
 
     /** Set data skipping index */
     DATA_SKIPPING_SET,
+
+    /**
+     * Ngram bloom filter data skipping index. Uses n-gram tokenization of 
string columns for
+     * bloom-filter-based text search acceleration. Available since ClickHouse 
v19.x.
+     */
+    DATA_SKIPPING_NGRAMBFV1,
+
+    /**
+     * Token bloom filter data skipping index. Uses tokenization (by 
non-alphanumeric delimiters) of
+     * string columns for bloom-filter-based text search acceleration. 
Available since ClickHouse
+     * v19.x.
+     */
+    DATA_SKIPPING_TOKENBFV1,
   }
 }
diff --git 
a/catalogs-contrib/catalog-jdbc-clickhouse/src/main/java/org/apache/gravitino/catalog/clickhouse/ClickHouseConstants.java
 
b/catalogs-contrib/catalog-jdbc-clickhouse/src/main/java/org/apache/gravitino/catalog/clickhouse/ClickHouseConstants.java
index 473ac35d3d..b040ba4bda 100644
--- 
a/catalogs-contrib/catalog-jdbc-clickhouse/src/main/java/org/apache/gravitino/catalog/clickhouse/ClickHouseConstants.java
+++ 
b/catalogs-contrib/catalog-jdbc-clickhouse/src/main/java/org/apache/gravitino/catalog/clickhouse/ClickHouseConstants.java
@@ -70,5 +70,23 @@ public class ClickHouseConstants {
 
     // Key for max unique values (N) in set(N) data-skipping index properties.
     public static final String SET_MAX_VALUES = "set_max_values";
+
+    /** The name of the data skipping index type for ngrambf_v1 in ClickHouse. 
*/
+    public static final String DATA_SKIPPING_NGRAMBFV1 = "ngrambf_v1";
+
+    /** The name of the data skipping index type for tokenbf_v1 in ClickHouse. 
*/
+    public static final String DATA_SKIPPING_TOKENBFV1 = "tokenbf_v1";
+
+    /** Property key for bloom filter size in ngrambf_v1 and tokenbf_v1 index 
properties. */
+    public static final String BLOOM_FILTER_SIZE = "bloom_filter_size";
+
+    /** Property key for the number of hash functions in ngrambf_v1 and 
tokenbf_v1 properties. */
+    public static final String HASH_FUNCTIONS = "hash_functions";
+
+    /** Property key for the random seed in ngrambf_v1 and tokenbf_v1 index 
properties. */
+    public static final String RANDOM_SEED = "random_seed";
+
+    /** Property key for the n-gram size in ngrambf_v1 index properties. */
+    public static final String NGRAM_SIZE = "ngram_size";
   }
 }
diff --git 
a/catalogs-contrib/catalog-jdbc-clickhouse/src/main/java/org/apache/gravitino/catalog/clickhouse/operations/ClickHouseTableOperations.java
 
b/catalogs-contrib/catalog-jdbc-clickhouse/src/main/java/org/apache/gravitino/catalog/clickhouse/operations/ClickHouseTableOperations.java
index 8d7afb9ed5..77115b9113 100644
--- 
a/catalogs-contrib/catalog-jdbc-clickhouse/src/main/java/org/apache/gravitino/catalog/clickhouse/operations/ClickHouseTableOperations.java
+++ 
b/catalogs-contrib/catalog-jdbc-clickhouse/src/main/java/org/apache/gravitino/catalog/clickhouse/operations/ClickHouseTableOperations.java
@@ -18,10 +18,16 @@
  */
 package org.apache.gravitino.catalog.clickhouse.operations;
 
+import static 
org.apache.gravitino.catalog.clickhouse.ClickHouseConstants.IndexConstants.BLOOM_FILTER_SIZE;
 import static 
org.apache.gravitino.catalog.clickhouse.ClickHouseConstants.IndexConstants.DATA_SKIPPING_BLOOM_FILTER;
 import static 
org.apache.gravitino.catalog.clickhouse.ClickHouseConstants.IndexConstants.DATA_SKIPPING_MINMAX_VALUE;
+import static 
org.apache.gravitino.catalog.clickhouse.ClickHouseConstants.IndexConstants.DATA_SKIPPING_NGRAMBFV1;
 import static 
org.apache.gravitino.catalog.clickhouse.ClickHouseConstants.IndexConstants.DATA_SKIPPING_SET;
+import static 
org.apache.gravitino.catalog.clickhouse.ClickHouseConstants.IndexConstants.DATA_SKIPPING_TOKENBFV1;
 import static 
org.apache.gravitino.catalog.clickhouse.ClickHouseConstants.IndexConstants.GRANULARITY;
+import static 
org.apache.gravitino.catalog.clickhouse.ClickHouseConstants.IndexConstants.HASH_FUNCTIONS;
+import static 
org.apache.gravitino.catalog.clickhouse.ClickHouseConstants.IndexConstants.NGRAM_SIZE;
+import static 
org.apache.gravitino.catalog.clickhouse.ClickHouseConstants.IndexConstants.RANDOM_SEED;
 import static 
org.apache.gravitino.catalog.clickhouse.ClickHouseConstants.IndexConstants.SET_MAX_VALUES;
 import static 
org.apache.gravitino.catalog.clickhouse.ClickHouseTablePropertiesMetadata.CLICKHOUSE_ENGINE_KEY;
 import static 
org.apache.gravitino.catalog.clickhouse.ClickHouseTablePropertiesMetadata.ENGINE_PROPERTY_ENTRY;
@@ -129,6 +135,13 @@ public class ClickHouseTableOperations extends 
JdbcTableOperations {
       ORDER BY COLUMN_NAME
       """;
 
+  private static final String SECONDARY_INDEX_QUERY =
+      "SELECT name, type, type_full, expr, granularity FROM 
system.data_skipping_indices "
+          + "WHERE database = ? AND table = ? ORDER BY name";
+  private static final String LEGACY_SECONDARY_INDEX_QUERY =
+      "SELECT name, type, expr, granularity FROM system.data_skipping_indices "
+          + "WHERE database = ? AND table = ? ORDER BY name";
+
   @Override
   public void create(
       String databaseName,
@@ -612,37 +625,15 @@ public class ClickHouseTableOperations extends 
JdbcTableOperations {
           sqlBuilder.append(" PRIMARY KEY (").append(fieldStr).append(")");
           break;
         case DATA_SKIPPING_MINMAX:
-          sqlBuilder
-              .append(" ")
-              .append(
-                  buildDataSkippingIndexDdl(
-                      index.name(),
-                      fieldStr,
-                      DATA_SKIPPING_MINMAX_VALUE,
-                      resolveGranularity(index.properties(), 1)));
-          break;
         case DATA_SKIPPING_BLOOM_FILTER:
-          sqlBuilder
-              .append(" ")
-              .append(
-                  buildDataSkippingIndexDdl(
-                      index.name(),
-                      fieldStr,
-                      DATA_SKIPPING_BLOOM_FILTER,
-                      resolveGranularity(index.properties(), 1)));
-          break;
         case DATA_SKIPPING_SET:
-          // SET index: set(N) max unique values default to 0 (unlimited), 
configurable via
-          // set_max_values property. GRANULARITY defaults to 1, configurable 
via granularity
-          // property, consistent with minmax and bloom_filter indexes.
+        case DATA_SKIPPING_NGRAMBFV1:
+        case DATA_SKIPPING_TOKENBFV1:
           sqlBuilder
               .append(" ")
               .append(
                   buildDataSkippingIndexDdl(
-                      index.name(),
-                      fieldStr,
-                      "set(" + resolveSetMaxValues(index.properties()) + ")",
-                      resolveGranularity(index.properties(), 1)));
+                      index.name(), fieldStr, index.type(), 
index.properties()));
           break;
         default:
           throw new IllegalArgumentException(
@@ -1119,28 +1110,13 @@ public class ClickHouseTableOperations extends 
JdbcTableOperations {
     Map<String, String> properties = addIndex.getProperties();
     switch (addIndex.getType()) {
       case DATA_SKIPPING_MINMAX:
-        return "ADD "
-            + buildDataSkippingIndexDdl(
-                addIndex.getName(),
-                fieldStr,
-                DATA_SKIPPING_MINMAX_VALUE,
-                resolveGranularity(properties, 1));
-
       case DATA_SKIPPING_BLOOM_FILTER:
-        return "ADD "
-            + buildDataSkippingIndexDdl(
-                addIndex.getName(),
-                fieldStr,
-                DATA_SKIPPING_BLOOM_FILTER,
-                resolveGranularity(properties, 1));
-
       case DATA_SKIPPING_SET:
+      case DATA_SKIPPING_NGRAMBFV1:
+      case DATA_SKIPPING_TOKENBFV1:
         return "ADD "
             + buildDataSkippingIndexDdl(
-                addIndex.getName(),
-                fieldStr,
-                "set(" + resolveSetMaxValues(properties) + ")",
-                resolveGranularity(properties, 1));
+                addIndex.getName(), fieldStr, addIndex.getType(), properties);
 
       case PRIMARY_KEY:
         throw new UnsupportedOperationException(
@@ -1613,41 +1589,95 @@ public class ClickHouseTableOperations extends 
JdbcTableOperations {
 
   private List<Index> getSecondaryIndexes(
       Connection connection, String databaseName, String tableName) throws 
SQLException {
+    try {
+      return querySecondaryIndexes(
+          connection, databaseName, tableName, SECONDARY_INDEX_QUERY, true);
+    } catch (SQLException e) {
+      if (!isMissingTypeFullColumn(e)) {
+        throw e;
+      }
+      LOG.warn(
+          "ClickHouse server does not expose 
system.data_skipping_indices.type_full; "
+              + "falling back to the legacy secondary-index query for {}.{}",
+          databaseName,
+          tableName);
+      return querySecondaryIndexes(
+          connection, databaseName, tableName, LEGACY_SECONDARY_INDEX_QUERY, 
false);
+    }
+  }
+
+  private List<Index> querySecondaryIndexes(
+      Connection connection,
+      String databaseName,
+      String tableName,
+      String query,
+      boolean includesTypeFull)
+      throws SQLException {
     List<Index> secondaryIndexes = new ArrayList<>();
-    try (PreparedStatement preparedStatement =
-        connection.prepareStatement(
-            "SELECT name, type, expr, granularity FROM 
system.data_skipping_indices "
-                + "WHERE database = ? AND table = ? ORDER BY name")) {
+    try (PreparedStatement preparedStatement = 
connection.prepareStatement(query)) {
       preparedStatement.setString(1, databaseName);
       preparedStatement.setString(2, tableName);
       try (ResultSet resultSet = preparedStatement.executeQuery()) {
         while (resultSet.next()) {
           String name = resultSet.getString("name");
           String type = resultSet.getString("type");
+          String parameterSource = includesTypeFull ? 
resultSet.getString("type_full") : type;
+          String parameterSourceName = includesTypeFull ? "type_full" : 
"legacy type";
           String expression = resultSet.getString("expr");
           long granularity = resultSet.getLong("granularity");
+          Index.IndexType indexType;
+          String[][] fields;
           try {
-            String[][] fields = parseIndexFields(expression);
-            if (ArrayUtils.isEmpty(fields)) {
-              continue;
-            }
-            // Only include granularity in properties when it differs from the 
default,
-            // so that indexes created without explicit granularity have empty 
properties
-            // and match the original creation state (avoids false 
index-change diffs).
-            Map<String, String> properties =
-                granularity == DEFAULT_INDEX_GRANULARITY
-                    ? Map.of()
-                    : Map.of(GRANULARITY, String.valueOf(granularity));
-            secondaryIndexes.add(
-                Indexes.of(getClickHouseIndexType(type), name, fields, 
properties));
+            indexType = getClickHouseIndexType(type);
+            fields = parseIndexFields(expression);
           } catch (IllegalArgumentException e) {
             LOG.warn(
-                "Skip unsupported data skipping index {} for {}.{} with 
expression {}",
+                "Skip unsupported data skipping index {} for {}.{} with type 
{} "
+                    + "(parameter metadata={}) and expression {}",
                 name,
                 databaseName,
                 tableName,
-                expression);
+                type,
+                parameterSource,
+                expression,
+                e);
+            continue;
+          }
+          if (ArrayUtils.isEmpty(fields)) {
+            continue;
+          }
+
+          // Only include granularity in properties when it differs from the 
default,
+          // so that indexes created without explicit granularity have empty 
properties
+          // and match the original creation state (avoids false index-change 
diffs).
+          Map<String, String> properties = new HashMap<>();
+          if (granularity != DEFAULT_INDEX_GRANULARITY) {
+            properties.put(GRANULARITY, String.valueOf(granularity));
           }
+          Map<String, String> bloomFilterProperties;
+          try {
+            bloomFilterProperties =
+                parseBloomFilterPropertiesForQuery(
+                    indexType, parameterSource, name, !includesTypeFull);
+          } catch (IllegalArgumentException e) {
+            throw new IllegalArgumentException(
+                "Failed to load data skipping index '%s' from %s.%s with %s 
'%s'"
+                    .formatted(name, databaseName, tableName, 
parameterSourceName, parameterSource),
+                e);
+          }
+          if (!includesTypeFull
+              && isParameterizedBloomFilterIndex(indexType)
+              && bloomFilterProperties.isEmpty()) {
+            LOG.warn(
+                "Legacy ClickHouse metadata does not expose bloom-filter 
parameters for "
+                    + "{} index '{}' on {}.{}; loaded Index.properties() is 
incomplete",
+                type,
+                name,
+                databaseName,
+                tableName);
+          }
+          properties.putAll(bloomFilterProperties);
+          secondaryIndexes.add(Indexes.of(indexType, name, fields, 
properties));
         }
       }
     }
@@ -1655,6 +1685,109 @@ public class ClickHouseTableOperations extends 
JdbcTableOperations {
     return secondaryIndexes;
   }
 
+  private static boolean isMissingTypeFullColumn(SQLException exception) {
+    for (SQLException current = exception; current != null; current = 
current.getNextException()) {
+      for (Throwable cause = current; cause != null; cause = cause.getCause()) 
{
+        String message = StringUtils.lowerCase(cause.getMessage());
+        if (message != null
+            && message.contains("type_full")
+            && (message.contains("unknown")
+                || message.contains("missing")
+                || message.contains("column"))) {
+          return true;
+        }
+      }
+    }
+    return false;
+  }
+
+  /**
+   * Parses the positional parameters returned by ClickHouse in {@code 
type_full} for the two
+   * parameterized bloom-filter data skipping indexes.
+   *
+   * @param indexType the mapped Gravitino index type
+   * @param typeFull the complete ClickHouse index type expression
+   * @param indexName the index name for validation messages
+   * @return the index properties, or an empty map for non-parameterized index 
types
+   * @throws IllegalArgumentException if a supported index has malformed or 
invalid parameters
+   */
+  @VisibleForTesting
+  static Map<String, String> parseBloomFilterProperties(
+      Index.IndexType indexType, String typeFull, String indexName) {
+    if (!isParameterizedBloomFilterIndex(indexType)) {
+      return Collections.emptyMap();
+    }
+
+    String expectedType =
+        indexType == Index.IndexType.DATA_SKIPPING_NGRAMBFV1
+            ? DATA_SKIPPING_NGRAMBFV1
+            : DATA_SKIPPING_TOKENBFV1;
+    String normalizedTypeFull = StringUtils.trimToEmpty(typeFull);
+    int paramsStart = normalizedTypeFull.indexOf('(');
+    int paramsEnd = normalizedTypeFull.lastIndexOf(')');
+    Preconditions.checkArgument(
+        paramsStart > 0 && paramsEnd == normalizedTypeFull.length() - 1,
+        "Invalid type_full '%s' for %s index '%s'",
+        typeFull,
+        expectedType,
+        indexName);
+    Preconditions.checkArgument(
+        StringUtils.equalsIgnoreCase(
+            expectedType, normalizedTypeFull.substring(0, paramsStart).trim()),
+        "type_full '%s' does not match %s index '%s'",
+        typeFull,
+        expectedType,
+        indexName);
+
+    String[] params = normalizedTypeFull.substring(paramsStart + 1, 
paramsEnd).split(",", -1);
+    int expectedParamCount = indexType == 
Index.IndexType.DATA_SKIPPING_NGRAMBFV1 ? 4 : 3;
+    Preconditions.checkArgument(
+        params.length == expectedParamCount,
+        "Expected %s parameters for %s index '%s', but got %s in '%s'",
+        expectedParamCount,
+        expectedType,
+        indexName,
+        params.length,
+        typeFull);
+
+    Map<String, String> properties = new HashMap<>();
+    int paramIndex = 0;
+    if (indexType == Index.IndexType.DATA_SKIPPING_NGRAMBFV1) {
+      properties.put(
+          NGRAM_SIZE,
+          requireIntWithMin(params[paramIndex++], NGRAM_SIZE, expectedType, 
indexName, 1));
+    }
+    properties.put(
+        BLOOM_FILTER_SIZE,
+        requireIntWithMin(params[paramIndex++], BLOOM_FILTER_SIZE, 
expectedType, indexName, 1));
+    properties.put(
+        HASH_FUNCTIONS,
+        requireIntWithMin(params[paramIndex++], HASH_FUNCTIONS, expectedType, 
indexName, 1));
+    properties.put(
+        RANDOM_SEED,
+        requireIntWithMin(params[paramIndex], RANDOM_SEED, expectedType, 
indexName, 0));
+    return Map.copyOf(properties);
+  }
+
+  private static Map<String, String> parseBloomFilterPropertiesForQuery(
+      Index.IndexType indexType,
+      String parameterSource,
+      String indexName,
+      boolean allowBareLegacyType) {
+    if (!isParameterizedBloomFilterIndex(indexType)) {
+      return Collections.emptyMap();
+    }
+    if (allowBareLegacyType && !StringUtils.contains(parameterSource, "(")) {
+      return Collections.emptyMap();
+    }
+    return parseBloomFilterProperties(indexType, parameterSource, indexName);
+  }
+
+  private static boolean isParameterizedBloomFilterIndex(Index.IndexType 
indexType) {
+    return indexType == Index.IndexType.DATA_SKIPPING_NGRAMBFV1
+        || indexType == Index.IndexType.DATA_SKIPPING_TOKENBFV1;
+  }
+
   /**
    * Maps a ClickHouse data skipping index type string to the corresponding 
Gravitino {@link
    * Index.IndexType}. Returns {@code DATA_SKIPPING_MINMAX} for blank/null 
input (ClickHouse
@@ -1679,16 +1812,148 @@ public class ClickHouseTableOperations extends 
JdbcTableOperations {
         return Index.IndexType.DATA_SKIPPING_BLOOM_FILTER;
       case DATA_SKIPPING_SET:
         return Index.IndexType.DATA_SKIPPING_SET;
+      case DATA_SKIPPING_NGRAMBFV1:
+        return Index.IndexType.DATA_SKIPPING_NGRAMBFV1;
+      case DATA_SKIPPING_TOKENBFV1:
+        return Index.IndexType.DATA_SKIPPING_TOKENBFV1;
       default:
-        // ClickHouse may return "set(N)" with parameter in some versions;
-        // match on prefix to handle both "set" and "set(N)" formats.
+        // ClickHouse may return type with parameters in some versions (e.g. 
"set(0)",
+        // "ngrambf_v1(3, 512, 3, 0)"). Match on prefix to handle both bare and
+        // parameterized formats.
         if (rawType.startsWith(DATA_SKIPPING_SET + "(")) {
           return Index.IndexType.DATA_SKIPPING_SET;
         }
+        if (rawType.startsWith(DATA_SKIPPING_NGRAMBFV1 + "(")) {
+          return Index.IndexType.DATA_SKIPPING_NGRAMBFV1;
+        }
+        if (rawType.startsWith(DATA_SKIPPING_TOKENBFV1 + "(")) {
+          return Index.IndexType.DATA_SKIPPING_TOKENBFV1;
+        }
         throw new IllegalArgumentException("Unsupported data skipping index 
type: " + rawType);
     }
   }
 
+  /**
+   * Validates that a property value is an integer with a given minimum bound, 
returning it as a
+   * string for DDL interpolation. Unlike {@link #resolveIntProperty(Map, 
String, int, int)}, this
+   * method treats the value as required — a missing or blank value throws 
{@link
+   * IllegalArgumentException} rather than returning a default. Used for 
bloom-filter parameters
+   * (e.g. {@code bloom_filter_size}, {@code ngram_size} require &ge; 1; 
{@code random_seed}
+   * requires &ge; 0).
+   *
+   * @param value the raw string value from the properties map
+   * @param paramName the parameter name for error messages
+   * @param indexType the index type name (e.g. "ngrambf_v1")
+   * @param indexName the index name for error messages
+   * @param minValue the minimum allowed value (inclusive)
+   * @return the validated value as a string
+   * @throws IllegalArgumentException if the value is null, blank, not an 
integer, or below minValue
+   */
+  private static String requireIntWithMin(
+      String value, String paramName, String indexType, String indexName, int 
minValue) {
+    Preconditions.checkArgument(
+        value != null && !value.isBlank(),
+        "%s is required for %s index '%s'",
+        paramName,
+        indexType,
+        indexName);
+    try {
+      int intVal = Integer.parseInt(value.strip());
+      Preconditions.checkArgument(
+          intVal >= minValue,
+          "%s must be >= %s for %s index '%s', but got '%s'",
+          paramName,
+          minValue,
+          indexType,
+          indexName,
+          value);
+      return String.valueOf(intVal);
+    } catch (NumberFormatException e) {
+      throw new IllegalArgumentException(
+          String.format(
+              "%s must be a valid integer for %s index '%s', but got '%s'",
+              paramName, indexType, indexName, value),
+          e);
+    }
+  }
+
+  /**
+   * Builds the full type clause for bloom-filter-based data skipping indexes, 
combining the type
+   * name (e.g. "ngrambf_v1") with the validated parameter clause. Extracted 
to eliminate
+   * duplication between the CREATE TABLE path ({@link #appendIndexesSql}) and 
the ALTER TABLE ADD
+   * INDEX path ({@link #addIndexDefinition}).
+   *
+   * @param props the index properties map
+   * @param indexType the index type name (one of the {@code DATA_SKIPPING_*} 
constants)
+   * @param indexName the index name, used in error messages
+   * @return the full type clause, e.g. "ngrambf_v1(3, 512, 3, 0)"
+   */
+  private static String buildBloomFilterTypeClause(
+      Map<String, String> props, String indexType, String indexName) {
+    return indexType + resolveBloomFilterParams(props, indexType, indexName);
+  }
+
+  /**
+   * Resolves bloom-filter-based data skipping index parameters from index 
properties for {@code
+   * ngrambf_v1} and {@code tokenbf_v1} index types. Used by both CREATE TABLE 
(via {@link
+   * Index#properties()}) and ALTER TABLE ADD INDEX (via {@link
+   * TableChange.AddIndex#getProperties()}).
+   *
+   * @param props the index properties map
+   * @param indexType "ngrambf_v1" or "tokenbf_v1" (determines whether 
ngram_size is required)
+   * @param indexName the index name, used in error messages
+   * @return the DDL parameter clause, e.g. "(3, 512, 3, 0)" for ngrambf_v1
+   * @throws IllegalArgumentException if any required parameter is missing or 
invalid
+   */
+  private static String resolveBloomFilterParams(
+      Map<String, String> props, String indexType, String indexName) {
+    String size =
+        requireIntWithMin(
+            props.get(BLOOM_FILTER_SIZE), "bloom_filter_size", indexType, 
indexName, 1);
+    String hashFuncs =
+        requireIntWithMin(props.get(HASH_FUNCTIONS), "hash_functions", 
indexType, indexName, 1);
+    String seed = requireIntWithMin(props.get(RANDOM_SEED), "random_seed", 
indexType, indexName, 0);
+
+    if (DATA_SKIPPING_NGRAMBFV1.equals(indexType)) {
+      String ngramSize =
+          requireIntWithMin(props.get(NGRAM_SIZE), "ngram_size", indexType, 
indexName, 1);
+      return String.format("(%s, %s, %s, %s)", ngramSize, size, hashFuncs, 
seed);
+    }
+    return String.format("(%s, %s, %s)", size, hashFuncs, seed);
+  }
+
+  private String buildDataSkippingIndexDdl(
+      String indexName,
+      String fieldStr,
+      Index.IndexType indexType,
+      Map<String, String> properties) {
+    return buildDataSkippingIndexDdl(
+        indexName,
+        fieldStr,
+        resolveDataSkippingIndexTypeClause(indexType, properties, indexName),
+        resolveGranularity(properties, 1));
+  }
+
+  private String resolveDataSkippingIndexTypeClause(
+      Index.IndexType indexType, Map<String, String> properties, String 
indexName) {
+    switch (indexType) {
+      case DATA_SKIPPING_MINMAX:
+        return DATA_SKIPPING_MINMAX_VALUE;
+      case DATA_SKIPPING_BLOOM_FILTER:
+        return DATA_SKIPPING_BLOOM_FILTER;
+      case DATA_SKIPPING_SET:
+        // SET index defaults to unlimited distinct values and supports an 
optional upper bound.
+        return "set(" + resolveSetMaxValues(properties) + ")";
+      case DATA_SKIPPING_NGRAMBFV1:
+        return buildBloomFilterTypeClause(properties, DATA_SKIPPING_NGRAMBFV1, 
indexName);
+      case DATA_SKIPPING_TOKENBFV1:
+        return buildBloomFilterTypeClause(properties, DATA_SKIPPING_TOKENBFV1, 
indexName);
+      default:
+        throw new IllegalArgumentException(
+            "Gravitino ClickHouse doesn't support index : " + indexType);
+    }
+  }
+
   private String buildDataSkippingIndexDdl(
       String indexName, String fieldStr, String typeName, int granularity) {
     Preconditions.checkArgument(
diff --git 
a/catalogs-contrib/catalog-jdbc-clickhouse/src/test/java/org/apache/gravitino/catalog/clickhouse/integration/test/CatalogClickHouseClusterIT.java
 
b/catalogs-contrib/catalog-jdbc-clickhouse/src/test/java/org/apache/gravitino/catalog/clickhouse/integration/test/CatalogClickHouseClusterIT.java
index 4ac96e8f31..0d2ba7bb4c 100644
--- 
a/catalogs-contrib/catalog-jdbc-clickhouse/src/test/java/org/apache/gravitino/catalog/clickhouse/integration/test/CatalogClickHouseClusterIT.java
+++ 
b/catalogs-contrib/catalog-jdbc-clickhouse/src/test/java/org/apache/gravitino/catalog/clickhouse/integration/test/CatalogClickHouseClusterIT.java
@@ -545,6 +545,96 @@ public class CatalogClickHouseClusterIT extends BaseIT {
         autoIncrementFalseException.getMessage().contains("auto increment is 
not supported"));
   }
 
+  @Test
+  public void testNgrambfAndTokenbfIndexesOnCluster() {
+    String tableName = GravitinoITUtils.genRandomName("ck_cluster_skip_idx");
+    NameIdentifier tableIdentifier = NameIdentifier.of(schemaName, tableName);
+    TableCatalog tableCatalog = catalog.asTableCatalog();
+    Map<String, String> ngramProperties =
+        Map.of(
+            "ngram_size", "3",
+            "bloom_filter_size", "512",
+            "hash_functions", "3",
+            "random_seed", "0");
+    Map<String, String> tokenProperties =
+        Map.of(
+            "bloom_filter_size", "256",
+            "hash_functions", "2",
+            "random_seed", "0",
+            "granularity", "4");
+
+    tableCatalog.createTable(
+        tableIdentifier,
+        createColumns(),
+        tableComment,
+        clusterMergeTreeProperties(),
+        Transforms.EMPTY_TRANSFORM,
+        Distributions.NONE,
+        getSortOrders("col_3"),
+        new Index[] {
+          Indexes.of(
+              Index.IndexType.DATA_SKIPPING_NGRAMBFV1,
+              "idx_ngram",
+              new String[][] {{"col_3"}},
+              ngramProperties),
+          Indexes.of(
+              Index.IndexType.DATA_SKIPPING_TOKENBFV1,
+              "idx_token",
+              new String[][] {{"col_3"}},
+              tokenProperties)
+        });
+
+    Table loaded = tableCatalog.loadTable(tableIdentifier);
+    assertIndexMetadata(
+        loaded.index(), "idx_ngram", Index.IndexType.DATA_SKIPPING_NGRAMBFV1, 
ngramProperties);
+    assertIndexMetadata(
+        loaded.index(), "idx_token", Index.IndexType.DATA_SKIPPING_TOKENBFV1, 
tokenProperties);
+
+    tableCatalog.alterTable(
+        tableIdentifier,
+        TableChange.addIndex(
+            Index.IndexType.DATA_SKIPPING_NGRAMBFV1,
+            "idx_ngram_alter",
+            new String[][] {{"col_3"}},
+            ngramProperties),
+        TableChange.addIndex(
+            Index.IndexType.DATA_SKIPPING_TOKENBFV1,
+            "idx_token_alter",
+            new String[][] {{"col_3"}},
+            tokenProperties));
+
+    Table altered = tableCatalog.loadTable(tableIdentifier);
+    assertIndexMetadata(
+        altered.index(),
+        "idx_ngram_alter",
+        Index.IndexType.DATA_SKIPPING_NGRAMBFV1,
+        ngramProperties);
+    assertIndexMetadata(
+        altered.index(),
+        "idx_token_alter",
+        Index.IndexType.DATA_SKIPPING_TOKENBFV1,
+        tokenProperties);
+
+    tableCatalog.alterTable(
+        tableIdentifier,
+        TableChange.deleteIndex("idx_ngram", false),
+        TableChange.deleteIndex("idx_token", false),
+        TableChange.deleteIndex("idx_ngram_alter", false),
+        TableChange.deleteIndex("idx_token_alter", false));
+  }
+
+  private void assertIndexMetadata(
+      Index[] indexes, String name, Index.IndexType type, Map<String, String> 
properties) {
+    Index index =
+        Arrays.stream(indexes)
+            .filter(candidate -> Objects.equals(name, candidate.name()))
+            .findFirst()
+            .orElseThrow(() -> new AssertionError("Missing index " + name));
+    Assertions.assertEquals(type, index.type());
+    Assertions.assertTrue(Arrays.deepEquals(new String[][] {{"col_3"}}, 
index.fieldNames()));
+    Assertions.assertEquals(properties, index.properties());
+  }
+
   @Test
   public void testDropTableOnCluster() {
     String dropTableName = 
GravitinoITUtils.genRandomName("ck_cluster_drop_tbl");
diff --git 
a/catalogs-contrib/catalog-jdbc-clickhouse/src/test/java/org/apache/gravitino/catalog/clickhouse/integration/test/CatalogClickHouseIT.java
 
b/catalogs-contrib/catalog-jdbc-clickhouse/src/test/java/org/apache/gravitino/catalog/clickhouse/integration/test/CatalogClickHouseIT.java
index fcc9c0bf24..72731536f4 100644
--- 
a/catalogs-contrib/catalog-jdbc-clickhouse/src/test/java/org/apache/gravitino/catalog/clickhouse/integration/test/CatalogClickHouseIT.java
+++ 
b/catalogs-contrib/catalog-jdbc-clickhouse/src/test/java/org/apache/gravitino/catalog/clickhouse/integration/test/CatalogClickHouseIT.java
@@ -3219,4 +3219,146 @@ public class CatalogClickHouseIT extends BaseIT {
         
normalizedCreateSql.contains("Enum16('low'=100,'medium'=200,'high'=300)"),
         "SHOW CREATE TABLE should contain Enum16 definition: " + createSql);
   }
+
+  @Test
+  @Tag("gravitino-docker-test")
+  void testCreateAndLoadTableWithNgrambfAndTokenbfIndexes() {
+    String tableName = GravitinoITUtils.genRandomName("ch_ngram_tokenbf_idx_");
+    NameIdentifier tableIdentifier = NameIdentifier.of(schemaName, tableName);
+    Map<String, String> properties = createProperties();
+    Column[] columns =
+        new Column[] {
+          Column.of(
+              CLICKHOUSE_COL_NAME1,
+              Types.IntegerType.get(),
+              "col_1_comment",
+              false,
+              false,
+              DEFAULT_VALUE_NOT_SET),
+          Column.of(
+              CLICKHOUSE_COL_NAME3,
+              Types.StringType.get(),
+              "col_3_comment",
+              false,
+              false,
+              DEFAULT_VALUE_NOT_SET),
+        };
+    SortOrder[] sortOrders = getSortOrders(CLICKHOUSE_COL_NAME1);
+    TableCatalog tableCatalog = catalog.asTableCatalog();
+    Map<String, String> ngramProperties =
+        Map.of(
+            "ngram_size", "3",
+            "bloom_filter_size", "512",
+            "hash_functions", "3",
+            "random_seed", "0");
+    Map<String, String> tokenProperties =
+        Map.of(
+            "bloom_filter_size", "256",
+            "hash_functions", "2",
+            "random_seed", "0",
+            "granularity", "4");
+
+    // Create table with ngrambf_v1 index
+    Index[] indexes =
+        new Index[] {
+          Indexes.of(
+              Index.IndexType.DATA_SKIPPING_NGRAMBFV1,
+              "idx_ngram",
+              new String[][] {{CLICKHOUSE_COL_NAME3}},
+              ngramProperties),
+          Indexes.of(
+              Index.IndexType.DATA_SKIPPING_TOKENBFV1,
+              "idx_token",
+              new String[][] {{CLICKHOUSE_COL_NAME3}},
+              tokenProperties),
+        };
+    tableCatalog.createTable(
+        tableIdentifier,
+        columns,
+        table_comment,
+        properties,
+        Transforms.EMPTY_TRANSFORM,
+        Distributions.NONE,
+        sortOrders,
+        indexes);
+
+    // Load and verify round-trip
+    Table loaded = tableCatalog.loadTable(tableIdentifier);
+    Index[] loadedIndexes = loaded.index();
+    Assertions.assertNotNull(loadedIndexes);
+
+    Index loadedNgram =
+        Arrays.stream(loadedIndexes)
+            .filter(idx -> Objects.equals(idx.name(), "idx_ngram"))
+            .findFirst()
+            .orElseThrow();
+    Assertions.assertEquals(Index.IndexType.DATA_SKIPPING_NGRAMBFV1, 
loadedNgram.type());
+    Assertions.assertArrayEquals(new String[][] {{CLICKHOUSE_COL_NAME3}}, 
loadedNgram.fieldNames());
+    Assertions.assertEquals(
+        Map.of(
+            "ngram_size", "3",
+            "bloom_filter_size", "512",
+            "hash_functions", "3",
+            "random_seed", "0"),
+        loadedNgram.properties());
+
+    Index loadedToken =
+        Arrays.stream(loadedIndexes)
+            .filter(idx -> Objects.equals(idx.name(), "idx_token"))
+            .findFirst()
+            .orElseThrow();
+    Assertions.assertEquals(Index.IndexType.DATA_SKIPPING_TOKENBFV1, 
loadedToken.type());
+    Assertions.assertArrayEquals(new String[][] {{CLICKHOUSE_COL_NAME3}}, 
loadedToken.fieldNames());
+    Assertions.assertEquals(tokenProperties, loadedToken.properties());
+
+    // Verify that both new index types also work through ALTER TABLE ADD 
INDEX.
+    tableCatalog.alterTable(
+        tableIdentifier,
+        TableChange.addIndex(
+            Index.IndexType.DATA_SKIPPING_NGRAMBFV1,
+            "idx_ngram_alter",
+            new String[][] {{CLICKHOUSE_COL_NAME3}},
+            ngramProperties));
+    tableCatalog.alterTable(
+        tableIdentifier,
+        TableChange.addIndex(
+            Index.IndexType.DATA_SKIPPING_TOKENBFV1,
+            "idx_token_alter",
+            new String[][] {{CLICKHOUSE_COL_NAME3}},
+            tokenProperties));
+
+    Table altered = tableCatalog.loadTable(tableIdentifier);
+    Index alteredNgram =
+        Arrays.stream(altered.index())
+            .filter(idx -> Objects.equals(idx.name(), "idx_ngram_alter"))
+            .findFirst()
+            .orElseThrow();
+    Assertions.assertEquals(Index.IndexType.DATA_SKIPPING_NGRAMBFV1, 
alteredNgram.type());
+    Assertions.assertEquals(
+        Map.of(
+            "ngram_size", "3",
+            "bloom_filter_size", "512",
+            "hash_functions", "3",
+            "random_seed", "0"),
+        alteredNgram.properties());
+
+    Index alteredToken =
+        Arrays.stream(altered.index())
+            .filter(idx -> Objects.equals(idx.name(), "idx_token_alter"))
+            .findFirst()
+            .orElseThrow();
+    Assertions.assertEquals(Index.IndexType.DATA_SKIPPING_TOKENBFV1, 
alteredToken.type());
+    Assertions.assertEquals(tokenProperties, alteredToken.properties());
+
+    String createSql =
+        clickhouseService.executeQueryForResult(
+            String.format("SHOW CREATE TABLE `%s`.`%s`", schemaName, 
tableName));
+    String normalizedCreateSql = createSql.replaceAll("\\s+", "");
+    Assertions.assertTrue(
+        normalizedCreateSql.contains("ngrambf_v1(3,512,3,0)"),
+        "SHOW CREATE TABLE should retain ngrambf_v1 parameters: " + createSql);
+    Assertions.assertTrue(
+        normalizedCreateSql.contains("tokenbf_v1(256,2,0)"),
+        "SHOW CREATE TABLE should retain tokenbf_v1 parameters: " + createSql);
+  }
 }
diff --git 
a/catalogs-contrib/catalog-jdbc-clickhouse/src/test/java/org/apache/gravitino/catalog/clickhouse/operations/TestClickHouseTableOperations.java
 
b/catalogs-contrib/catalog-jdbc-clickhouse/src/test/java/org/apache/gravitino/catalog/clickhouse/operations/TestClickHouseTableOperations.java
index 5f3fdbd8ea..9ff657b9c3 100644
--- 
a/catalogs-contrib/catalog-jdbc-clickhouse/src/test/java/org/apache/gravitino/catalog/clickhouse/operations/TestClickHouseTableOperations.java
+++ 
b/catalogs-contrib/catalog-jdbc-clickhouse/src/test/java/org/apache/gravitino/catalog/clickhouse/operations/TestClickHouseTableOperations.java
@@ -1736,15 +1736,543 @@ public class TestClickHouseTableOperations extends 
TestClickHouse {
     Assertions.assertEquals(IndexType.DATA_SKIPPING_SET, 
ops.getClickHouseIndexType("set(0)"));
     Assertions.assertEquals(IndexType.DATA_SKIPPING_SET, 
ops.getClickHouseIndexType("set(100)"));
 
+    // ngrambf_v1 and tokenbf_v1 bloom filter data skipping indexes
+    Assertions.assertEquals(
+        IndexType.DATA_SKIPPING_NGRAMBFV1, 
ops.getClickHouseIndexType("ngrambf_v1"));
+    Assertions.assertEquals(
+        IndexType.DATA_SKIPPING_TOKENBFV1, 
ops.getClickHouseIndexType("tokenbf_v1"));
+
     // Blank/null defaults to MINMAX
     Assertions.assertEquals(IndexType.DATA_SKIPPING_MINMAX, 
ops.getClickHouseIndexType(""));
     Assertions.assertEquals(IndexType.DATA_SKIPPING_MINMAX, 
ops.getClickHouseIndexType(null));
 
+    // Parameterized formats (C2 prefix matching)
+    Assertions.assertEquals(
+        IndexType.DATA_SKIPPING_NGRAMBFV1, 
ops.getClickHouseIndexType("ngrambf_v1(3, 512, 3, 0)"));
+    Assertions.assertEquals(
+        IndexType.DATA_SKIPPING_TOKENBFV1, 
ops.getClickHouseIndexType("tokenbf_v1(256, 2, 0)"));
+
     // Unsupported type
     Assertions.assertThrows(
         IllegalArgumentException.class, () -> 
ops.getClickHouseIndexType("unknown_type"));
   }
 
+  @Test
+  public void testParseBloomFilterProperties() {
+    Assertions.assertEquals(
+        Map.of(
+            "ngram_size", "3",
+            "bloom_filter_size", "512",
+            "hash_functions", "3",
+            "random_seed", "0"),
+        ClickHouseTableOperations.parseBloomFilterProperties(
+            IndexType.DATA_SKIPPING_NGRAMBFV1, "ngrambf_v1(3, 512, 3, 0)", 
"idx_ngram"));
+    Assertions.assertEquals(
+        Map.of(
+            "bloom_filter_size", "256",
+            "hash_functions", "2",
+            "random_seed", "1"),
+        ClickHouseTableOperations.parseBloomFilterProperties(
+            IndexType.DATA_SKIPPING_TOKENBFV1, " tokenbf_v1(256,2,1) ", 
"idx_token"));
+
+    Assertions.assertThrows(
+        IllegalArgumentException.class,
+        () ->
+            ClickHouseTableOperations.parseBloomFilterProperties(
+                IndexType.DATA_SKIPPING_NGRAMBFV1, "ngrambf_v1(3,512,3)", 
"idx_bad"));
+    Assertions.assertThrows(
+        IllegalArgumentException.class,
+        () ->
+            ClickHouseTableOperations.parseBloomFilterProperties(
+                IndexType.DATA_SKIPPING_TOKENBFV1, "tokenbf_v1(256,2,-1)", 
"idx_bad"));
+  }
+
+  @Test
+  public void testCreateTableWithNgrambfAndTokenbfIndexes() {
+    TestableClickHouseTableOperations ops = new 
TestableClickHouseTableOperations();
+    ops.initialize(
+        null,
+        new ClickHouseExceptionConverter(),
+        new ClickHouseTypeConverter(),
+        new ClickHouseColumnDefaultValueConverter(),
+        new HashMap<>());
+
+    JdbcColumn[] cols =
+        new JdbcColumn[] {
+          JdbcColumn.builder()
+              .withName("c1")
+              .withType(Types.LongType.get())
+              .withNullable(true)
+              .build(),
+          JdbcColumn.builder()
+              .withName("c2")
+              .withType(Types.StringType.get())
+              .withNullable(true)
+              .build(),
+        };
+
+    // ngrambf_v1 with full parameters
+    Index[] indexes1 =
+        new Index[] {
+          Indexes.of(
+              IndexType.DATA_SKIPPING_NGRAMBFV1,
+              "idx_ngram",
+              new String[][] {{"c2"}},
+              Map.of(
+                  "ngram_size",
+                  "3",
+                  "bloom_filter_size",
+                  "512",
+                  "hash_functions",
+                  "3",
+                  "random_seed",
+                  "1")),
+        };
+
+    String sql1 =
+        ops.buildCreateSql(
+            "t_ngram",
+            cols,
+            "comment",
+            new HashMap<>(),
+            new Transform[0],
+            Distributions.NONE,
+            indexes1,
+            ClickHouseUtils.getSortOrders("c1"));
+
+    Assertions.assertTrue(
+        sql1.contains("INDEX `idx_ngram` `c2` TYPE ngrambf_v1(3, 512, 3, 1) 
GRANULARITY 1"),
+        "DDL should contain ngrambf_v1 with parameters: " + sql1);
+
+    // tokenbf_v1 with full parameters
+    Index[] indexes2 =
+        new Index[] {
+          Indexes.of(
+              IndexType.DATA_SKIPPING_TOKENBFV1,
+              "idx_token",
+              new String[][] {{"c2"}},
+              Map.of("bloom_filter_size", "256", "hash_functions", "2", 
"random_seed", "0")),
+        };
+
+    String sql2 =
+        ops.buildCreateSql(
+            "t_token",
+            cols,
+            "comment",
+            new HashMap<>(),
+            new Transform[0],
+            Distributions.NONE,
+            indexes2,
+            ClickHouseUtils.getSortOrders("c1"));
+
+    Assertions.assertTrue(
+        sql2.contains("INDEX `idx_token` `c2` TYPE tokenbf_v1(256, 2, 0) 
GRANULARITY 1"),
+        "DDL should contain tokenbf_v1 with parameters: " + sql2);
+
+    // ngrambf_v1 with custom GRANULARITY from properties
+    Index[] indexes3 =
+        new Index[] {
+          Indexes.of(
+              IndexType.DATA_SKIPPING_NGRAMBFV1,
+              "idx_ngram_g",
+              new String[][] {{"c2"}},
+              Map.of(
+                  "ngram_size",
+                  "4",
+                  "bloom_filter_size",
+                  "1024",
+                  "hash_functions",
+                  "3",
+                  "random_seed",
+                  "1",
+                  "granularity",
+                  "8")),
+        };
+
+    String sql3 =
+        ops.buildCreateSql(
+            "t_ngram_g",
+            cols,
+            "comment",
+            new HashMap<>(),
+            new Transform[0],
+            Distributions.NONE,
+            indexes3,
+            ClickHouseUtils.getSortOrders("c1"));
+
+    Assertions.assertTrue(
+        sql3.contains("INDEX `idx_ngram_g` `c2` TYPE ngrambf_v1(4, 1024, 3, 1) 
GRANULARITY 8"),
+        "DDL should contain custom GRANULARITY: " + sql3);
+  }
+
+  @Test
+  public void testNgrambfTokenbfMissingParameters() {
+    TestableClickHouseTableOperations ops = new 
TestableClickHouseTableOperations();
+    ops.initialize(
+        null,
+        new ClickHouseExceptionConverter(),
+        new ClickHouseTypeConverter(),
+        new ClickHouseColumnDefaultValueConverter(),
+        new HashMap<>());
+
+    JdbcColumn[] cols =
+        new JdbcColumn[] {
+          JdbcColumn.builder()
+              .withName("c1")
+              .withType(Types.LongType.get())
+              .withNullable(true)
+              .build(),
+          JdbcColumn.builder()
+              .withName("c2")
+              .withType(Types.StringType.get())
+              .withNullable(true)
+              .build(),
+        };
+
+    // ngrambf_v1 without ngram_size — should throw
+    Index[] noNgramSize =
+        new Index[] {
+          Indexes.of(
+              IndexType.DATA_SKIPPING_NGRAMBFV1,
+              "idx_bad",
+              new String[][] {{"c2"}},
+              Map.of("bloom_filter_size", "512", "hash_functions", "3", 
"random_seed", "1")),
+        };
+    Assertions.assertThrows(
+        IllegalArgumentException.class,
+        () ->
+            ops.buildCreateSql(
+                "t_bad",
+                cols,
+                "comment",
+                new HashMap<>(),
+                new Transform[0],
+                Distributions.NONE,
+                noNgramSize,
+                ClickHouseUtils.getSortOrders("c1")));
+
+    // tokenbf_v1 without bloom_filter_size — should throw
+    Index[] noBloomSize =
+        new Index[] {
+          Indexes.of(
+              IndexType.DATA_SKIPPING_TOKENBFV1,
+              "idx_bad2",
+              new String[][] {{"c2"}},
+              Map.of("hash_functions", "3", "random_seed", "1")),
+        };
+    Assertions.assertThrows(
+        IllegalArgumentException.class,
+        () ->
+            ops.buildCreateSql(
+                "t_bad2",
+                cols,
+                "comment",
+                new HashMap<>(),
+                new Transform[0],
+                Distributions.NONE,
+                noBloomSize,
+                ClickHouseUtils.getSortOrders("c1")));
+
+    // ngrambf_v1 without hash_functions — should throw
+    Index[] noHashFuncs =
+        new Index[] {
+          Indexes.of(
+              IndexType.DATA_SKIPPING_NGRAMBFV1,
+              "idx_bad3",
+              new String[][] {{"c2"}},
+              Map.of(
+                  "ngram_size", "3",
+                  "bloom_filter_size", "512",
+                  "random_seed", "1")),
+        };
+    Assertions.assertThrows(
+        IllegalArgumentException.class,
+        () ->
+            ops.buildCreateSql(
+                "t_bad3",
+                cols,
+                "comment",
+                new HashMap<>(),
+                new Transform[0],
+                Distributions.NONE,
+                noHashFuncs,
+                ClickHouseUtils.getSortOrders("c1")));
+
+    // tokenbf_v1 without hash_functions — should throw
+    Index[] tokenbfNoHash =
+        new Index[] {
+          Indexes.of(
+              IndexType.DATA_SKIPPING_TOKENBFV1,
+              "idx_bad4",
+              new String[][] {{"c2"}},
+              Map.of("bloom_filter_size", "256", "random_seed", "1")),
+        };
+    Assertions.assertThrows(
+        IllegalArgumentException.class,
+        () ->
+            ops.buildCreateSql(
+                "t_bad4",
+                cols,
+                "comment",
+                new HashMap<>(),
+                new Transform[0],
+                Distributions.NONE,
+                tokenbfNoHash,
+                ClickHouseUtils.getSortOrders("c1")));
+
+    // tokenbf_v1 without random_seed — should throw
+    Index[] noSeed =
+        new Index[] {
+          Indexes.of(
+              IndexType.DATA_SKIPPING_TOKENBFV1,
+              "idx_bad5",
+              new String[][] {{"c2"}},
+              Map.of("bloom_filter_size", "256", "hash_functions", "2")),
+        };
+    Assertions.assertThrows(
+        IllegalArgumentException.class,
+        () ->
+            ops.buildCreateSql(
+                "t_bad5",
+                cols,
+                "comment",
+                new HashMap<>(),
+                new Transform[0],
+                Distributions.NONE,
+                noSeed,
+                ClickHouseUtils.getSortOrders("c1")));
+  }
+
+  @Test
+  public void testNgrambfTokenbfInvalidParameterValues() {
+    TestableClickHouseTableOperations ops = new 
TestableClickHouseTableOperations();
+    ops.initialize(
+        null,
+        new ClickHouseExceptionConverter(),
+        new ClickHouseTypeConverter(),
+        new ClickHouseColumnDefaultValueConverter(),
+        new HashMap<>());
+
+    JdbcColumn[] cols =
+        new JdbcColumn[] {
+          JdbcColumn.builder()
+              .withName("c1")
+              .withType(Types.LongType.get())
+              .withNullable(true)
+              .build(),
+          JdbcColumn.builder()
+              .withName("c2")
+              .withType(Types.StringType.get())
+              .withNullable(true)
+              .build(),
+        };
+
+    // Non-numeric bloom_filter_size should throw
+    Index[] nonNumericSize =
+        new Index[] {
+          Indexes.of(
+              IndexType.DATA_SKIPPING_TOKENBFV1,
+              "idx_bad",
+              new String[][] {{"c2"}},
+              Map.of(
+                  "bloom_filter_size", "abc",
+                  "hash_functions", "3",
+                  "random_seed", "1")),
+        };
+    Assertions.assertThrows(
+        IllegalArgumentException.class,
+        () ->
+            ops.buildCreateSql(
+                "t_bad",
+                cols,
+                "comment",
+                new HashMap<>(),
+                new Transform[0],
+                Distributions.NONE,
+                nonNumericSize,
+                ClickHouseUtils.getSortOrders("c1")));
+
+    // Negative bloom_filter_size should throw
+    Index[] negativeSize =
+        new Index[] {
+          Indexes.of(
+              IndexType.DATA_SKIPPING_TOKENBFV1,
+              "idx_bad2",
+              new String[][] {{"c2"}},
+              Map.of(
+                  "bloom_filter_size", "-1",
+                  "hash_functions", "3",
+                  "random_seed", "1")),
+        };
+    Assertions.assertThrows(
+        IllegalArgumentException.class,
+        () ->
+            ops.buildCreateSql(
+                "t_bad2",
+                cols,
+                "comment",
+                new HashMap<>(),
+                new Transform[0],
+                Distributions.NONE,
+                negativeSize,
+                ClickHouseUtils.getSortOrders("c1")));
+
+    // Zero bloom_filter_size should throw (>= 1 required)
+    Index[] zeroSize =
+        new Index[] {
+          Indexes.of(
+              IndexType.DATA_SKIPPING_TOKENBFV1,
+              "idx_bad3",
+              new String[][] {{"c2"}},
+              Map.of(
+                  "bloom_filter_size", "0",
+                  "hash_functions", "3",
+                  "random_seed", "1")),
+        };
+    Assertions.assertThrows(
+        IllegalArgumentException.class,
+        () ->
+            ops.buildCreateSql(
+                "t_bad3",
+                cols,
+                "comment",
+                new HashMap<>(),
+                new Transform[0],
+                Distributions.NONE,
+                zeroSize,
+                ClickHouseUtils.getSortOrders("c1")));
+
+    // Non-numeric hash_functions should throw
+    Index[] nonNumericHash =
+        new Index[] {
+          Indexes.of(
+              IndexType.DATA_SKIPPING_NGRAMBFV1,
+              "idx_bad4",
+              new String[][] {{"c2"}},
+              Map.of(
+                  "ngram_size", "3",
+                  "bloom_filter_size", "512",
+                  "hash_functions", "abc",
+                  "random_seed", "1")),
+        };
+    Assertions.assertThrows(
+        IllegalArgumentException.class,
+        () ->
+            ops.buildCreateSql(
+                "t_bad4",
+                cols,
+                "comment",
+                new HashMap<>(),
+                new Transform[0],
+                Distributions.NONE,
+                nonNumericHash,
+                ClickHouseUtils.getSortOrders("c1")));
+
+    // Negative random_seed should throw (>= 0 required)
+    Index[] negativeSeed =
+        new Index[] {
+          Indexes.of(
+              IndexType.DATA_SKIPPING_TOKENBFV1,
+              "idx_bad5",
+              new String[][] {{"c2"}},
+              Map.of(
+                  "bloom_filter_size", "256",
+                  "hash_functions", "3",
+                  "random_seed", "-1")),
+        };
+    Assertions.assertThrows(
+        IllegalArgumentException.class,
+        () ->
+            ops.buildCreateSql(
+                "t_bad5",
+                cols,
+                "comment",
+                new HashMap<>(),
+                new Transform[0],
+                Distributions.NONE,
+                negativeSeed,
+                ClickHouseUtils.getSortOrders("c1")));
+
+    // Non-numeric ngram_size should throw (ngrambf_v1 specific)
+    Index[] nonNumericNgram =
+        new Index[] {
+          Indexes.of(
+              IndexType.DATA_SKIPPING_NGRAMBFV1,
+              "idx_bad6",
+              new String[][] {{"c2"}},
+              Map.of(
+                  "ngram_size", "xyz",
+                  "bloom_filter_size", "512",
+                  "hash_functions", "3",
+                  "random_seed", "1")),
+        };
+    Assertions.assertThrows(
+        IllegalArgumentException.class,
+        () ->
+            ops.buildCreateSql(
+                "t_bad6",
+                cols,
+                "comment",
+                new HashMap<>(),
+                new Transform[0],
+                Distributions.NONE,
+                nonNumericNgram,
+                ClickHouseUtils.getSortOrders("c1")));
+  }
+
+  @Test
+  public void testAlterTableAddIndexWithNgrambfAndTokenbf() {
+    StubClickHouseTableOperations ops = new StubClickHouseTableOperations();
+    ops.initialize(
+        null,
+        new ClickHouseExceptionConverter(),
+        new ClickHouseTypeConverter(),
+        new ClickHouseColumnDefaultValueConverter(),
+        new HashMap<>());
+    ops.setTable(buildStubTable());
+
+    // ALTER TABLE ADD INDEX for ngrambf_v1 with full properties
+    String ngrambfSql =
+        ops.buildAlterSql(
+            "db",
+            "tbl",
+            new TableChange[] {
+              TableChange.addIndex(
+                  IndexType.DATA_SKIPPING_NGRAMBFV1,
+                  "idx_ngram",
+                  new String[][] {{"c2"}},
+                  Map.of(
+                      "ngram_size", "3",
+                      "bloom_filter_size", "512",
+                      "hash_functions", "3",
+                      "random_seed", "0"))
+            });
+    Assertions.assertTrue(
+        ngrambfSql.contains(
+            "ADD INDEX `idx_ngram` `c2` TYPE ngrambf_v1(3, 512, 3, 0) 
GRANULARITY 1"),
+        "DDL should contain ngrambf_v1 with parameters: " + ngrambfSql);
+
+    // ALTER TABLE ADD INDEX for tokenbf_v1 with full properties
+    String tokenbfSql =
+        ops.buildAlterSql(
+            "db",
+            "tbl",
+            new TableChange[] {
+              TableChange.addIndex(
+                  IndexType.DATA_SKIPPING_TOKENBFV1,
+                  "idx_token",
+                  new String[][] {{"c2"}},
+                  Map.of(
+                      "bloom_filter_size", "256",
+                      "hash_functions", "2",
+                      "random_seed", "1",
+                      "granularity", "4"))
+            });
+    Assertions.assertTrue(
+        tokenbfSql.contains("ADD INDEX `idx_token` `c2` TYPE tokenbf_v1(256, 
2, 1) GRANULARITY 4"),
+        "DDL should contain tokenbf_v1 with custom GRANULARITY: " + 
tokenbfSql);
+  }
+
   @Test
   public void testAlterTableNullabilityValidationFails() {
     StubClickHouseTableOperations ops = new StubClickHouseTableOperations();
diff --git 
a/catalogs-contrib/catalog-jdbc-clickhouse/src/test/java/org/apache/gravitino/catalog/clickhouse/operations/TestClickHouseTableOperationsUnit.java
 
b/catalogs-contrib/catalog-jdbc-clickhouse/src/test/java/org/apache/gravitino/catalog/clickhouse/operations/TestClickHouseTableOperationsUnit.java
index 76d04b4525..8dfe14b204 100644
--- 
a/catalogs-contrib/catalog-jdbc-clickhouse/src/test/java/org/apache/gravitino/catalog/clickhouse/operations/TestClickHouseTableOperationsUnit.java
+++ 
b/catalogs-contrib/catalog-jdbc-clickhouse/src/test/java/org/apache/gravitino/catalog/clickhouse/operations/TestClickHouseTableOperationsUnit.java
@@ -23,6 +23,7 @@ import static 
org.apache.gravitino.catalog.clickhouse.ClickHouseUtils.getSortOrd
 import java.sql.Connection;
 import java.sql.PreparedStatement;
 import java.sql.ResultSet;
+import java.sql.SQLException;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -32,6 +33,7 @@ import 
org.apache.gravitino.catalog.clickhouse.converter.ClickHouseColumnDefault
 import 
org.apache.gravitino.catalog.clickhouse.converter.ClickHouseExceptionConverter;
 import 
org.apache.gravitino.catalog.clickhouse.converter.ClickHouseTypeConverter;
 import org.apache.gravitino.catalog.jdbc.JdbcColumn;
+import org.apache.gravitino.exceptions.GravitinoRuntimeException;
 import org.apache.gravitino.exceptions.NoSuchTableException;
 import org.apache.gravitino.rel.expressions.FunctionExpression;
 import org.apache.gravitino.rel.expressions.NamedReference;
@@ -474,4 +476,138 @@ public class TestClickHouseTableOperationsUnit {
             IllegalArgumentException.class, () -> 
newOps().callGenerateCreateTableSql(properties));
     Assertions.assertTrue(exception.getMessage().contains("balanced"));
   }
+
+  @Test
+  void testGetIndexesFailsOnMalformedParameterizedIndexMetadata() throws 
Exception {
+    ExposedClickHouseTableOperations ops = newOps();
+
+    PreparedStatement primaryKeyStmt = Mockito.mock(PreparedStatement.class);
+    ResultSet primaryKeyRs = Mockito.mock(ResultSet.class);
+    PreparedStatement secondaryStmt = Mockito.mock(PreparedStatement.class);
+    ResultSet secondaryRs = Mockito.mock(ResultSet.class);
+
+    Mockito.when(primaryKeyRs.next()).thenReturn(false);
+    Mockito.when(primaryKeyStmt.executeQuery()).thenReturn(primaryKeyRs);
+    Mockito.when(secondaryRs.next()).thenReturn(true, false);
+    Mockito.when(secondaryStmt.executeQuery()).thenReturn(secondaryRs);
+    Mockito.when(secondaryRs.getString("name")).thenReturn("idx_bad");
+    Mockito.when(secondaryRs.getString("type")).thenReturn("ngrambf_v1");
+    Mockito.when(secondaryRs.getString("type_full")).thenReturn(null);
+    Mockito.when(secondaryRs.getString("expr")).thenReturn("col_1");
+    Mockito.when(secondaryRs.getLong("granularity")).thenReturn(1L);
+
+    Connection connection = Mockito.mock(Connection.class);
+    Mockito.when(connection.prepareStatement(Mockito.anyString()))
+        .thenReturn(primaryKeyStmt)
+        .thenReturn(secondaryStmt);
+
+    IllegalArgumentException exception =
+        Assertions.assertThrows(
+            IllegalArgumentException.class, () -> 
ops.callGetIndexes(connection, "db", "tbl"));
+    Assertions.assertTrue(exception.getMessage().contains("idx_bad"));
+    Assertions.assertTrue(exception.getMessage().contains("type_full"));
+  }
+
+  @Test
+  void testGetIndexesSkipsUnsupportedExpressionForParameterizedIndex() throws 
Exception {
+    ExposedClickHouseTableOperations ops = newOps();
+
+    PreparedStatement primaryKeyStmt = Mockito.mock(PreparedStatement.class);
+    ResultSet primaryKeyRs = Mockito.mock(ResultSet.class);
+    PreparedStatement secondaryStmt = Mockito.mock(PreparedStatement.class);
+    ResultSet secondaryRs = Mockito.mock(ResultSet.class);
+
+    Mockito.when(primaryKeyRs.next()).thenReturn(false);
+    Mockito.when(primaryKeyStmt.executeQuery()).thenReturn(primaryKeyRs);
+    Mockito.when(secondaryRs.next()).thenReturn(true, true, false);
+    Mockito.when(secondaryStmt.executeQuery()).thenReturn(secondaryRs);
+    Mockito.when(secondaryRs.getString("name")).thenReturn("idx_bad_expr", 
"idx_valid");
+    Mockito.when(secondaryRs.getString("type")).thenReturn("ngrambf_v1", 
"tokenbf_v1");
+    Mockito.when(secondaryRs.getString("type_full"))
+        .thenReturn("ngrambf_v1(3, 512, 3, 0)", "tokenbf_v1(256, 2, 0)");
+    Mockito.when(secondaryRs.getString("expr")).thenReturn("cityHash64(col_1) 
% 16", "col_2");
+    Mockito.when(secondaryRs.getLong("granularity")).thenReturn(1L, 1L);
+
+    Connection connection = Mockito.mock(Connection.class);
+    Mockito.when(connection.prepareStatement(Mockito.anyString()))
+        .thenReturn(primaryKeyStmt)
+        .thenReturn(secondaryStmt);
+
+    List<Index> indexes = ops.callGetIndexes(connection, "db", "tbl");
+
+    Assertions.assertEquals(1, indexes.size());
+    Assertions.assertEquals("idx_valid", indexes.get(0).name());
+    Assertions.assertEquals(Index.IndexType.DATA_SKIPPING_TOKENBFV1, 
indexes.get(0).type());
+    Assertions.assertArrayEquals(new String[][] {{"col_2"}}, 
indexes.get(0).fieldNames());
+    Assertions.assertEquals(
+        Map.of(
+            "bloom_filter_size", "256",
+            "hash_functions", "2",
+            "random_seed", "0"),
+        indexes.get(0).properties());
+  }
+
+  @Test
+  void testGetIndexesFallsBackWhenTypeFullColumnIsMissing() throws Exception {
+    ExposedClickHouseTableOperations ops = newOps();
+
+    PreparedStatement primaryKeyStmt = Mockito.mock(PreparedStatement.class);
+    ResultSet primaryKeyRs = Mockito.mock(ResultSet.class);
+    PreparedStatement modernSecondaryStmt = 
Mockito.mock(PreparedStatement.class);
+    PreparedStatement legacySecondaryStmt = 
Mockito.mock(PreparedStatement.class);
+    ResultSet legacySecondaryRs = Mockito.mock(ResultSet.class);
+
+    Mockito.when(primaryKeyRs.next()).thenReturn(false);
+    Mockito.when(primaryKeyStmt.executeQuery()).thenReturn(primaryKeyRs);
+    Mockito.when(modernSecondaryStmt.executeQuery())
+        .thenThrow(new SQLException("Unknown identifier 'type_full'"));
+    
Mockito.when(legacySecondaryStmt.executeQuery()).thenReturn(legacySecondaryRs);
+    Mockito.when(legacySecondaryRs.next()).thenReturn(true, false);
+    Mockito.when(legacySecondaryRs.getString("name")).thenReturn("idx_legacy");
+    
Mockito.when(legacySecondaryRs.getString("type")).thenReturn("ngrambf_v1(3, 
512, 3, 0)");
+    Mockito.when(legacySecondaryRs.getString("expr")).thenReturn("col_1");
+    Mockito.when(legacySecondaryRs.getLong("granularity")).thenReturn(1L);
+
+    Connection connection = Mockito.mock(Connection.class);
+    Mockito.when(connection.prepareStatement(Mockito.anyString()))
+        .thenReturn(primaryKeyStmt)
+        .thenReturn(modernSecondaryStmt)
+        .thenReturn(legacySecondaryStmt);
+
+    List<Index> indexes = ops.callGetIndexes(connection, "db", "tbl");
+
+    Assertions.assertEquals(1, indexes.size());
+    Assertions.assertEquals(Index.IndexType.DATA_SKIPPING_NGRAMBFV1, 
indexes.get(0).type());
+    Assertions.assertEquals(
+        Map.of(
+            "ngram_size", "3",
+            "bloom_filter_size", "512",
+            "hash_functions", "3",
+            "random_seed", "0"),
+        indexes.get(0).properties());
+  }
+
+  @Test
+  void testGetIndexesDoesNotFallbackForOtherSqlErrors() throws Exception {
+    ExposedClickHouseTableOperations ops = newOps();
+
+    PreparedStatement primaryKeyStmt = Mockito.mock(PreparedStatement.class);
+    ResultSet primaryKeyRs = Mockito.mock(ResultSet.class);
+    PreparedStatement secondaryStmt = Mockito.mock(PreparedStatement.class);
+    Mockito.when(primaryKeyRs.next()).thenReturn(false);
+    Mockito.when(primaryKeyStmt.executeQuery()).thenReturn(primaryKeyRs);
+    Mockito.when(secondaryStmt.executeQuery())
+        .thenThrow(new SQLException("Connection reset by peer"));
+
+    Connection connection = Mockito.mock(Connection.class);
+    Mockito.when(connection.prepareStatement(Mockito.anyString()))
+        .thenReturn(primaryKeyStmt)
+        .thenReturn(secondaryStmt);
+
+    GravitinoRuntimeException exception =
+        Assertions.assertThrows(
+            GravitinoRuntimeException.class, () -> 
ops.callGetIndexes(connection, "db", "tbl"));
+    Assertions.assertTrue(exception.getCause() instanceof SQLException);
+    Mockito.verify(connection, 
Mockito.times(2)).prepareStatement(Mockito.anyString());
+  }
 }
diff --git a/docs/jdbc-clickhouse-catalog.md b/docs/jdbc-clickhouse-catalog.md
index 2371ea2bcc..90af2cc5b2 100644
--- a/docs/jdbc-clickhouse-catalog.md
+++ b/docs/jdbc-clickhouse-catalog.md
@@ -172,7 +172,7 @@ See [Manage Catalogs and 
Schemas](./manage-catalogs-and-schemas.md#schema-operat
 | Mapping             | Gravitino table maps to a ClickHouse table             
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
   |
 | Engines             | **MergeTree family** (`MergeTree` default, 
`ReplacingMergeTree`, `SummingMergeTree`, `AggregatingMergeTree`, 
`CollapsingMergeTree`, `VersionedCollapsingMergeTree`, `GraphiteMergeTree`): 
fully supported, data persists across restarts. **Log family** (`TinyLog`, 
`StripeLog`, `Log`): supported, data and table definition persist across 
restarts. **`Null`**: supported, table persists, data is always discarded by 
design. **`Set`**: supported, table definition persists.  [...]
 | Ordering/Partition  | MergeTree-family requires exactly one `ORDER BY` 
column; only single-column identity `PARTITION BY` is supported on MergeTree 
engines. Other engines reject `ORDER BY`/`PARTITION BY`.                        
                                                                                
                                                                                
                                                                                
            |
-| Indexes             | Primary key; data-skipping indexes 
`DATA_SKIPPING_MINMAX`, `DATA_SKIPPING_BLOOM_FILTER`, and `DATA_SKIPPING_SET` 
(configurable granularity via `Index.properties()`).                            
                                                                                
                                                                                
                                                                                
                                    [...]
+| Indexes             | Primary key; data-skipping indexes 
`DATA_SKIPPING_MINMAX`, `DATA_SKIPPING_BLOOM_FILTER`, `DATA_SKIPPING_SET`, 
`DATA_SKIPPING_NGRAMBFV1`, and `DATA_SKIPPING_TOKENBFV1` (configurable 
granularity via `Index.properties()`).                                          
                                                                                
                                                                                
                                                [...]
 | Distribution        | Gravitino enforces `Distributions.NONE`; no custom 
distribution strategies.                                                        
                                                                                
                                                                                
                                                                                
                                                                                
       |
 | Column defaults     | Supported.                                             
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
   |
 | Unsupported         | Engine change after creation; removing table 
properties; auto-increment columns.                                             
                                                                                
                                                                                
                                                                                
                                                                                
             |
@@ -243,9 +243,13 @@ If you need Gravitino to manage an existing cluster 
database or table, recreate
   - `DATA_SKIPPING_MINMAX` (default `GRANULARITY 1`)
   - `DATA_SKIPPING_BLOOM_FILTER` (default `GRANULARITY 1`)
   - `DATA_SKIPPING_SET` (default `GRANULARITY 1`, plus configurable `set(N)` 
max values)
+  - `DATA_SKIPPING_NGRAMBFV1` (`GRANULARITY` customizable via 
`Index.properties()`, default 1; requires `ngram_size`, `bloom_filter_size`, 
`hash_functions`, `random_seed` in `Index.properties()`)
+  - `DATA_SKIPPING_TOKENBFV1` (`GRANULARITY` customizable via 
`Index.properties()`, default 1; requires `bloom_filter_size`, 
`hash_functions`, `random_seed` in `Index.properties()`)
 
   Custom `GRANULARITY` can be specified via the `Index.properties()` API (key 
`granularity`, value must be a positive integer). For `DATA_SKIPPING_SET`, the 
max unique values can be configured via `set_max_values` (non-negative 
integer). If not specified, the defaults above apply.
 
+  On ClickHouse versions without `system.data_skipping_indices.type_full`, 
Gravitino falls back to the legacy metadata query. If the legacy `type` value 
does not include the bloom-filter parameters, the index type and fields are 
preserved but the required parameter properties cannot be reconstructed; 
provide the properties explicitly before recreating the table.
+
 ### Partitioning, Sorting, and Distribution
 
 - `ORDER BY`: required for MergeTree-family engines and only columns identity 
are supported;
@@ -350,7 +354,7 @@ Supported:
 - Rename column.
 - Update column type/comment/default/position/nullability.
 - Delete columns (with `IF EXISTS` support).
-- Add data-skipping indexes with custom `GRANULARITY` and `set(N)` via 
`Index.properties()`; drop data-skipping indexes. Adding/dropping primary key 
is not supported.
+- Add and drop data-skipping indexes; configure custom `GRANULARITY`, 
`set(N)`, and `ngrambf_v1`/`tokenbf_v1` Bloom-filter parameters via 
`Index.properties()`. Adding/dropping primary key is not supported.
 - Update table comment.
 
 Unsupported:
diff --git a/docs/open-api/indexes.yaml b/docs/open-api/indexes.yaml
index c4e1a00cf2..af250b8967 100644
--- a/docs/open-api/indexes.yaml
+++ b/docs/open-api/indexes.yaml
@@ -38,6 +38,8 @@ components:
             - "data_skipping_minmax"
             - "data_skipping_bloom_filter"
             - "data_skipping_set"
+            - "data_skipping_ngrambfv1"
+            - "data_skipping_tokenbfv1"
         name:
           type: string
           description: The name of the index

Reply via email to