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 f8acb110ff [#13376] feat(clickhouse): preserve legacy Annoy and
USearch index metadata (#13432)
f8acb110ff is described below
commit f8acb110ff3f6b2df6c55fde087fdb72e9d39470
Author: Thunderkeg <[email protected]>
AuthorDate: Sun Oct 11 20:36:16 2026 +0800
[#13376] feat(clickhouse): preserve legacy Annoy and USearch index metadata
(#13432)
### What changes were proposed in this pull request?
Preserve legacy ClickHouse Annoy and USearch index metadata when the
catalog loads tables. Parse the documented optional parameters, retain
the complete reported type expression, and expose recognized values
through Gravitino's index properties.
Annoy parameters are exposed as `annoy_distance_function` and
`annoy_trees`; USearch parameters are exposed as
`usearch_distance_function` and `usearch_scalar_kind`. The original
expression remains available in `clickhouse_type_full`. Empty parameter
lists and bare legacy type names are preserved without a parsing
warning; unrecognized expressions remain available in
`clickhouse_type_full` and produce a warning.
Treat both legacy types as metadata-only. Gravitino rejects CREATE
TABLE, ALTER TABLE ADD INDEX, and table recreation requests that use
them, and does not translate them to `vector_similarity`.
### Why are the changes needed?
ClickHouse 24.x can report legacy Annoy and USearch index definitions
through `system.data_skipping_indices`. The catalog needs to preserve
those definitions and their documented parameters when loading tables,
without implying that the legacy indexes support writes or vector
search.
Fix: #13376
### Does this PR introduce any user-facing change?
Yes. It adds the `DATA_SKIPPING_ANNOY` and `DATA_SKIPPING_USEARCH` index
types and the read-only properties `annoy_distance_function`,
`annoy_trees`, `usearch_distance_function`, `usearch_scalar_kind`, and
`clickhouse_type_full`. Clients must understand the new enum values to
expose these indexes.
### How was this patch tested?
- `./gradlew :catalogs-contrib:catalog-jdbc-clickhouse:test --tests
'org.apache.gravitino.catalog.clickhouse.operations.TestClickHouseTableOperationsUnit'
-PskipITs` — 89 tests passed, including documented parameter forms,
malformed parameters, warning behavior, and raw-expression preservation.
- `TestJsonUtils` — 13 tests passed.
- Focused Python serde tests — 57 tests passed.
- `CatalogClickHouseIT.testLoadLegacyUsearchIndexMetadata` and
`CatalogClickHouseIT.testLoadLegacyAnnoyIndexMetadata` — 2 tests passed
using ClickHouse 23.8.16.16 and 24.8.14.39 fixtures.
- `GRAVITINO_CLICKHOUSE_TEST_IMAGE=clickhouse:25.8.14.17 ./gradlew
:catalogs-contrib:catalog-jdbc-clickhouse:test --tests
'org.apache.gravitino.catalog.clickhouse.integration.test.CatalogClickHouseClusterIT'
-PskipDockerTests=false` — 21 tests passed.
- `./gradlew :catalogs-contrib:catalog-jdbc-clickhouse:build -x test -x
rat` and `./gradlew compileJava -x test` passed; Spotless, the OpenAPI
documentation build, Black, and `git diff --check` passed.
---------
Signed-off-by: jiangxt2 <[email protected]>
---
.../org/apache/gravitino/rel/indexes/Index.java | 6 +
.../catalog/clickhouse/ClickHouseConstants.java | 21 +
.../operations/ClickHouseTableOperations.java | 225 ++++++++++-
.../integration/test/CatalogClickHouseIT.java | 226 +++++++++++
.../TestClickHouseTableOperationsUnit.java | 433 ++++++++++++++++++++-
.../gravitino/api/rel/indexes/index.py | 8 +
.../dto/rel/indexes/json_serdes/index_serdes.py | 9 +-
.../tests/unittests/dto/rel/test_index_serdes.py | 60 +++
.../org/apache/gravitino/json/TestJsonUtils.java | 14 +
docs/jdbc-clickhouse-catalog.md | 10 +-
docs/open-api/indexes.yaml | 2 +
11 files changed, 996 insertions(+), 18 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 e139ac9c69..dbfdbe0195 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
@@ -149,5 +149,11 @@ public interface Index {
* Vector similarity data skipping index for approximate nearest-neighbor
search in ClickHouse.
*/
DATA_SKIPPING_VECTOR_SIMILARITY,
+
+ /** Legacy Annoy data-skipping index metadata in ClickHouse. */
+ DATA_SKIPPING_ANNOY,
+
+ /** Legacy USearch data-skipping index metadata in ClickHouse. */
+ DATA_SKIPPING_USEARCH,
}
}
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 6f40eedf6f..6263ac27c8 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
@@ -106,6 +106,27 @@ public class ClickHouseConstants {
public static final String HNSW_CANDIDATE_LIST_SIZE_FOR_CONSTRUCTION =
"hnsw_candidate_list_size_for_construction";
+ /** The name of the legacy Annoy data-skipping index type in ClickHouse. */
+ public static final String DATA_SKIPPING_ANNOY = "annoy";
+
+ /** The name of the legacy USearch data-skipping index type in ClickHouse.
*/
+ public static final String DATA_SKIPPING_USEARCH = "usearch";
+
+ /** Property key for the number of trees in a legacy Annoy index. */
+ public static final String ANNOY_TREES = "annoy_trees";
+
+ /** Property key for the distance function in a legacy Annoy index. */
+ public static final String ANNOY_DISTANCE_FUNCTION =
"annoy_distance_function";
+
+ /** Property key for the distance function in a legacy USearch index. */
+ public static final String USEARCH_DISTANCE_FUNCTION =
"usearch_distance_function";
+
+ /** Property key for the scalar kind in a legacy USearch index. */
+ public static final String USEARCH_SCALAR_KIND = "usearch_scalar_kind";
+
+ /** Property key for ClickHouse's complete index type expression. */
+ public static final String CLICKHOUSE_TYPE_FULL = "clickhouse_type_full";
+
/** Property key for bloom filter size in ngrambf_v1 and tokenbf_v1 index
properties. */
public static final String BLOOM_FILTER_SIZE = "bloom_filter_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 bb07965db0..5570da8825 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,12 +18,17 @@
*/
package org.apache.gravitino.catalog.clickhouse.operations;
+import static
org.apache.gravitino.catalog.clickhouse.ClickHouseConstants.IndexConstants.ANNOY_DISTANCE_FUNCTION;
+import static
org.apache.gravitino.catalog.clickhouse.ClickHouseConstants.IndexConstants.ANNOY_TREES;
import static
org.apache.gravitino.catalog.clickhouse.ClickHouseConstants.IndexConstants.BLOOM_FILTER_SIZE;
+import static
org.apache.gravitino.catalog.clickhouse.ClickHouseConstants.IndexConstants.CLICKHOUSE_TYPE_FULL;
+import static
org.apache.gravitino.catalog.clickhouse.ClickHouseConstants.IndexConstants.DATA_SKIPPING_ANNOY;
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.DATA_SKIPPING_USEARCH;
import static
org.apache.gravitino.catalog.clickhouse.ClickHouseConstants.IndexConstants.DATA_SKIPPING_VECTOR_SIMILARITY;
import static
org.apache.gravitino.catalog.clickhouse.ClickHouseConstants.IndexConstants.GRANULARITY;
import static
org.apache.gravitino.catalog.clickhouse.ClickHouseConstants.IndexConstants.HASH_FUNCTIONS;
@@ -32,6 +37,8 @@ import static
org.apache.gravitino.catalog.clickhouse.ClickHouseConstants.IndexC
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.ClickHouseConstants.IndexConstants.USEARCH_DISTANCE_FUNCTION;
+import static
org.apache.gravitino.catalog.clickhouse.ClickHouseConstants.IndexConstants.USEARCH_SCALAR_KIND;
import static
org.apache.gravitino.catalog.clickhouse.ClickHouseConstants.IndexConstants.VECTOR_SIMILARITY_DIMENSIONS;
import static
org.apache.gravitino.catalog.clickhouse.ClickHouseConstants.IndexConstants.VECTOR_SIMILARITY_DISTANCE_FUNCTION;
import static
org.apache.gravitino.catalog.clickhouse.ClickHouseConstants.IndexConstants.VECTOR_SIMILARITY_QUANTIZATION;
@@ -356,6 +363,7 @@ public class ClickHouseTableOperations extends
JdbcTableOperations {
Transform[] partitioning,
Distribution distribution,
Index[] indexes) {
+ rejectLegacyAnnUsearchIndexes(tableName, indexes);
throw new UnsupportedOperationException(
"generateCreateTableSql with out sortOrders in clickhouse is not
supported");
}
@@ -371,6 +379,8 @@ public class ClickHouseTableOperations extends
JdbcTableOperations {
Index[] indexes,
SortOrder[] sortOrders) {
+ rejectLegacyAnnUsearchIndexes(tableName, indexes);
+
Preconditions.checkArgument(
Distributions.isNone(distribution), "ClickHouse does not support
distribution");
@@ -1326,6 +1336,9 @@ public class ClickHouseTableOperations extends
JdbcTableOperations {
private String addIndexDefinition(JdbcTable table, TableChange.AddIndex
addIndex) {
Preconditions.checkArgument(
StringUtils.isNotBlank(addIndex.getName()), "Index name is required");
+ if (isLegacyAnnUsearchIndex(addIndex.getType())) {
+ throw legacyAnnUsearchWriteException(table.name(), addIndex.getName(),
addIndex.getType());
+ }
Preconditions.checkArgument(
ArrayUtils.isNotEmpty(addIndex.getFieldNames()), "Index field names
are required");
@@ -2049,21 +2062,49 @@ public class ClickHouseTableOperations extends
JdbcTableOperations {
continue;
}
+ if (isLegacyAnnUsearchIndex(indexType) &&
StringUtils.isBlank(parameterSource)) {
+ parameterSource = type;
+ parameterSourceName = "legacy type";
+ }
+
Map<String, String> parameterProperties = Collections.emptyMap();
String[][] fields;
try {
fields = parseIndexFields(expression);
} catch (IllegalArgumentException ignored) {
- LOG.warn(
- "Skip unsupported data skipping index {} for {}.{} with type
{} because its "
- + "expression cannot be represented as index field names",
- name,
- databaseName,
- tableName,
- type);
+ if (isLegacyAnnUsearchIndex(indexType)) {
+ LOG.warn(
+ "Cannot expose legacy ClickHouse index {} of type {} on
{}.{} because its "
+ + "expression '{}' cannot be represented as Gravitino
index field names; "
+ + "the index is omitted from the loaded metadata",
+ name,
+ type,
+ databaseName,
+ tableName,
+ expression);
+ } else {
+ LOG.warn(
+ "Skip unsupported data skipping index {} for {}.{} with type
{} because its "
+ + "expression cannot be represented as index field
names",
+ name,
+ databaseName,
+ tableName,
+ type);
+ }
continue;
}
if (ArrayUtils.isEmpty(fields)) {
+ if (isLegacyAnnUsearchIndex(indexType)) {
+ LOG.warn(
+ "Cannot expose legacy ClickHouse index {} of type {} on
{}.{} because its "
+ + "expression '{}' contains no Gravitino index fields;
the index is "
+ + "omitted from the loaded metadata",
+ name,
+ type,
+ databaseName,
+ tableName,
+ expression);
+ }
continue;
}
@@ -2081,6 +2122,7 @@ public class ClickHouseTableOperations extends
JdbcTableOperations {
if (indexType == Index.IndexType.DATA_SKIPPING_SET
|| isParameterizedBloomFilterIndex(indexType)
+ || isLegacyAnnUsearchIndex(indexType)
|| indexType == Index.IndexType.DATA_SKIPPING_VECTOR_SIMILARITY)
{
try {
parameterProperties =
@@ -2117,7 +2159,7 @@ public class ClickHouseTableOperations extends
JdbcTableOperations {
indexType == Index.IndexType.DATA_SKIPPING_VECTOR_SIMILARITY
? DEFAULT_VECTOR_SIMILARITY_GRANULARITY
: DEFAULT_INDEX_GRANULARITY;
- if (granularity != defaultGranularity) {
+ if (granularity != defaultGranularity ||
isLegacyAnnUsearchIndex(indexType)) {
properties.put(GRANULARITY, String.valueOf(granularity));
}
if (!includesTypeFull
@@ -2252,6 +2294,9 @@ public class ClickHouseTableOperations extends
JdbcTableOperations {
case DATA_SKIPPING_TOKENBFV1:
return parseBloomFilterPropertiesForQuery(
indexType, parameterSource, indexName, allowBareLegacyType);
+ case DATA_SKIPPING_ANNOY:
+ case DATA_SKIPPING_USEARCH:
+ return parseLegacyAnnUsearchProperties(indexType, parameterSource,
indexName);
case DATA_SKIPPING_VECTOR_SIMILARITY:
if (allowBareLegacyType && !StringUtils.contains(parameterSource,
"(")) {
return Collections.emptyMap();
@@ -2539,11 +2584,157 @@ public class ClickHouseTableOperations extends
JdbcTableOperations {
|| indexType == Index.IndexType.DATA_SKIPPING_TOKENBFV1;
}
+ private static Map<String, String> parseLegacyAnnUsearchProperties(
+ Index.IndexType indexType, String typeFull, String indexName) {
+ String expectedType = legacyAnnUsearchTypeName(indexType);
+ String rawTypeFull = StringUtils.defaultString(typeFull);
+ String normalizedTypeFull = rawTypeFull.trim();
+ Map<String, String> properties = new HashMap<>();
+ if (StringUtils.isNotEmpty(rawTypeFull)) {
+ properties.put(CLICKHOUSE_TYPE_FULL, rawTypeFull);
+ }
+
+ int paramsStart = normalizedTypeFull.indexOf('(');
+ if (paramsStart < 0 && StringUtils.equalsIgnoreCase(expectedType,
normalizedTypeFull)) {
+ return Map.copyOf(properties);
+ }
+ int paramsEnd = normalizedTypeFull.lastIndexOf(')');
+ if (paramsStart <= 0
+ || paramsEnd != normalizedTypeFull.length() - 1
+ || !StringUtils.equalsIgnoreCase(
+ expectedType, normalizedTypeFull.substring(0,
paramsStart).trim())) {
+ LOG.warn(
+ "Could not parse legacy ClickHouse type expression '{}' for index
'{}' of type {}; "
+ + "preserving it in '{}'",
+ rawTypeFull,
+ indexName,
+ expectedType,
+ CLICKHOUSE_TYPE_FULL);
+ return Map.copyOf(properties);
+ }
+
+ // ClickHouse documents both parameter lists as optional, so an empty list
is valid metadata.
+ String parameterText = normalizedTypeFull.substring(paramsStart + 1,
paramsEnd);
+ if (StringUtils.isBlank(parameterText)) {
+ return Map.copyOf(properties);
+ }
+ // These parameters are fixed identifiers or an integer, so commas delimit
the argument list.
+ String[] parameters = parameterText.split(",", -1);
+
+ if (indexType == Index.IndexType.DATA_SKIPPING_ANNOY) {
+ if (parameters.length == 1) {
+ String parameter = unquoteMetadataParameter(parameters[0].trim());
+ if (isUnsignedInteger(parameter)) {
+ // Preserve the one-argument form that predates the optional
distance parameter.
+ properties.put(ANNOY_TREES, parameter);
+ } else if (isLegacyAnnUsearchDistanceFunction(parameter)) {
+ properties.put(ANNOY_DISTANCE_FUNCTION, parameter);
+ } else {
+ warnUnparsedLegacyAnnUsearchParameters(rawTypeFull, expectedType,
indexName);
+ }
+ } else if (parameters.length == 2) {
+ String distanceFunction =
unquoteMetadataParameter(parameters[0].trim());
+ String trees = unquoteMetadataParameter(parameters[1].trim());
+ if (isLegacyAnnUsearchDistanceFunction(distanceFunction) &&
isUnsignedInteger(trees)) {
+ properties.put(ANNOY_DISTANCE_FUNCTION, distanceFunction);
+ properties.put(ANNOY_TREES, trees);
+ } else {
+ warnUnparsedLegacyAnnUsearchParameters(rawTypeFull, expectedType,
indexName);
+ }
+ } else {
+ warnUnparsedLegacyAnnUsearchParameters(rawTypeFull, expectedType,
indexName);
+ }
+ } else if (parameters.length == 1) {
+ String distanceFunction = unquoteMetadataParameter(parameters[0].trim());
+ if (isLegacyAnnUsearchDistanceFunction(distanceFunction)) {
+ properties.put(USEARCH_DISTANCE_FUNCTION, distanceFunction);
+ } else {
+ warnUnparsedLegacyAnnUsearchParameters(rawTypeFull, expectedType,
indexName);
+ }
+ } else if (parameters.length == 2) {
+ String distanceFunction = unquoteMetadataParameter(parameters[0].trim());
+ String scalarKind = unquoteMetadataParameter(parameters[1].trim());
+ if (isLegacyAnnUsearchDistanceFunction(distanceFunction)
+ && isLegacyUsearchScalarKind(scalarKind)) {
+ properties.put(USEARCH_DISTANCE_FUNCTION, distanceFunction);
+ properties.put(USEARCH_SCALAR_KIND, scalarKind);
+ } else {
+ warnUnparsedLegacyAnnUsearchParameters(rawTypeFull, expectedType,
indexName);
+ }
+ } else {
+ warnUnparsedLegacyAnnUsearchParameters(rawTypeFull, expectedType,
indexName);
+ }
+ return Map.copyOf(properties);
+ }
+
+ private static boolean isUnsignedInteger(String value) {
+ return value.matches("[0-9]+");
+ }
+
+ private static boolean isLegacyAnnUsearchDistanceFunction(String value) {
+ return StringUtils.equalsIgnoreCase("L2Distance", value)
+ || StringUtils.equalsIgnoreCase("cosineDistance", value);
+ }
+
+ private static boolean isLegacyUsearchScalarKind(String value) {
+ return StringUtils.equalsAnyIgnoreCase(value, "f64", "f32", "f16", "i8");
+ }
+
+ private static void warnUnparsedLegacyAnnUsearchParameters(
+ String typeFull, String expectedType, String indexName) {
+ LOG.warn(
+ "Could not parse parameters in legacy ClickHouse type expression '{}'
for index '{}' "
+ + "of type {}; preserving the complete expression",
+ typeFull,
+ indexName,
+ expectedType);
+ }
+
+ private static String unquoteMetadataParameter(String parameter) {
+ if (parameter.length() >= 2
+ && ((parameter.startsWith("'") && parameter.endsWith("'"))
+ || (parameter.startsWith("\"") && parameter.endsWith("\"")))) {
+ return parameter.substring(1, parameter.length() - 1);
+ }
+ return parameter;
+ }
+
+ private static boolean isLegacyAnnUsearchIndex(Index.IndexType indexType) {
+ return indexType == Index.IndexType.DATA_SKIPPING_ANNOY
+ || indexType == Index.IndexType.DATA_SKIPPING_USEARCH;
+ }
+
+ private static String legacyAnnUsearchTypeName(Index.IndexType indexType) {
+ return indexType == Index.IndexType.DATA_SKIPPING_ANNOY
+ ? DATA_SKIPPING_ANNOY
+ : DATA_SKIPPING_USEARCH;
+ }
+
+ private static void rejectLegacyAnnUsearchIndexes(String tableName, Index[]
indexes) {
+ if (ArrayUtils.isEmpty(indexes)) {
+ return;
+ }
+ for (Index index : indexes) {
+ if (isLegacyAnnUsearchIndex(index.type())) {
+ throw legacyAnnUsearchWriteException(tableName, index.name(),
index.type());
+ }
+ }
+ }
+
+ private static IllegalArgumentException legacyAnnUsearchWriteException(
+ String tableName, String indexName, Index.IndexType indexType) {
+ return new IllegalArgumentException(
+ ("ClickHouse legacy index type %s is metadata-only and cannot be
written for table '%s', "
+ + "index '%s'")
+ .formatted(indexType, tableName, indexName));
+ }
+
/**
* 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
- * default). Also handles parameterized formats that some ClickHouse
versions may return from
- * {@code system.data_skipping_indices}.
+ * default). Also handles parameterized formats such as {@code set(N)},
{@code annoy(N)}, {@code
+ * usearch(distanceFunction)}, and {@code vector_similarity(...)} that some
ClickHouse versions
+ * may return from {@code system.data_skipping_indices}.
*
* @param rawType the index type string from ClickHouse metadata (e.g.
"minmax", "bloom_filter",
* "set", "set(0)", or "vector_similarity")
@@ -2567,6 +2758,10 @@ public class ClickHouseTableOperations extends
JdbcTableOperations {
return Index.IndexType.DATA_SKIPPING_NGRAMBFV1;
case DATA_SKIPPING_TOKENBFV1:
return Index.IndexType.DATA_SKIPPING_TOKENBFV1;
+ case DATA_SKIPPING_ANNOY:
+ return Index.IndexType.DATA_SKIPPING_ANNOY;
+ case DATA_SKIPPING_USEARCH:
+ return Index.IndexType.DATA_SKIPPING_USEARCH;
case DATA_SKIPPING_VECTOR_SIMILARITY:
return Index.IndexType.DATA_SKIPPING_VECTOR_SIMILARITY;
default:
@@ -2581,6 +2776,12 @@ public class ClickHouseTableOperations extends
JdbcTableOperations {
if (rawType.startsWith(DATA_SKIPPING_TOKENBFV1 + "(")) {
return Index.IndexType.DATA_SKIPPING_TOKENBFV1;
}
+ if (isParameterizedLegacyAnnUsearchType(rawType, DATA_SKIPPING_ANNOY))
{
+ return Index.IndexType.DATA_SKIPPING_ANNOY;
+ }
+ if (isParameterizedLegacyAnnUsearchType(rawType,
DATA_SKIPPING_USEARCH)) {
+ return Index.IndexType.DATA_SKIPPING_USEARCH;
+ }
if (rawType.startsWith(DATA_SKIPPING_VECTOR_SIMILARITY + "(")) {
return Index.IndexType.DATA_SKIPPING_VECTOR_SIMILARITY;
}
@@ -2588,6 +2789,10 @@ public class ClickHouseTableOperations extends
JdbcTableOperations {
}
}
+ private static boolean isParameterizedLegacyAnnUsearchType(String rawType,
String typeName) {
+ return rawType.startsWith(typeName + "(") && rawType.endsWith(")");
+ }
+
/**
* 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
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 f1705e2a3d..461308cf54 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
@@ -18,8 +18,10 @@
*/
package org.apache.gravitino.catalog.clickhouse.integration.test;
+import static
org.apache.gravitino.catalog.clickhouse.ClickHouseConstants.IndexConstants.CLICKHOUSE_TYPE_FULL;
import static
org.apache.gravitino.catalog.clickhouse.ClickHouseConstants.IndexConstants.GRANULARITY;
import static
org.apache.gravitino.catalog.clickhouse.ClickHouseConstants.IndexConstants.SET_MAX_VALUES;
+import static
org.apache.gravitino.catalog.clickhouse.ClickHouseConstants.IndexConstants.USEARCH_DISTANCE_FUNCTION;
import static
org.apache.gravitino.catalog.clickhouse.ClickHouseTablePropertiesMetadata.ENGINE;
import static
org.apache.gravitino.catalog.clickhouse.ClickHouseTablePropertiesMetadata.ENGINE.MERGETREE;
import static
org.apache.gravitino.catalog.clickhouse.ClickHouseTablePropertiesMetadata.ENGINE.REPLACINGMERGETREE;
@@ -34,7 +36,11 @@ import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import java.io.IOException;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.ResultSet;
import java.sql.SQLException;
+import java.sql.Statement;
import java.time.LocalDate;
import java.util.Arrays;
import java.util.Collections;
@@ -922,6 +928,226 @@ public class CatalogClickHouseIT extends BaseIT {
assertSetIndexMetadata(nativeLoaded, "idx_native_set", setProperties);
}
+ @Test
+ void testLoadLegacyUsearchIndexMetadata() throws Exception {
+ String legacyCatalogName =
GravitinoITUtils.genRandomName("clickhouse_legacy_catalog");
+ String legacySchemaName =
GravitinoITUtils.genRandomName("clickhouse_legacy_schema");
+ String legacyTableName =
GravitinoITUtils.genRandomName("clickhouse_legacy_usearch");
+ ClickHouseContainer legacyContainer =
+ ClickHouseContainer.builder()
+ .withImage("clickhouse/clickhouse-server:23.8.16.16")
+ .withHostName("gravitino-ci-clickhouse-legacy")
+ .withEnvVars(Map.of("CLICKHOUSE_PASSWORD",
ClickHouseContainer.PASSWORD))
+ .withExposePorts(Set.of(ClickHouseContainer.CLICKHOUSE_PORT))
+ .withNetwork(containerSuite.getNetwork())
+ .build();
+ Catalog legacyCatalog = null;
+ try {
+ legacyContainer.start();
+ legacyContainer.createDatabase(TEST_DB_NAME);
+
+ Map<String, String> catalogProperties = Maps.newHashMap();
+ String jdbcUrl = legacyContainer.getJdbcUrl(TEST_DB_NAME);
+ String baseJdbcUrl = jdbcUrl.substring(0, jdbcUrl.lastIndexOf("/"));
+ catalogProperties.put(JdbcConfig.JDBC_URL.getKey(), baseJdbcUrl);
+ catalogProperties.put(
+ JdbcConfig.JDBC_DRIVER.getKey(),
legacyContainer.getDriverClassName(TEST_DB_NAME));
+ catalogProperties.put(JdbcConfig.USERNAME.getKey(),
legacyContainer.getUsername());
+ catalogProperties.put(JdbcConfig.PASSWORD.getKey(),
legacyContainer.getPassword());
+ legacyCatalog =
+ metalake.createCatalog(
+ legacyCatalogName,
+ Catalog.Type.RELATIONAL,
+ provider,
+ "legacy ClickHouse index metadata fixture",
+ catalogProperties);
+ legacyCatalog = metalake.loadCatalog(legacyCatalogName);
+ legacyCatalog.asSchemas().createSchema(legacySchemaName, "legacy index
metadata", Map.of());
+
+ // This official 23.8 image exposes USearch but not Annoy; Annoy
metadata parsing is covered
+ // by the catalog unit tests.
+ String usearchTypeFull;
+ String legacyJdbcUrl = baseJdbcUrl +
"?custom_settings=allow_experimental_usearch_index%3D1";
+ try (Connection legacyConnection =
+ DriverManager.getConnection(
+ legacyJdbcUrl, legacyContainer.getUsername(),
legacyContainer.getPassword());
+ Statement legacyStatement = legacyConnection.createStatement()) {
+ legacyStatement.execute(
+ String.format(
+ "CREATE TABLE `%s`.`%s` ("
+ + "`id` UInt64, `usearch_embedding` Array(Float32), "
+ + "INDEX `idx_legacy_usearch` `usearch_embedding` TYPE
usearch('cosineDistance') GRANULARITY 1"
+ + ") ENGINE = MergeTree ORDER BY `id`",
+ legacySchemaName, legacyTableName));
+
+ try (ResultSet resultSet =
+ legacyStatement.executeQuery(
+ String.format(
+ "SELECT type_full FROM system.data_skipping_indices WHERE
database = '%s' "
+ + "AND table = '%s' AND name = 'idx_legacy_usearch'",
+ legacySchemaName, legacyTableName))) {
+ Assertions.assertTrue(resultSet.next(), "Expected to find the legacy
USearch index");
+ usearchTypeFull = resultSet.getString("type_full");
+ }
+ }
+ Assertions.assertEquals("usearch('cosineDistance')", usearchTypeFull);
+
+ Table loaded =
+ legacyCatalog
+ .asTableCatalog()
+ .loadTable(NameIdentifier.of(legacySchemaName, legacyTableName));
+ assertLegacyIndexMetadata(
+ loaded,
+ "idx_legacy_usearch",
+ Index.IndexType.DATA_SKIPPING_USEARCH,
+ "usearch_embedding",
+ Map.of(
+ USEARCH_DISTANCE_FUNCTION,
+ "cosineDistance",
+ CLICKHOUSE_TYPE_FULL,
+ usearchTypeFull,
+ GRANULARITY,
+ "1"));
+ } finally {
+ try {
+ if (legacyCatalog != null) {
+ try {
+ legacyCatalog.asSchemas().dropSchema(legacySchemaName, true);
+ } catch (Exception ignored) {
+ // The historical container is discarded below; cleanup failures
must not hide test
+ // errors.
+ }
+ metalake.disableCatalog(legacyCatalogName);
+ metalake.dropCatalog(legacyCatalogName, true);
+ }
+ } finally {
+ legacyContainer.close();
+ }
+ }
+ }
+
+ @Test
+ void testLoadLegacyAnnoyIndexMetadata() throws Exception {
+ String legacyCatalogName =
GravitinoITUtils.genRandomName("clickhouse_legacy_annoy_catalog");
+ String legacySchemaName =
GravitinoITUtils.genRandomName("clickhouse_legacy_annoy_schema");
+ String legacyTableName =
GravitinoITUtils.genRandomName("clickhouse_legacy_annoy_table");
+ ClickHouseContainer legacyContainer =
+ ClickHouseContainer.builder()
+ .withImage("clickhouse/clickhouse-server:24.8.14.39")
+ .withHostName("gravitino-ci-clickhouse-legacy-annoy")
+ .withEnvVars(Map.of("CLICKHOUSE_PASSWORD",
ClickHouseContainer.PASSWORD))
+ .withExposePorts(Set.of(ClickHouseContainer.CLICKHOUSE_PORT))
+ .withNetwork(containerSuite.getNetwork())
+ .build();
+ Catalog legacyCatalog = null;
+ try {
+ legacyContainer.start();
+ legacyContainer.createDatabase(TEST_DB_NAME);
+
+ Map<String, String> catalogProperties = Maps.newHashMap();
+ String jdbcUrl = legacyContainer.getJdbcUrl(TEST_DB_NAME);
+ String baseJdbcUrl = jdbcUrl.substring(0, jdbcUrl.lastIndexOf("/"));
+ catalogProperties.put(JdbcConfig.JDBC_URL.getKey(), baseJdbcUrl);
+ catalogProperties.put(
+ JdbcConfig.JDBC_DRIVER.getKey(),
legacyContainer.getDriverClassName(TEST_DB_NAME));
+ catalogProperties.put(JdbcConfig.USERNAME.getKey(),
legacyContainer.getUsername());
+ catalogProperties.put(JdbcConfig.PASSWORD.getKey(),
legacyContainer.getPassword());
+ legacyCatalog =
+ metalake.createCatalog(
+ legacyCatalogName,
+ Catalog.Type.RELATIONAL,
+ provider,
+ "legacy Annoy index metadata fixture",
+ catalogProperties);
+ legacyCatalog = metalake.loadCatalog(legacyCatalogName);
+ legacyCatalog.asSchemas().createSchema(legacySchemaName, "legacy Annoy
metadata", Map.of());
+
+ String nativeType;
+ String nativeTypeFull;
+ long nativeGranularity;
+ String legacyJdbcUrl =
+ baseJdbcUrl +
"?custom_settings=allow_experimental_vector_similarity_index%3D1";
+ try (Connection legacyConnection =
+ DriverManager.getConnection(
+ legacyJdbcUrl, legacyContainer.getUsername(),
legacyContainer.getPassword());
+ Statement legacyStatement = legacyConnection.createStatement()) {
+ // ClickHouse 24.8 retains the legacy Annoy metadata-load
compatibility path. Do not insert
+ // or search: legacy index operations are intentionally unsupported by
this contribution.
+ legacyStatement.execute(
+ String.format(
+ "CREATE TABLE `%s`.`%s` ("
+ + "`id` Int32, `embedding` Array(Float32), "
+ + "INDEX `idx_legacy_annoy` `embedding` TYPE annoy()"
+ + ") ENGINE = MergeTree ORDER BY `id`",
+ legacySchemaName, legacyTableName));
+
+ try (ResultSet resultSet =
+ legacyStatement.executeQuery(
+ String.format(
+ "SELECT type, type_full, granularity FROM
system.data_skipping_indices "
+ + "WHERE database = '%s' AND table = '%s' AND name =
'idx_legacy_annoy'",
+ legacySchemaName, legacyTableName))) {
+ Assertions.assertTrue(resultSet.next(), "Expected to find the legacy
Annoy index");
+ nativeType = resultSet.getString("type");
+ nativeTypeFull = resultSet.getString("type_full");
+ nativeGranularity = resultSet.getLong("granularity");
+ }
+ }
+
+ Assertions.assertEquals("annoy", nativeType);
+ Assertions.assertTrue(
+ StringUtils.startsWithIgnoreCase(nativeTypeFull, "annoy"),
+ "Expected ClickHouse to preserve the Annoy type expression, but got:
" + nativeTypeFull);
+ Assertions.assertTrue(nativeGranularity > 0);
+
+ Table loaded =
+ legacyCatalog
+ .asTableCatalog()
+ .loadTable(NameIdentifier.of(legacySchemaName, legacyTableName));
+ Index loadedAnnoy =
+ Arrays.stream(loaded.index())
+ .filter(index -> Objects.equals(index.name(),
"idx_legacy_annoy"))
+ .findFirst()
+ .orElseThrow(() -> new AssertionError("Missing legacy Annoy
index"));
+ Assertions.assertEquals(Index.IndexType.DATA_SKIPPING_ANNOY,
loadedAnnoy.type());
+ Assertions.assertArrayEquals(new String[][] {{"embedding"}},
loadedAnnoy.fieldNames());
+ Assertions.assertEquals(nativeTypeFull,
loadedAnnoy.properties().get(CLICKHOUSE_TYPE_FULL));
+ Assertions.assertEquals(
+ String.valueOf(nativeGranularity),
loadedAnnoy.properties().get(GRANULARITY));
+ } finally {
+ try {
+ if (legacyCatalog != null) {
+ try {
+ legacyCatalog.asSchemas().dropSchema(legacySchemaName, true);
+ } catch (Exception ignored) {
+ // The historical container is discarded below; cleanup failures
must not hide test
+ // errors.
+ }
+ metalake.disableCatalog(legacyCatalogName);
+ metalake.dropCatalog(legacyCatalogName, true);
+ }
+ } finally {
+ legacyContainer.close();
+ }
+ }
+ }
+
+ private void assertLegacyIndexMetadata(
+ Table table,
+ String indexName,
+ Index.IndexType indexType,
+ String fieldName,
+ Map<String, String> properties) {
+ Index index =
+ Arrays.stream(table.index())
+ .filter(candidate -> Objects.equals(indexName, candidate.name()))
+ .findFirst()
+ .orElseThrow(() -> new AssertionError("Missing index " +
indexName));
+ Assertions.assertEquals(indexType, index.type());
+ Assertions.assertArrayEquals(new String[][] {{fieldName}},
index.fieldNames());
+ Assertions.assertEquals(properties, index.properties());
+ }
+
private void assertSetIndexMetadata(
Table table, String indexName, Map<String, String> expectedProperties) {
Index index =
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 1a77ef0a1a..b16c407641 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
@@ -18,6 +18,12 @@
*/
package org.apache.gravitino.catalog.clickhouse.operations;
+import static
org.apache.gravitino.catalog.clickhouse.ClickHouseConstants.IndexConstants.ANNOY_DISTANCE_FUNCTION;
+import static
org.apache.gravitino.catalog.clickhouse.ClickHouseConstants.IndexConstants.ANNOY_TREES;
+import static
org.apache.gravitino.catalog.clickhouse.ClickHouseConstants.IndexConstants.CLICKHOUSE_TYPE_FULL;
+import static
org.apache.gravitino.catalog.clickhouse.ClickHouseConstants.IndexConstants.GRANULARITY;
+import static
org.apache.gravitino.catalog.clickhouse.ClickHouseConstants.IndexConstants.USEARCH_DISTANCE_FUNCTION;
+import static
org.apache.gravitino.catalog.clickhouse.ClickHouseConstants.IndexConstants.USEARCH_SCALAR_KIND;
import static
org.apache.gravitino.catalog.clickhouse.ClickHouseTablePropertiesMetadata.CLICKHOUSE_PROJECTIONS_KEY;
import static
org.apache.gravitino.catalog.clickhouse.ClickHouseUtils.getSortOrders;
@@ -27,6 +33,7 @@ import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
+import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -39,6 +46,7 @@ import
org.apache.gravitino.catalog.clickhouse.converter.ClickHouseExceptionConv
import
org.apache.gravitino.catalog.clickhouse.converter.ClickHouseTypeConverter;
import org.apache.gravitino.catalog.jdbc.JdbcColumn;
import org.apache.gravitino.catalog.jdbc.JdbcTable;
+import org.apache.gravitino.catalog.jdbc.operation.JdbcTableOperations;
import org.apache.gravitino.exceptions.GravitinoRuntimeException;
import org.apache.gravitino.exceptions.NoSuchTableException;
import org.apache.gravitino.rel.TableChange;
@@ -51,6 +59,14 @@ import
org.apache.gravitino.rel.expressions.transforms.Transforms;
import org.apache.gravitino.rel.indexes.Index;
import org.apache.gravitino.rel.indexes.Indexes;
import org.apache.gravitino.rel.types.Types;
+import org.apache.logging.log4j.Level;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.core.LoggerContext;
+import org.apache.logging.log4j.core.appender.AbstractAppender;
+import org.apache.logging.log4j.core.config.AbstractConfiguration;
+import org.apache.logging.log4j.core.config.LoggerConfig;
+import org.apache.logging.log4j.core.layout.PatternLayout;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
@@ -58,6 +74,26 @@ import org.mockito.Mockito;
public class TestClickHouseTableOperationsUnit {
+ private static final class CaptureAppender extends AbstractAppender {
+ private final List<LogEvent> events = new ArrayList<>();
+
+ CaptureAppender(String name) {
+ super(name, null, PatternLayout.createDefaultLayout(), true, null);
+ }
+
+ @Override
+ public void append(LogEvent event) {
+ events.add(event.toImmutable());
+ }
+
+ List<LogEvent> getEvents() {
+ return events;
+ }
+ }
+
+ private static final String JDBC_TABLE_OPERATIONS_LOGGER_NAME =
+ JdbcTableOperations.class.getName();
+
private static final class ExposedClickHouseTableOperations extends
ClickHouseTableOperations {
private JdbcTable table;
@@ -110,6 +146,21 @@ public class TestClickHouseTableOperationsUnit {
getSortOrders("id"));
}
+ String callGenerateCreateTableSqlWithoutSort(JdbcColumn[] columns, Index[]
indexes) {
+ return generateCreateTableSql(
+ "test_table",
+ columns,
+ "",
+ Map.of(),
+ Transforms.EMPTY_TRANSFORM,
+ Distributions.NONE,
+ indexes);
+ }
+
+ Index.IndexType callGetClickHouseIndexType(String rawType) {
+ return getClickHouseIndexType(rawType);
+ }
+
void setTable(JdbcTable table) {
this.table = table;
}
@@ -147,6 +198,63 @@ public class TestClickHouseTableOperationsUnit {
Assertions.assertTrue(exception.getMessage().contains("ClickHouse does not
support varchar"));
}
+ @Test
+ void
testLegacyAnnUsearchTypesAreDistinctAndParameterizedFormsAreRecognized() {
+ ExposedClickHouseTableOperations ops = newOps();
+
+ Assertions.assertEquals(
+ Index.IndexType.DATA_SKIPPING_ANNOY,
ops.callGetClickHouseIndexType("annoy"));
+ Assertions.assertEquals(
+ Index.IndexType.DATA_SKIPPING_ANNOY,
ops.callGetClickHouseIndexType("annoy(100)"));
+ Assertions.assertEquals(
+ Index.IndexType.DATA_SKIPPING_USEARCH,
ops.callGetClickHouseIndexType("usearch"));
+ Assertions.assertEquals(
+ Index.IndexType.DATA_SKIPPING_USEARCH,
+ ops.callGetClickHouseIndexType("usearch(cosineDistance)"));
+ Assertions.assertEquals(
+ Index.IndexType.DATA_SKIPPING_VECTOR_SIMILARITY,
+ ops.callGetClickHouseIndexType("vector_similarity"));
+ Assertions.assertThrows(
+ IllegalArgumentException.class, () ->
ops.callGetClickHouseIndexType("unknown_index"));
+ }
+
+ @Test
+ void testCreateAndAlterRejectLegacyAnnUsearchIndexesWithContext() {
+ JdbcColumn[] columns =
+ new JdbcColumn[] {
+ JdbcColumn.builder()
+ .withName("id")
+ .withType(Types.IntegerType.get())
+ .withNullable(false)
+ .build()
+ };
+
+ for (Index.IndexType indexType :
+ List.of(Index.IndexType.DATA_SKIPPING_ANNOY,
Index.IndexType.DATA_SKIPPING_USEARCH)) {
+ Index index = Indexes.of(indexType, "idx_legacy", new String[][]
{{"id"}});
+ IllegalArgumentException createException =
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () -> newOps().callGenerateCreateTableSql(columns, Map.of(), new
Index[] {index}));
+ assertLegacyWriteError(createException, indexType);
+
+ IllegalArgumentException createWithoutSortException =
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () -> newOps().callGenerateCreateTableSqlWithoutSort(columns,
new Index[] {index}));
+ assertLegacyWriteError(createWithoutSortException, indexType);
+
+ IllegalArgumentException alterException =
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ newAlterOps(Map.of())
+ .callGenerateAlterTableSql(
+ TableChange.addIndex(indexType, "idx_legacy", new
String[][] {{"id"}})));
+ assertLegacyWriteError(alterException, indexType);
+ }
+ }
+
@Test
void testToPartitionExpressionSupportsStartFunctions() {
Assertions.assertEquals(
@@ -228,6 +336,54 @@ public class TestClickHouseTableOperationsUnit {
return ops;
}
+ private static List<Index> getIndexesWithoutWarnings(
+ ExposedClickHouseTableOperations ops,
+ Connection connection,
+ String databaseName,
+ String tableName)
+ throws Exception {
+ return getIndexesExpectingWarnings(ops, connection, databaseName,
tableName, 0);
+ }
+
+ private static List<Index> getIndexesExpectingWarnings(
+ ExposedClickHouseTableOperations ops,
+ Connection connection,
+ String databaseName,
+ String tableName,
+ int expectedWarningCount)
+ throws Exception {
+ LoggerContext loggerContext =
+ (LoggerContext)
+
LogManager.getContext(ClickHouseTableOperations.class.getClassLoader(), false);
+ AbstractConfiguration configuration = (AbstractConfiguration)
loggerContext.getConfiguration();
+ CaptureAppender warningCapture = new
CaptureAppender("clickhouseMetadataWarningCapture");
+ warningCapture.start();
+ configuration.addAppender(warningCapture);
+ LoggerConfig loggerConfig =
+ new LoggerConfig(JDBC_TABLE_OPERATIONS_LOGGER_NAME, Level.WARN, false);
+ loggerConfig.addAppender(warningCapture, Level.WARN, null);
+ configuration.addLogger(JDBC_TABLE_OPERATIONS_LOGGER_NAME, loggerConfig);
+ loggerContext.updateLoggers();
+
+ try {
+ List<Index> indexes = ops.callGetIndexes(connection, databaseName,
tableName);
+ Assertions.assertEquals(
+ expectedWarningCount,
+ warningCapture.getEvents().size(),
+ "Unexpected warning count while loading legacy Annoy and USearch
metadata");
+ Assertions.assertTrue(
+ warningCapture.getEvents().stream()
+ .allMatch(event -> Level.WARN.equals(event.getLevel())),
+ "Legacy Annoy and USearch metadata warnings should use WARN level");
+ return indexes;
+ } finally {
+ configuration.removeLogger(JDBC_TABLE_OPERATIONS_LOGGER_NAME);
+ warningCapture.stop();
+ configuration.removeAppender(warningCapture.getName());
+ loggerContext.updateLoggers();
+ }
+ }
+
private ExposedClickHouseTableOperations newAlterOps(Map<String, String>
properties) {
ExposedClickHouseTableOperations ops = newOps();
JdbcColumn idColumn =
@@ -1435,10 +1591,6 @@ public class TestClickHouseTableOperationsUnit {
Assertions.assertEquals(
Index.IndexType.DATA_SKIPPING_VECTOR_SIMILARITY,
ops.getClickHouseIndexType("vector_similarity('hnsw', 'L2Distance',
3)"));
- Assertions.assertThrows(
- IllegalArgumentException.class, () ->
ops.getClickHouseIndexType("annoy"));
- Assertions.assertThrows(
- IllegalArgumentException.class, () ->
ops.getClickHouseIndexType("usearch"));
}
@Test
@@ -1699,6 +1851,271 @@ public class TestClickHouseTableOperationsUnit {
indexes.get(0).properties());
}
+ @Test
+ void testGetIndexesPreservesLegacyAnnUsearchMetadata() throws Exception {
+ ExposedClickHouseTableOperations ops = newOps();
+ ResultSet primaryKeyRs = Mockito.mock(ResultSet.class);
+ ResultSet secondaryRs = Mockito.mock(ResultSet.class);
+ PreparedStatement primaryKeyStmt = Mockito.mock(PreparedStatement.class);
+ PreparedStatement secondaryStmt = Mockito.mock(PreparedStatement.class);
+
+ Mockito.when(primaryKeyRs.next()).thenReturn(false);
+ Mockito.when(primaryKeyStmt.executeQuery()).thenReturn(primaryKeyRs);
+ Mockito.when(secondaryStmt.executeQuery()).thenReturn(secondaryRs);
+ Mockito.when(secondaryRs.next()).thenReturn(true, true, true, true, true,
true, true, false);
+ Mockito.when(secondaryRs.getString("name"))
+ .thenReturn(
+ "idx_annoy",
+ "idx_usearch",
+ "idx_annoy_without_parameters",
+ "idx_annoy_distance_only",
+ "idx_annoy_distance_and_trees",
+ "idx_usearch_without_parameters",
+ "idx_usearch_distance_and_scalar_kind");
+ Mockito.when(secondaryRs.getString("type"))
+ .thenReturn("annoy", "usearch", "annoy", "annoy", "annoy", "usearch",
"usearch");
+ // The native metadata DDL in ClickHouse issue #41729 uses annoy(100) with
GRANULARITY 1.
+ // This is a parser fixture only; it does not validate a successful
system-table read path.
+ Mockito.when(secondaryRs.getString("type_full"))
+ .thenReturn(
+ "annoy(100)",
+ "usearch('cosineDistance')",
+ "annoy()",
+ "annoy('L2Distance')",
+ "annoy('cosineDistance', 100)",
+ "usearch()",
+ "usearch('L2Distance', 'f16')");
+ Mockito.when(secondaryRs.getString("expr"))
+ .thenReturn(
+ "embedding",
+ "tuple(embedding)",
+ "embedding",
+ "embedding",
+ "embedding",
+ "embedding",
+ "embedding");
+ Mockito.when(secondaryRs.getLong("granularity")).thenReturn(1L, 1L, 1L,
1L, 1L, 1L, 1L);
+
+ Connection connection = Mockito.mock(Connection.class);
+ Mockito.when(connection.prepareStatement(Mockito.anyString()))
+ .thenReturn(primaryKeyStmt)
+ .thenReturn(secondaryStmt);
+
+ List<Index> indexes = getIndexesWithoutWarnings(ops, connection, "db",
"tbl");
+
+ Assertions.assertEquals(7, indexes.size());
+ Index annoy = indexes.get(0);
+ Assertions.assertEquals(Index.IndexType.DATA_SKIPPING_ANNOY, annoy.type());
+ Assertions.assertEquals("idx_annoy", annoy.name());
+ Assertions.assertArrayEquals(new String[][] {{"embedding"}},
annoy.fieldNames());
+ Assertions.assertEquals(
+ Map.of(ANNOY_TREES, "100", CLICKHOUSE_TYPE_FULL, "annoy(100)",
GRANULARITY, "1"),
+ annoy.properties());
+
+ Index usearch = indexes.get(1);
+ Assertions.assertEquals(Index.IndexType.DATA_SKIPPING_USEARCH,
usearch.type());
+ Assertions.assertEquals("idx_usearch", usearch.name());
+ Assertions.assertArrayEquals(new String[][] {{"embedding"}},
usearch.fieldNames());
+ Assertions.assertEquals(
+ Map.of(
+ USEARCH_DISTANCE_FUNCTION,
+ "cosineDistance",
+ CLICKHOUSE_TYPE_FULL,
+ "usearch('cosineDistance')",
+ GRANULARITY,
+ "1"),
+ usearch.properties());
+
+ Index annoyWithoutParameters = indexes.get(2);
+ Assertions.assertEquals(Index.IndexType.DATA_SKIPPING_ANNOY,
annoyWithoutParameters.type());
+ Assertions.assertEquals("idx_annoy_without_parameters",
annoyWithoutParameters.name());
+ Assertions.assertArrayEquals(
+ new String[][] {{"embedding"}}, annoyWithoutParameters.fieldNames());
+ Assertions.assertEquals(
+ Map.of(CLICKHOUSE_TYPE_FULL, "annoy()", GRANULARITY, "1"),
+ annoyWithoutParameters.properties());
+
+ Index annoyDistanceOnly = indexes.get(3);
+ Assertions.assertEquals(Index.IndexType.DATA_SKIPPING_ANNOY,
annoyDistanceOnly.type());
+ Assertions.assertEquals(
+ Map.of(
+ ANNOY_DISTANCE_FUNCTION,
+ "L2Distance",
+ CLICKHOUSE_TYPE_FULL,
+ "annoy('L2Distance')",
+ GRANULARITY,
+ "1"),
+ annoyDistanceOnly.properties());
+
+ Index annoyDistanceAndTrees = indexes.get(4);
+ Assertions.assertEquals(Index.IndexType.DATA_SKIPPING_ANNOY,
annoyDistanceAndTrees.type());
+ Assertions.assertEquals(
+ Map.of(
+ ANNOY_DISTANCE_FUNCTION,
+ "cosineDistance",
+ ANNOY_TREES,
+ "100",
+ CLICKHOUSE_TYPE_FULL,
+ "annoy('cosineDistance', 100)",
+ GRANULARITY,
+ "1"),
+ annoyDistanceAndTrees.properties());
+
+ Index usearchWithoutParameters = indexes.get(5);
+ Assertions.assertEquals(Index.IndexType.DATA_SKIPPING_USEARCH,
usearchWithoutParameters.type());
+ Assertions.assertEquals(
+ Map.of(CLICKHOUSE_TYPE_FULL, "usearch()", GRANULARITY, "1"),
+ usearchWithoutParameters.properties());
+
+ Index usearchDistanceAndScalarKind = indexes.get(6);
+ Assertions.assertEquals(
+ Index.IndexType.DATA_SKIPPING_USEARCH,
usearchDistanceAndScalarKind.type());
+ Assertions.assertEquals(
+ Map.of(
+ USEARCH_DISTANCE_FUNCTION,
+ "L2Distance",
+ USEARCH_SCALAR_KIND,
+ "f16",
+ CLICKHOUSE_TYPE_FULL,
+ "usearch('L2Distance', 'f16')",
+ GRANULARITY,
+ "1"),
+ usearchDistanceAndScalarKind.properties());
+ }
+
+ @Test
+ void testGetIndexesWarnsAndPreservesMalformedLegacyAnnUsearchParameters()
throws Exception {
+ ExposedClickHouseTableOperations ops = newOps();
+ ResultSet primaryKeyRs = Mockito.mock(ResultSet.class);
+ ResultSet secondaryRs = Mockito.mock(ResultSet.class);
+ PreparedStatement primaryKeyStmt = Mockito.mock(PreparedStatement.class);
+ PreparedStatement secondaryStmt = Mockito.mock(PreparedStatement.class);
+
+ String[] typeFullExpressions = {
+ "annoy('bogus')",
+ "annoy('L2Distance', 'not-a-number')",
+ "annoy('L2Distance', 10, 20)",
+ "usearch('bogus')",
+ "usearch('L2Distance', 'invalid-kind')",
+ "usearch('L2Distance', 'f16', 'extra')"
+ };
+ String[] indexNames = {
+ "idx_annoy_invalid_distance",
+ "idx_annoy_invalid_trees",
+ "idx_annoy_extra_parameters",
+ "idx_usearch_invalid_distance",
+ "idx_usearch_invalid_scalar_kind",
+ "idx_usearch_extra_parameters"
+ };
+
+ Mockito.when(primaryKeyRs.next()).thenReturn(false);
+ Mockito.when(primaryKeyStmt.executeQuery()).thenReturn(primaryKeyRs);
+ Mockito.when(secondaryStmt.executeQuery()).thenReturn(secondaryRs);
+ Mockito.when(secondaryRs.next()).thenReturn(true, true, true, true, true,
true, false);
+ Mockito.when(secondaryRs.getString("name"))
+ .thenReturn(
+ indexNames[0],
+ indexNames[1],
+ indexNames[2],
+ indexNames[3],
+ indexNames[4],
+ indexNames[5]);
+ Mockito.when(secondaryRs.getString("type"))
+ .thenReturn("annoy", "annoy", "annoy", "usearch", "usearch",
"usearch");
+ Mockito.when(secondaryRs.getString("type_full"))
+ .thenReturn(
+ typeFullExpressions[0],
+ typeFullExpressions[1],
+ typeFullExpressions[2],
+ typeFullExpressions[3],
+ typeFullExpressions[4],
+ typeFullExpressions[5]);
+ Mockito.when(secondaryRs.getString("expr"))
+ .thenReturn("embedding", "embedding", "embedding", "embedding",
"embedding", "embedding");
+ Mockito.when(secondaryRs.getLong("granularity")).thenReturn(1L, 1L, 1L,
1L, 1L, 1L);
+
+ Connection connection = Mockito.mock(Connection.class);
+ Mockito.when(connection.prepareStatement(Mockito.anyString()))
+ .thenReturn(primaryKeyStmt)
+ .thenReturn(secondaryStmt);
+
+ List<Index> indexes =
+ getIndexesExpectingWarnings(ops, connection, "db", "tbl",
typeFullExpressions.length);
+
+ Assertions.assertEquals(typeFullExpressions.length, indexes.size());
+ for (int i = 0; i < typeFullExpressions.length; i++) {
+ Assertions.assertEquals(indexNames[i], indexes.get(i).name());
+ Assertions.assertEquals(
+ Map.of(CLICKHOUSE_TYPE_FULL, typeFullExpressions[i], GRANULARITY,
"1"),
+ indexes.get(i).properties());
+ }
+ }
+
+ @Test
+ void testGetIndexesFallsBackAndPreservesLegacyAnnUsearchTypes() 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, true, true, false);
+ Mockito.when(legacySecondaryRs.getString("name"))
+ .thenReturn("idx_legacy_annoy", "idx_legacy_usearch", "idx_unknown");
+ Mockito.when(legacySecondaryRs.getString("type"))
+ .thenReturn("annoy", "usearch", "vector_similarity");
+ Mockito.when(legacySecondaryRs.getString("expr"))
+ .thenReturn("embedding", "embedding", "embedding");
+ Mockito.when(legacySecondaryRs.getLong("granularity")).thenReturn(1L, 1L,
1L);
+
+ Connection connection = Mockito.mock(Connection.class);
+ Mockito.when(connection.prepareStatement(Mockito.anyString()))
+ .thenReturn(primaryKeyStmt)
+ .thenReturn(modernSecondaryStmt)
+ .thenReturn(legacySecondaryStmt);
+
+ List<Index> indexes = getIndexesExpectingWarnings(ops, connection, "db",
"tbl", 2);
+
+ Assertions.assertEquals(2, indexes.size());
+ Assertions.assertEquals(
+ Map.of(CLICKHOUSE_TYPE_FULL, "annoy", GRANULARITY, "1"),
indexes.get(0).properties());
+ Assertions.assertEquals(Index.IndexType.DATA_SKIPPING_USEARCH,
indexes.get(1).type());
+ Assertions.assertArrayEquals(new String[][] {{"embedding"}},
indexes.get(1).fieldNames());
+ Assertions.assertEquals(
+ Map.of(CLICKHOUSE_TYPE_FULL, "usearch", GRANULARITY, "1"),
indexes.get(1).properties());
+ }
+
+ @Test
+ void testGetIndexesReportsUnrepresentableLegacyExpressionByOmittingIt()
throws Exception {
+ ExposedClickHouseTableOperations ops = newOps();
+ ResultSet primaryKeyRs = Mockito.mock(ResultSet.class);
+ ResultSet secondaryRs = Mockito.mock(ResultSet.class);
+ PreparedStatement primaryKeyStmt = Mockito.mock(PreparedStatement.class);
+ PreparedStatement secondaryStmt = Mockito.mock(PreparedStatement.class);
+
+ Mockito.when(primaryKeyRs.next()).thenReturn(false);
+ Mockito.when(primaryKeyStmt.executeQuery()).thenReturn(primaryKeyRs);
+ Mockito.when(secondaryStmt.executeQuery()).thenReturn(secondaryRs);
+ Mockito.when(secondaryRs.next()).thenReturn(true, false);
+ Mockito.when(secondaryRs.getString("name")).thenReturn("idx_annoy_expr");
+ Mockito.when(secondaryRs.getString("type")).thenReturn("annoy");
+ Mockito.when(secondaryRs.getString("type_full")).thenReturn("annoy(100)");
+ Mockito.when(secondaryRs.getString("expr")).thenReturn("lower(embedding)");
+
+ Connection connection = Mockito.mock(Connection.class);
+ Mockito.when(connection.prepareStatement(Mockito.anyString()))
+ .thenReturn(primaryKeyStmt)
+ .thenReturn(secondaryStmt);
+
+ Assertions.assertTrue(ops.callGetIndexes(connection, "db",
"tbl").isEmpty());
+ }
+
@Test
void testGetIndexesFallsBackAndReadsLegacySetParameters() throws Exception {
ExposedClickHouseTableOperations ops = newOps();
@@ -2411,4 +2828,12 @@ public class TestClickHouseTableOperationsUnit {
return Assertions.assertThrows(
IllegalArgumentException.class, () ->
getIndexesForSetMetadata(typeFull, expression));
}
+
+ private static void assertLegacyWriteError(
+ IllegalArgumentException exception, Index.IndexType indexType) {
+ Assertions.assertTrue(exception.getMessage().contains("test_table"));
+ Assertions.assertTrue(exception.getMessage().contains("idx_legacy"));
+ Assertions.assertTrue(exception.getMessage().contains(indexType.name()));
+ Assertions.assertTrue(exception.getMessage().contains("metadata-only"));
+ }
}
diff --git a/clients/client-python/gravitino/api/rel/indexes/index.py
b/clients/client-python/gravitino/api/rel/indexes/index.py
index d55929a040..0fe335acc2 100644
--- a/clients/client-python/gravitino/api/rel/indexes/index.py
+++ b/clients/client-python/gravitino/api/rel/indexes/index.py
@@ -32,6 +32,8 @@ class Index(ABC):
"""The enum IndexType defines the type of the index.
Catalogs may support generic key types and connector-specific index
types.
+ `DATA_SKIPPING_ANNOY` and `DATA_SKIPPING_USEARCH` are exposed for
reading legacy
+ ClickHouse index metadata only.
"""
PRIMARY_KEY = "PRIMARY_KEY"
@@ -50,6 +52,12 @@ class Index(ABC):
columns that are not designated as the primary key. The uniqueness
constraint imposed by
UNIQUE KEY helps in avoiding redundancy and ensuring data accuracy in
the database."""
+ DATA_SKIPPING_ANNOY = "DATA_SKIPPING_ANNOY"
+ """Legacy ClickHouse Annoy index metadata. This type is
metadata-only."""
+
+ DATA_SKIPPING_USEARCH = "DATA_SKIPPING_USEARCH"
+ """Legacy ClickHouse USearch index metadata. This type is
metadata-only."""
+
DATA_SKIPPING_VECTOR_SIMILARITY = "DATA_SKIPPING_VECTOR_SIMILARITY"
"""ClickHouse vector similarity data skipping index."""
diff --git
a/clients/client-python/gravitino/dto/rel/indexes/json_serdes/index_serdes.py
b/clients/client-python/gravitino/dto/rel/indexes/json_serdes/index_serdes.py
index eeca69bca2..608561c835 100644
---
a/clients/client-python/gravitino/dto/rel/indexes/json_serdes/index_serdes.py
+++
b/clients/client-python/gravitino/dto/rel/indexes/json_serdes/index_serdes.py
@@ -25,14 +25,17 @@ from gravitino.utils.serdes import SerdesUtilsBase
class IndexSerdes(SerdesUtilsBase, JsonSerializable[Index]):
+ INDEX_PROPERTIES = "properties"
+
@classmethod
def serialize(cls, value: Index) -> dict[str, Any]:
result: dict[str, Any] = {cls.INDEX_TYPE: value.type().name.upper()}
if value.name() is not None:
result[cls.INDEX_NAME] = value.name()
result[cls.INDEX_FIELD_NAMES] = value.field_names()
- if value.properties():
- result["properties"] = value.properties()
+ properties = value.properties()
+ if properties:
+ result[cls.INDEX_PROPERTIES] = properties
return result
@@ -56,5 +59,5 @@ class IndexSerdes(SerdesUtilsBase, JsonSerializable[Index]):
index_type,
data.get(cls.INDEX_NAME),
data[cls.INDEX_FIELD_NAMES],
- data.get("properties"),
+ data.get(cls.INDEX_PROPERTIES),
)
diff --git a/clients/client-python/tests/unittests/dto/rel/test_index_serdes.py
b/clients/client-python/tests/unittests/dto/rel/test_index_serdes.py
index 18b960b87c..99247713ac 100644
--- a/clients/client-python/tests/unittests/dto/rel/test_index_serdes.py
+++ b/clients/client-python/tests/unittests/dto/rel/test_index_serdes.py
@@ -38,6 +38,21 @@ class MockDataClass(DataClassJsonMixin):
class TestIndexSerdes(unittest.TestCase):
+ def test_index_serdes_without_properties_override(self):
+ class IndexWithoutProperties(Index):
+ def type(self) -> Index.IndexType:
+ return Index.IndexType.PRIMARY_KEY
+
+ def name(self) -> str:
+ return "PRIMARY"
+
+ def field_names(self) -> list[list[str]]:
+ return [["id"]]
+
+ index = IndexWithoutProperties()
+ self.assertEqual({}, index.properties())
+ self.assertNotIn("properties", IndexSerdes.serialize(index))
+
def test_index_serdes_invalid_json(self):
invalid_json_string = [
'{"indexes": [{}]}',
@@ -117,6 +132,51 @@ class TestIndexSerdes(unittest.TestCase):
serialized_dict = json.loads(mock_data_class.to_json())
self.assertDictEqual(json_dict, serialized_dict)
+ def test_index_serdes_legacy_clickhouse_metadata(self):
+ annoy_properties = {
+ "annoy_trees": "100",
+ "clickhouse_type_full": "annoy(100)",
+ "granularity": "1",
+ }
+ usearch_properties = {
+ "clickhouse_type_full": "usearch('cosineDistance')",
+ "granularity": "1",
+ "usearch_distance_function": "cosineDistance",
+ }
+
+ annoy = IndexSerdes.deserialize(
+ {
+ "indexType": "data_skipping_annoy",
+ "name": "idx_annoy",
+ "fieldNames": [["embedding"]],
+ "properties": annoy_properties,
+ }
+ )
+ usearch = IndexSerdes.deserialize(
+ {
+ "indexType": "data_skipping_usearch",
+ "name": "idx_usearch",
+ "fieldNames": [["embedding"]],
+ "properties": usearch_properties,
+ }
+ )
+
+ self.assertEqual(Index.IndexType.DATA_SKIPPING_ANNOY, annoy.type())
+ self.assertEqual(annoy_properties, annoy.properties())
+ self.assertEqual(Index.IndexType.DATA_SKIPPING_USEARCH, usearch.type())
+ self.assertEqual(usearch_properties, usearch.properties())
+
+ serialized_json = MockDataClass([annoy, usearch]).to_json()
+ serialized = json.loads(serialized_json)
+ self.assertEqual(annoy_properties,
serialized["indexes"][0]["properties"])
+ self.assertEqual(usearch_properties,
serialized["indexes"][1]["properties"])
+
+ round_tripped = MockDataClass.from_json(serialized_json).indexes
+ self.assertEqual(Index.IndexType.DATA_SKIPPING_ANNOY,
round_tripped[0].type())
+ self.assertEqual(annoy_properties, round_tripped[0].properties())
+ self.assertEqual(Index.IndexType.DATA_SKIPPING_USEARCH,
round_tripped[1].type())
+ self.assertEqual(usearch_properties, round_tripped[1].properties())
+
def test_vector_similarity_index_properties_round_trip(self):
json_string = """
{
diff --git a/common/src/test/java/org/apache/gravitino/json/TestJsonUtils.java
b/common/src/test/java/org/apache/gravitino/json/TestJsonUtils.java
index 4d836e4bf6..dd9dca0026 100644
--- a/common/src/test/java/org/apache/gravitino/json/TestJsonUtils.java
+++ b/common/src/test/java/org/apache/gravitino/json/TestJsonUtils.java
@@ -496,6 +496,20 @@ public class TestJsonUtils {
objectMapper.readValue(expected, IndexDTO.class),
objectMapper.readValue(jsonValue, IndexDTO.class));
+ Index legacyIndex =
+ IndexDTO.builder()
+ .withIndexType(Index.IndexType.DATA_SKIPPING_USEARCH)
+ .withName("idx_legacy_usearch")
+ .withFieldNames(new String[][] {{"embedding"}})
+ .withProperties(Map.of("clickhouse_type_full",
"usearch(cosineDistance)"))
+ .build();
+ jsonValue = JsonUtils.objectMapper().writeValueAsString(legacyIndex);
+ Index legacyRoundTrip = objectMapper.readValue(jsonValue, IndexDTO.class);
+ Assertions.assertEquals(Index.IndexType.DATA_SKIPPING_USEARCH,
legacyRoundTrip.type());
+ Assertions.assertEquals("idx_legacy_usearch", legacyRoundTrip.name());
+ Assertions.assertEquals(
+ Map.of("clickhouse_type_full", "usearch(cosineDistance)"),
legacyRoundTrip.properties());
+
Index idx2 =
IndexDTO.builder()
.withIndexType(Index.IndexType.PRIMARY_KEY)
diff --git a/docs/jdbc-clickhouse-catalog.md b/docs/jdbc-clickhouse-catalog.md
index ddab8ff151..370ba3758a 100644
--- a/docs/jdbc-clickhouse-catalog.md
+++ b/docs/jdbc-clickhouse-catalog.md
@@ -178,7 +178,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; `PARTITION BY` supports single-column identity and the function
expressions listed below. Other engines reject `ORDER BY`/`PARTITION BY`.
|
-| Indexes | Primary key; data-skipping indexes
`DATA_SKIPPING_MINMAX`, `DATA_SKIPPING_BLOOM_FILTER`, `DATA_SKIPPING_SET`,
`DATA_SKIPPING_NGRAMBFV1`, `DATA_SKIPPING_TOKENBFV1`, and
`DATA_SKIPPING_VECTOR_SIMILARITY` (configurable granularity via
`Index.properties()`).
[...]
+| Indexes | Primary key; data-skipping indexes
`DATA_SKIPPING_MINMAX`, `DATA_SKIPPING_BLOOM_FILTER`, `DATA_SKIPPING_SET`,
`DATA_SKIPPING_NGRAMBFV1`, `DATA_SKIPPING_TOKENBFV1`, and
`DATA_SKIPPING_VECTOR_SIMILARITY` (configurable granularity via
`Index.properties()`). Legacy `DATA_SKIPPING_ANNOY` and `DATA_SKIPPING_USEARCH`
definitions can be read as metadata only.
[...]
| Distribution | Gravitino enforces `Distributions.NONE`; no custom
distribution strategies.
|
| Column defaults | Supported.
|
| Unsupported | Engine and connector-owned property changes after
creation; mixing table setting changes with schema changes; auto-increment
columns.
|
@@ -303,16 +303,24 @@ The `engine_parameters` property applies to
`ReplacingMergeTree`, `SummingMergeT
When adding a data-skipping index to an existing table through Gravitino's
`ALTER TABLE ADD INDEX`, existing data parts are not synchronously backfilled,
and Gravitino does not issue a materialization command. To materialize the
index on existing data, run `ALTER TABLE <table> MATERIALIZE INDEX
<index_name>` directly in ClickHouse. This can take time, especially for vector
indexes; ClickHouse may also materialize indexes during later background
merges, depending on its settings.
+ - Legacy `DATA_SKIPPING_ANNOY` and `DATA_SKIPPING_USEARCH` metadata read
from ClickHouse versions that expose these definitions. Their reported type
expression is retained in `clickhouse_type_full`.
+ - Annoy parameters are exposed as `annoy_distance_function` (`L2Distance` or
`cosineDistance`) and `annoy_trees`. USearch parameters are exposed as
`usearch_distance_function` (`L2Distance` or `cosineDistance`) and
`usearch_scalar_kind` (`f64`, `f32`, `f16`, or `i8`). Only parameters present
in the reported expression are materialized; omitted parameters are not
inferred. Empty parameter lists and bare legacy type names are valid metadata
forms.
+ - These index types are metadata-only: Gravitino rejects CREATE TABLE
definitions, ALTER TABLE ADD INDEX requests, and table recreation that use
them. They do not provide insert or vector-search support. Gravitino does not
automatically migrate them to `vector_similarity`; any migration must be
explicit.
+
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.
+ The legacy fallback query may return only the bare `annoy` or `usearch` type
name. Gravitino preserves that reported name without a parsing warning;
parameter properties remain absent because the server did not expose them.
+
For `DATA_SKIPPING_VECTOR_SIMILARITY`, Gravitino restores parameters from
the legacy `type` value when present. If the legacy value is bare and cannot
provide the required parameters, the index is skipped with a warning.
ClickHouse 26.4 and later can create native `vector_similarity` indexes with
`dotProduct`. Gravitino skips native vector indexes with unsupported parameter
values, including `dotProduct`, and logs a warning when loading the table.
Skipped indexes are absent from the returned table metadata and will not be
recreated from it. Creating or adding a `dotProduct` vector index through
Gravitino remains unsupported.
ClickHouse data-skipping indexes whose field expressions cannot be
represented as Gravitino field names, such as `lower(name)` or `name + 1`, are
skipped with a warning when the table is loaded and are not recreated. Direct
column references and tuples containing only column references remain supported.
+ ClickHouse legacy Annoy and USearch indexes with unrepresentable field
expressions are also omitted with an explicit warning. The Gravitino index API
does not represent arbitrary index expressions.
+
### Partitioning, Sorting, and Distribution
- `ORDER BY`: required for MergeTree-family engines and only columns identity
are supported;
diff --git a/docs/open-api/indexes.yaml b/docs/open-api/indexes.yaml
index cbcdbde96a..894761b776 100644
--- a/docs/open-api/indexes.yaml
+++ b/docs/open-api/indexes.yaml
@@ -40,6 +40,8 @@ components:
- "data_skipping_set"
- "data_skipping_ngrambfv1"
- "data_skipping_tokenbfv1"
+ - "data_skipping_annoy"
+ - "data_skipping_usearch"
- "data_skipping_vector_similarity"
name:
type: string