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 ebdaf30e02 [#11935] feat(clickhouse): add vector similarity index
support (#13430)
ebdaf30e02 is described below
commit ebdaf30e021ef351fc20a794775265ee6dded36f
Author: StormSpirit <[email protected]>
AuthorDate: Thu Oct 1 10:58:18 2026 +0800
[#11935] feat(clickhouse): add vector similarity index support (#13430)
### What changes were proposed in this pull request?
Add the public `DATA_SKIPPING_VECTOR_SIMILARITY` index type and
ClickHouse support for discovering, creating, altering, and reloading
`vector_similarity` indexes with validated HNSW properties and
normalized defaults. Add the Python enum and preserve index properties
through JSON round-trip so the Python client can deserialize
vector-index metadata for tables whose other index types it supports.
When loading a native vector index with an unsupported algorithm,
distance function, or quantization, skip that index with a warning while
preserving the rest of the table metadata; malformed supported metadata
still fails. Update the ClickHouse catalog guide to describe these
behaviors.
### Why are the changes needed?
Expose ClickHouse 25.8+ vector similarity indexes through Gravitino's
relational index API while preserving supported fields, parameters, and
effective granularity on reload. Without the Python enum, a response
containing the new index type prevents Python `load_table` from
returning the table. ClickHouse 26.4 added native dotProduct vector
indexes; since creation through Gravitino remains out of scope, their
metadata must not prevent the whole table from loading.
Fix: #11935
### Does this PR introduce _any_ user-facing change?
Adds `Index.IndexType.DATA_SKIPPING_VECTOR_SIMILARITY` for ClickHouse
25.8 or later, with `L2Distance` and `cosineDistance`, HNSW dimensions,
optional quantization and construction parameters, and a default
granularity of 100000000. The Python client recognizes this type and
preserves non-empty index properties. Native dotProduct indexes on
ClickHouse 26.4 or later are omitted from loaded Gravitino metadata with
a warning, so recreating a table from that metadata will omit those
indexes; CREATE TABLE and ALTER TABLE ADD INDEX through Gravitino
continue to reject dotProduct.
### How was this patch tested?
- Before the follow-up, `./gradlew rat`, Spotless checks for `common`,
`integration-test-common`, and `catalog-jdbc-clickhouse`, `./gradlew
:common:test --tests 'org.apache.gravitino.json.TestJsonUtils'
-PskipITs` (13 tests), `./gradlew
:catalogs-contrib:catalog-jdbc-clickhouse:build -x test`, and `./gradlew
:docs:build` passed.
- Before the follow-up, `./gradlew
:catalogs-contrib:catalog-jdbc-clickhouse:test -PskipITs
-PskipDockerTests=true` passed 135 tests, and the same task with
`-PskipDockerTests=false` passed 179 tests; on ClickHouse 25.8.14.17 the
full `CatalogClickHouseIT` passed 63 tests and the full
`CatalogClickHouseClusterIT` passed 21 tests.
- On the synchronized follow-up code, the Python `TestIndexSerdes` and
`TestRelationalCatalog` suites passed 38 tests on Python 3.12.12; Black
and Ruff checks passed for the changed Python files.
- On the synchronized follow-up code, `./gradlew
:catalogs-contrib:catalog-jdbc-clickhouse:spotlessCheck
:catalogs-contrib:catalog-jdbc-clickhouse:test --tests
'org.apache.gravitino.catalog.clickhouse.operations.TestClickHouseTableOperationsUnit'
-PskipITs` passed 72 tests.
- Before synchronizing with newer main, the follow-up code with
`GRAVITINO_CLICKHOUSE_TEST_IMAGE=clickhouse:25.8.14.17` passed the
targeted `CatalogClickHouseIT.testVectorSimilarityIndex` once and the
full `CatalogClickHouseClusterIT` with 21 tests; neither run skipped
tests.
- Before synchronizing with newer main,
`CatalogClickHouseIT.testLoadTableWithUnsupportedNativeDotProductVectorIndex`
executed and passed once on ClickHouse 26.4.2.10 with HTTP compression
disabled in a temporary local test image; the log confirmed the warning
for the skipped dotProduct index. The repository's JDBC 0.7.1 driver
failed during container health checking with the default 26.4 HTTP
compression setting, before the test body. CI uses ClickHouse 25.8, so
the 26.4-only test is skipped there; this PR does not fix the
default-compression JDBC incompatibility. The main synchronization did
not change the vector-index production path, so these Docker suites were
not repeated on the synchronized code.
---------
Signed-off-by: jiangxt2 <[email protected]>
---
.github/workflows/contrib-catalog-test.yml | 2 +
.../org/apache/gravitino/rel/indexes/Index.java | 9 +-
.../catalog/clickhouse/ClickHouseConstants.java | 22 +
.../operations/ClickHouseTableOperations.java | 414 ++++++++++++++++++-
.../test/CatalogClickHouseClusterIT.java | 85 ++++
.../integration/test/CatalogClickHouseIT.java | 164 +++++++-
.../TestClickHouseTableOperationsUnit.java | 453 ++++++++++++++++++++-
.../gravitino/api/rel/indexes/index.py | 11 +-
.../gravitino/dto/rel/indexes/index_dto.py | 5 +-
.../dto/rel/indexes/json_serdes/index_serdes.py | 7 +-
.../tests/unittests/dto/rel/test_index_dto.py | 26 ++
.../tests/unittests/dto/rel/test_index_serdes.py | 29 ++
.../tests/unittests/test_relational_catalog.py | 33 ++
.../org/apache/gravitino/json/TestJsonUtils.java | 16 +
docs/jdbc-clickhouse-catalog.md | 13 +-
docs/open-api/indexes.yaml | 1 +
...partitioning-distribution-sort-order-indexes.md | 1 +
.../integration/test/container/ContainerSuite.java | 35 ++
.../src/test/resources/clickhouse-test-users.xml | 25 ++
19 files changed, 1331 insertions(+), 20 deletions(-)
diff --git a/.github/workflows/contrib-catalog-test.yml
b/.github/workflows/contrib-catalog-test.yml
index 8adb813725..cef67c2097 100644
--- a/.github/workflows/contrib-catalog-test.yml
+++ b/.github/workflows/contrib-catalog-test.yml
@@ -100,6 +100,8 @@ jobs:
- name: Run contrib catalog tests
id: contribCatalogTest
+ env:
+ GRAVITINO_CLICKHOUSE_TEST_IMAGE: clickhouse:25.8.14.17
run: |
CHANGED_CONTRIB_MODULES="${{
needs.changes.outputs.changed_catalogs_contrib_modules }}"
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 ec3d6cbce7..e139ac9c69 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
@@ -54,8 +54,8 @@ public interface Index {
}
/**
- * The enum IndexType defines the type of the index. Currently, PRIMARY_KEY
and UNIQUE_KEY are
- * supported.
+ * The index type. Catalogs may support generic key types as well as
connector-specific index
+ * types.
*/
enum IndexType {
/**
@@ -144,5 +144,10 @@ public interface Index {
* v19.x.
*/
DATA_SKIPPING_TOKENBFV1,
+
+ /**
+ * Vector similarity data skipping index for approximate nearest-neighbor
search in ClickHouse.
+ */
+ DATA_SKIPPING_VECTOR_SIMILARITY,
}
}
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 94420487d7..6f40eedf6f 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
@@ -84,6 +84,28 @@ public class ClickHouseConstants {
/** The name of the data skipping index type for tokenbf_v1 in ClickHouse.
*/
public static final String DATA_SKIPPING_TOKENBFV1 = "tokenbf_v1";
+ /** The name of the vector similarity data skipping index type in
ClickHouse. */
+ public static final String DATA_SKIPPING_VECTOR_SIMILARITY =
"vector_similarity";
+
+ /** The property key for the algorithm used by vector similarity indexes.
*/
+ public static final String VECTOR_SIMILARITY_TYPE = "type";
+
+ /** The property key for the distance function used by vector similarity
indexes. */
+ public static final String VECTOR_SIMILARITY_DISTANCE_FUNCTION =
"distance_function";
+
+ /** The property key for the vector dimension used by vector similarity
indexes. */
+ public static final String VECTOR_SIMILARITY_DIMENSIONS = "dimensions";
+
+ /** The property key for HNSW vector quantization. */
+ public static final String VECTOR_SIMILARITY_QUANTIZATION = "quantization";
+
+ /** The property key for the HNSW maximum connections per layer. */
+ public static final String HNSW_MAX_CONNECTIONS_PER_LAYER =
"hnsw_max_connections_per_layer";
+
+ /** The property key for the HNSW candidate list size used during
construction. */
+ public static final String HNSW_CANDIDATE_LIST_SIZE_FOR_CONSTRUCTION =
+ "hnsw_candidate_list_size_for_construction";
+
/** 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 3af9eac869..dac8cd44ed 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
@@ -24,11 +24,18 @@ import static
org.apache.gravitino.catalog.clickhouse.ClickHouseConstants.IndexC
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_VECTOR_SIMILARITY;
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.HNSW_CANDIDATE_LIST_SIZE_FOR_CONSTRUCTION;
+import static
org.apache.gravitino.catalog.clickhouse.ClickHouseConstants.IndexConstants.HNSW_MAX_CONNECTIONS_PER_LAYER;
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.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;
+import static
org.apache.gravitino.catalog.clickhouse.ClickHouseConstants.IndexConstants.VECTOR_SIMILARITY_TYPE;
import static
org.apache.gravitino.catalog.clickhouse.ClickHouseTablePropertiesMetadata.CLICKHOUSE_ENGINE_KEY;
import static
org.apache.gravitino.catalog.clickhouse.ClickHouseTablePropertiesMetadata.CLICKHOUSE_PROJECTIONS_KEY;
import static
org.apache.gravitino.catalog.clickhouse.ClickHouseTablePropertiesMetadata.ENGINE_PROPERTY_ENTRY;
@@ -107,9 +114,28 @@ public class ClickHouseTableOperations extends
JdbcTableOperations {
"Invalid ClickHouse table SETTINGS metadata";
private static final int ERROR_CODE_UNKNOWN_TABLE = 60;
private static final Set<String> REQUIRED_PROJECTION_COLUMNS =
Set.of("name", "type", "query");
- /** Default GRANULARITY for data skipping indexes, matching ClickHouse's own
default. */
+ /** Default GRANULARITY for ordinary data skipping indexes, matching
ClickHouse's own default. */
private static final long DEFAULT_INDEX_GRANULARITY = 1;
+ private static final int DEFAULT_VECTOR_SIMILARITY_GRANULARITY = 100_000_000;
+ private static final int DEFAULT_HNSW_MAX_CONNECTIONS_PER_LAYER = 32;
+ private static final int DEFAULT_HNSW_CANDIDATE_LIST_SIZE_FOR_CONSTRUCTION =
128;
+ private static final String DEFAULT_VECTOR_SIMILARITY_TYPE = "hnsw";
+ private static final String DEFAULT_VECTOR_SIMILARITY_QUANTIZATION = "bf16";
+ private static final Set<String> VECTOR_SIMILARITY_DISTANCE_FUNCTIONS =
+ Set.of("L2Distance", "cosineDistance");
+ private static final Set<String> VECTOR_SIMILARITY_QUANTIZATIONS =
+ Set.of("f64", "f32", "f16", "bf16", "i8", "b1");
+ private static final Set<String> VECTOR_SIMILARITY_PROPERTIES =
+ Set.of(
+ VECTOR_SIMILARITY_TYPE,
+ VECTOR_SIMILARITY_DISTANCE_FUNCTION,
+ VECTOR_SIMILARITY_DIMENSIONS,
+ VECTOR_SIMILARITY_QUANTIZATION,
+ HNSW_MAX_CONNECTIONS_PER_LAYER,
+ HNSW_CANDIDATE_LIST_SIZE_FOR_CONSTRUCTION,
+ GRANULARITY);
+
private static final BigInteger MIN_SET_MAX_VALUES = BigInteger.ZERO;
private static final BigInteger MAX_SET_MAX_VALUES =
BigInteger.valueOf(Integer.MAX_VALUE);
private static final String SET_MAX_VALUES_RANGE =
@@ -695,6 +721,7 @@ public class ClickHouseTableOperations extends
JdbcTableOperations {
case DATA_SKIPPING_SET:
case DATA_SKIPPING_NGRAMBFV1:
case DATA_SKIPPING_TOKENBFV1:
+ case DATA_SKIPPING_VECTOR_SIMILARITY:
sqlBuilder
.append(" ")
.append(
@@ -1324,6 +1351,7 @@ public class ClickHouseTableOperations extends
JdbcTableOperations {
case DATA_SKIPPING_SET:
case DATA_SKIPPING_NGRAMBFV1:
case DATA_SKIPPING_TOKENBFV1:
+ case DATA_SKIPPING_VECTOR_SIMILARITY:
return "ADD "
+ buildDataSkippingIndexDdl(
addIndex.getName(), fieldStr, addIndex.getType(), properties);
@@ -2049,11 +2077,34 @@ public class ClickHouseTableOperations extends
JdbcTableOperations {
continue;
}
+ if (indexType == Index.IndexType.DATA_SKIPPING_VECTOR_SIMILARITY
+ && !includesTypeFull
+ && !StringUtils.contains(parameterSource, "(")) {
+ LOG.warn(
+ "Skip vector similarity index '{}' on {}.{} because the legacy
metadata query "
+ + "does not expose its required parameters",
+ name,
+ databaseName,
+ tableName);
+ continue;
+ }
+
if (indexType == Index.IndexType.DATA_SKIPPING_SET
- || isParameterizedBloomFilterIndex(indexType)) {
+ || isParameterizedBloomFilterIndex(indexType)
+ || indexType == Index.IndexType.DATA_SKIPPING_VECTOR_SIMILARITY)
{
try {
parameterProperties =
parseIndexPropertiesForQuery(indexType, parameterSource,
name, !includesTypeFull);
+ } catch (UnsupportedVectorSimilarityIndexException e) {
+ LOG.warn(
+ "Skip unsupported vector similarity index '{}' on {}.{} with
{} '{}': {}",
+ name,
+ databaseName,
+ tableName,
+ parameterSourceName,
+ parameterSource,
+ e.getMessage());
+ continue;
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException(
"Failed to load data skipping index '%s' from %s.%s with %s
'%s': %s"
@@ -2072,7 +2123,11 @@ public class ClickHouseTableOperations extends
JdbcTableOperations {
// 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) {
+ long defaultGranularity =
+ indexType == Index.IndexType.DATA_SKIPPING_VECTOR_SIMILARITY
+ ? DEFAULT_VECTOR_SIMILARITY_GRANULARITY
+ : DEFAULT_INDEX_GRANULARITY;
+ if (granularity != defaultGranularity) {
properties.put(GRANULARITY, String.valueOf(granularity));
}
if (!includesTypeFull
@@ -2207,6 +2262,11 @@ public class ClickHouseTableOperations extends
JdbcTableOperations {
case DATA_SKIPPING_TOKENBFV1:
return parseBloomFilterPropertiesForQuery(
indexType, parameterSource, indexName, allowBareLegacyType);
+ case DATA_SKIPPING_VECTOR_SIMILARITY:
+ if (allowBareLegacyType && !StringUtils.contains(parameterSource,
"(")) {
+ return Collections.emptyMap();
+ }
+ return parseVectorSimilarityProperties(parameterSource, indexName);
default:
return Collections.emptyMap();
}
@@ -2305,6 +2365,185 @@ public class ClickHouseTableOperations extends
JdbcTableOperations {
return parseBloomFilterProperties(indexType, parameterSource, indexName);
}
+ /**
+ * Parses the positional parameters reported for a ClickHouse vector
similarity index.
+ *
+ * @param typeFull the complete ClickHouse index type expression
+ * @param indexName the index name for validation messages
+ * @return the canonical Gravitino properties for the index
+ * @throws IllegalArgumentException if the type expression or any parameter
is invalid
+ */
+ @VisibleForTesting
+ static Map<String, String> parseVectorSimilarityProperties(String typeFull,
String indexName) {
+ String normalizedTypeFull = StringUtils.trimToEmpty(typeFull);
+ int paramsStart = normalizedTypeFull.indexOf('(');
+ Preconditions.checkArgument(
+ paramsStart > 0 && normalizedTypeFull.endsWith(")"),
+ "Invalid type_full '%s' for vector_similarity index '%s'",
+ typeFull,
+ indexName);
+ Preconditions.checkArgument(
+ StringUtils.equalsIgnoreCase(
+ DATA_SKIPPING_VECTOR_SIMILARITY, normalizedTypeFull.substring(0,
paramsStart).trim()),
+ "type_full '%s' does not match vector_similarity index '%s'",
+ typeFull,
+ indexName);
+
+ String parameterText =
+ normalizedTypeFull.substring(paramsStart + 1,
normalizedTypeFull.length() - 1);
+ List<String> parameters = splitVectorSimilarityParameters(parameterText,
typeFull, indexName);
+ Preconditions.checkArgument(
+ parameters.size() >= 3 && parameters.size() <= 6,
+ "Expected 3 to 6 parameters for vector_similarity index '%s', but got
%s in '%s'",
+ indexName,
+ parameters.size(),
+ typeFull);
+
+ String type =
+ parseVectorSimilarityStringParameter(parameters.get(0),
VECTOR_SIMILARITY_TYPE, indexName);
+ if (!StringUtils.equalsIgnoreCase(DEFAULT_VECTOR_SIMILARITY_TYPE, type)) {
+ throw new UnsupportedVectorSimilarityIndexException(
+ "Unsupported %s '%s' for vector_similarity index '%s'"
+ .formatted(VECTOR_SIMILARITY_TYPE, type, indexName));
+ }
+ String distanceFunction =
+ parseVectorSimilarityStringParameter(
+ parameters.get(1), VECTOR_SIMILARITY_DISTANCE_FUNCTION, indexName);
+ if (!VECTOR_SIMILARITY_DISTANCE_FUNCTIONS.contains(distanceFunction)) {
+ throw new UnsupportedVectorSimilarityIndexException(
+ "Unsupported %s '%s' for vector_similarity index '%s'"
+ .formatted(VECTOR_SIMILARITY_DISTANCE_FUNCTION,
distanceFunction, indexName));
+ }
+ String dimensions =
+ requireIntWithMin(
+ parameters.get(2),
+ VECTOR_SIMILARITY_DIMENSIONS,
+ DATA_SKIPPING_VECTOR_SIMILARITY,
+ indexName,
+ 1);
+
+ String quantization = DEFAULT_VECTOR_SIMILARITY_QUANTIZATION;
+ if (parameters.size() >= 4) {
+ quantization =
+ parseVectorSimilarityStringParameter(
+ parameters.get(3), VECTOR_SIMILARITY_QUANTIZATION, indexName);
+ if (!VECTOR_SIMILARITY_QUANTIZATIONS.contains(quantization)) {
+ throw new UnsupportedVectorSimilarityIndexException(
+ "Unsupported %s '%s' for vector_similarity index '%s'"
+ .formatted(VECTOR_SIMILARITY_QUANTIZATION, quantization,
indexName));
+ }
+ }
+
+ int maxConnections = DEFAULT_HNSW_MAX_CONNECTIONS_PER_LAYER;
+ if (parameters.size() >= 5) {
+ maxConnections =
+ Integer.parseInt(
+ requireIntWithMin(
+ parameters.get(4),
+ HNSW_MAX_CONNECTIONS_PER_LAYER,
+ DATA_SKIPPING_VECTOR_SIMILARITY,
+ indexName,
+ 0));
+ }
+
+ int candidateListSize = DEFAULT_HNSW_CANDIDATE_LIST_SIZE_FOR_CONSTRUCTION;
+ if (parameters.size() >= 6) {
+ candidateListSize =
+ Integer.parseInt(
+ requireIntWithMin(
+ parameters.get(5),
+ HNSW_CANDIDATE_LIST_SIZE_FOR_CONSTRUCTION,
+ DATA_SKIPPING_VECTOR_SIMILARITY,
+ indexName,
+ 0));
+ }
+
+ Map<String, String> properties = new HashMap<>();
+ properties.put(VECTOR_SIMILARITY_TYPE, DEFAULT_VECTOR_SIMILARITY_TYPE);
+ properties.put(VECTOR_SIMILARITY_DISTANCE_FUNCTION, distanceFunction);
+ properties.put(VECTOR_SIMILARITY_DIMENSIONS, dimensions);
+ if (!DEFAULT_VECTOR_SIMILARITY_QUANTIZATION.equals(quantization)) {
+ properties.put(VECTOR_SIMILARITY_QUANTIZATION, quantization);
+ }
+ if (maxConnections != 0 && maxConnections !=
DEFAULT_HNSW_MAX_CONNECTIONS_PER_LAYER) {
+ properties.put(HNSW_MAX_CONNECTIONS_PER_LAYER,
String.valueOf(maxConnections));
+ }
+ if (candidateListSize != 0
+ && candidateListSize !=
DEFAULT_HNSW_CANDIDATE_LIST_SIZE_FOR_CONSTRUCTION) {
+ properties.put(HNSW_CANDIDATE_LIST_SIZE_FOR_CONSTRUCTION,
String.valueOf(candidateListSize));
+ }
+ return Map.copyOf(properties);
+ }
+
+ private static List<String> splitVectorSimilarityParameters(
+ String parameterText, String typeFull, String indexName) {
+ List<String> parameters = new ArrayList<>();
+ StringBuilder parameter = new StringBuilder();
+ boolean inSingleQuotedString = false;
+ for (int i = 0; i < parameterText.length(); i++) {
+ char current = parameterText.charAt(i);
+ if (current == '\'') {
+ if (inSingleQuotedString
+ && i + 1 < parameterText.length()
+ && parameterText.charAt(i + 1) == '\'') {
+ parameter.append("''");
+ i++;
+ } else {
+ inSingleQuotedString = !inSingleQuotedString;
+ parameter.append(current);
+ }
+ } else if (!inSingleQuotedString && current == ',') {
+ parameters.add(parameter.toString().trim());
+ parameter.setLength(0);
+ } else if (!inSingleQuotedString && (current == '(' || current == ')')) {
+ throw new IllegalArgumentException(
+ "Invalid nested parentheses in type_full '%s' for
vector_similarity index '%s'"
+ .formatted(typeFull, indexName));
+ } else {
+ parameter.append(current);
+ }
+ }
+ Preconditions.checkArgument(
+ !inSingleQuotedString,
+ "Invalid quoted parameter in type_full '%s' for vector_similarity
index '%s'",
+ typeFull,
+ indexName);
+ parameters.add(parameter.toString().trim());
+ Preconditions.checkArgument(
+ parameters.stream().noneMatch(StringUtils::isBlank),
+ "Empty parameter in type_full '%s' for vector_similarity index '%s'",
+ typeFull,
+ indexName);
+ return List.copyOf(parameters);
+ }
+
+ private static String parseVectorSimilarityStringParameter(
+ String rawValue, String propertyName, String indexName) {
+ String value = StringUtils.trimToEmpty(rawValue);
+ if (value.startsWith("'") || value.endsWith("'")) {
+ Preconditions.checkArgument(
+ value.length() >= 2 && value.startsWith("'") && value.endsWith("'"),
+ "Malformed %s parameter '%s' for vector_similarity index '%s'",
+ propertyName,
+ rawValue,
+ indexName);
+ value = value.substring(1, value.length() - 1).replace("''", "'");
+ } else {
+ Preconditions.checkArgument(
+ !StringUtils.contains(value, "'"),
+ "Malformed %s parameter '%s' for vector_similarity index '%s'",
+ propertyName,
+ rawValue,
+ indexName);
+ }
+ Preconditions.checkArgument(
+ StringUtils.isNotBlank(value),
+ "%s is required for vector_similarity index '%s'",
+ propertyName,
+ indexName);
+ return value;
+ }
+
private static boolean isParameterizedBloomFilterIndex(Index.IndexType
indexType) {
return indexType == Index.IndexType.DATA_SKIPPING_NGRAMBFV1
|| indexType == Index.IndexType.DATA_SKIPPING_TOKENBFV1;
@@ -2313,11 +2552,11 @@ public class ClickHouseTableOperations extends
JdbcTableOperations {
/**
* 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 the {@code set(N)} parameterized format that some
ClickHouse versions
- * may return from {@code system.data_skipping_indices}.
+ * default). Also handles parameterized formats 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)")
+ * "set", "set(0)", or "vector_similarity")
* @return the corresponding Gravitino IndexType
* @throws IllegalArgumentException if the type is not supported
*/
@@ -2338,10 +2577,11 @@ public class ClickHouseTableOperations extends
JdbcTableOperations {
return Index.IndexType.DATA_SKIPPING_NGRAMBFV1;
case DATA_SKIPPING_TOKENBFV1:
return Index.IndexType.DATA_SKIPPING_TOKENBFV1;
+ case DATA_SKIPPING_VECTOR_SIMILARITY:
+ return Index.IndexType.DATA_SKIPPING_VECTOR_SIMILARITY;
default:
- // 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.
+ // ClickHouse may return type with parameters in some versions. Match
on prefix to handle
+ // both bare and parameterized formats.
if (rawType.startsWith(DATA_SKIPPING_SET + "(")) {
return Index.IndexType.DATA_SKIPPING_SET;
}
@@ -2351,6 +2591,9 @@ public class ClickHouseTableOperations extends
JdbcTableOperations {
if (rawType.startsWith(DATA_SKIPPING_TOKENBFV1 + "(")) {
return Index.IndexType.DATA_SKIPPING_TOKENBFV1;
}
+ if (rawType.startsWith(DATA_SKIPPING_VECTOR_SIMILARITY + "(")) {
+ return Index.IndexType.DATA_SKIPPING_VECTOR_SIMILARITY;
+ }
throw new IllegalArgumentException("Unsupported data skipping index
type: " + rawType);
}
}
@@ -2449,11 +2692,20 @@ public class ClickHouseTableOperations extends
JdbcTableOperations {
String fieldStr,
Index.IndexType indexType,
Map<String, String> properties) {
+ Map<String, String> safeProperties = properties == null ?
Collections.emptyMap() : properties;
+ int defaultGranularity =
+ indexType == Index.IndexType.DATA_SKIPPING_VECTOR_SIMILARITY
+ ? DEFAULT_VECTOR_SIMILARITY_GRANULARITY
+ : (int) DEFAULT_INDEX_GRANULARITY;
+ int granularity =
+ indexType == Index.IndexType.DATA_SKIPPING_VECTOR_SIMILARITY
+ ? resolveVectorSimilarityGranularity(safeProperties, indexName)
+ : resolveGranularity(safeProperties, defaultGranularity);
return buildDataSkippingIndexDdl(
indexName,
fieldStr,
- resolveDataSkippingIndexTypeClause(indexType, properties, indexName),
- resolveGranularity(properties, 1));
+ resolveDataSkippingIndexTypeClause(indexType, safeProperties,
indexName),
+ granularity);
}
private String resolveDataSkippingIndexTypeClause(
@@ -2470,12 +2722,143 @@ public class ClickHouseTableOperations extends
JdbcTableOperations {
return buildBloomFilterTypeClause(properties, DATA_SKIPPING_NGRAMBFV1,
indexName);
case DATA_SKIPPING_TOKENBFV1:
return buildBloomFilterTypeClause(properties, DATA_SKIPPING_TOKENBFV1,
indexName);
+ case DATA_SKIPPING_VECTOR_SIMILARITY:
+ return buildVectorSimilarityTypeClause(properties, indexName);
default:
throw new IllegalArgumentException(
"Gravitino ClickHouse doesn't support index : " + indexType);
}
}
+ private static String buildVectorSimilarityTypeClause(
+ Map<String, String> properties, String indexName) {
+ Map<String, String> safeProperties = properties == null ?
Collections.emptyMap() : properties;
+ for (String propertyName : safeProperties.keySet()) {
+ Preconditions.checkArgument(
+ VECTOR_SIMILARITY_PROPERTIES.contains(propertyName),
+ "Unsupported property '%s' for vector_similarity index '%s'",
+ propertyName,
+ indexName);
+ }
+
+ String type =
+ requireVectorSimilarityProperty(safeProperties,
VECTOR_SIMILARITY_TYPE, indexName);
+ Preconditions.checkArgument(
+ StringUtils.equalsIgnoreCase(DEFAULT_VECTOR_SIMILARITY_TYPE, type),
+ "Unsupported %s '%s' for vector_similarity index '%s'",
+ VECTOR_SIMILARITY_TYPE,
+ type,
+ indexName);
+
+ String distanceFunction =
+ requireVectorSimilarityProperty(
+ safeProperties, VECTOR_SIMILARITY_DISTANCE_FUNCTION, indexName);
+ Preconditions.checkArgument(
+ VECTOR_SIMILARITY_DISTANCE_FUNCTIONS.contains(distanceFunction),
+ "Unsupported %s '%s' for vector_similarity index '%s'",
+ VECTOR_SIMILARITY_DISTANCE_FUNCTION,
+ distanceFunction,
+ indexName);
+
+ String dimensions =
+ requireIntWithMin(
+ safeProperties.get(VECTOR_SIMILARITY_DIMENSIONS),
+ VECTOR_SIMILARITY_DIMENSIONS,
+ DATA_SKIPPING_VECTOR_SIMILARITY,
+ indexName,
+ 1);
+
+ String quantization = DEFAULT_VECTOR_SIMILARITY_QUANTIZATION;
+ if (safeProperties.containsKey(VECTOR_SIMILARITY_QUANTIZATION)) {
+ quantization =
+ requireVectorSimilarityProperty(
+ safeProperties, VECTOR_SIMILARITY_QUANTIZATION, indexName);
+ Preconditions.checkArgument(
+ VECTOR_SIMILARITY_QUANTIZATIONS.contains(quantization),
+ "Unsupported %s '%s' for vector_similarity index '%s'",
+ VECTOR_SIMILARITY_QUANTIZATION,
+ quantization,
+ indexName);
+ }
+
+ String maxConnections = null;
+ if (safeProperties.containsKey(HNSW_MAX_CONNECTIONS_PER_LAYER)) {
+ maxConnections =
+ requireIntWithMin(
+ safeProperties.get(HNSW_MAX_CONNECTIONS_PER_LAYER),
+ HNSW_MAX_CONNECTIONS_PER_LAYER,
+ DATA_SKIPPING_VECTOR_SIMILARITY,
+ indexName,
+ 0);
+ }
+
+ String candidateListSize = null;
+ if (safeProperties.containsKey(HNSW_CANDIDATE_LIST_SIZE_FOR_CONSTRUCTION))
{
+ candidateListSize =
+ requireIntWithMin(
+ safeProperties.get(HNSW_CANDIDATE_LIST_SIZE_FOR_CONSTRUCTION),
+ HNSW_CANDIDATE_LIST_SIZE_FOR_CONSTRUCTION,
+ DATA_SKIPPING_VECTOR_SIMILARITY,
+ indexName,
+ 0);
+ }
+
+ List<String> parameters =
+ new ArrayList<>(List.of("'hnsw'", "'" + distanceFunction + "'",
dimensions));
+ boolean includeQuantization =
+ !DEFAULT_VECTOR_SIMILARITY_QUANTIZATION.equals(quantization)
+ || hasNonDefaultInteger(maxConnections,
DEFAULT_HNSW_MAX_CONNECTIONS_PER_LAYER)
+ || hasNonDefaultInteger(
+ candidateListSize,
DEFAULT_HNSW_CANDIDATE_LIST_SIZE_FOR_CONSTRUCTION);
+ if (includeQuantization) {
+ parameters.add("'" + quantization + "'");
+ }
+ boolean includeMaxConnections =
+ hasNonDefaultInteger(maxConnections,
DEFAULT_HNSW_MAX_CONNECTIONS_PER_LAYER)
+ || hasNonDefaultInteger(
+ candidateListSize,
DEFAULT_HNSW_CANDIDATE_LIST_SIZE_FOR_CONSTRUCTION);
+ if (includeMaxConnections) {
+ parameters.add(
+ hasNonDefaultInteger(maxConnections,
DEFAULT_HNSW_MAX_CONNECTIONS_PER_LAYER)
+ ? maxConnections
+ : String.valueOf(DEFAULT_HNSW_MAX_CONNECTIONS_PER_LAYER));
+ }
+ if (hasNonDefaultInteger(
+ candidateListSize, DEFAULT_HNSW_CANDIDATE_LIST_SIZE_FOR_CONSTRUCTION))
{
+ parameters.add(candidateListSize);
+ }
+ return DATA_SKIPPING_VECTOR_SIMILARITY + "(" + String.join(", ",
parameters) + ")";
+ }
+
+ private static boolean hasNonDefaultInteger(String value, int defaultValue) {
+ return value != null && !value.equals("0") &&
!value.equals(String.valueOf(defaultValue));
+ }
+
+ private static String requireVectorSimilarityProperty(
+ Map<String, String> properties, String propertyName, String indexName) {
+ String value = properties.get(propertyName);
+ Preconditions.checkArgument(
+ StringUtils.isNotBlank(value),
+ "%s is required for vector_similarity index '%s'",
+ propertyName,
+ indexName);
+ return value.trim();
+ }
+
+ private static int resolveVectorSimilarityGranularity(
+ Map<String, String> properties, String indexName) {
+ if (!properties.containsKey(GRANULARITY)) {
+ return DEFAULT_VECTOR_SIMILARITY_GRANULARITY;
+ }
+ return Integer.parseInt(
+ requireIntWithMin(
+ properties.get(GRANULARITY),
+ GRANULARITY,
+ DATA_SKIPPING_VECTOR_SIMILARITY,
+ indexName,
+ 1));
+ }
+
private String buildDataSkippingIndexDdl(
String indexName, String fieldStr, String typeName, int granularity) {
Preconditions.checkArgument(
@@ -2640,4 +3023,13 @@ public class ClickHouseTableOperations extends
JdbcTableOperations {
return sqlBuilder;
}
+
+ private static final class UnsupportedVectorSimilarityIndexException
+ extends IllegalArgumentException {
+ private static final long serialVersionUID = 1L;
+
+ private UnsupportedVectorSimilarityIndexException(String message) {
+ super(message);
+ }
+ }
}
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 0cee91a224..6dd44dd032 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
@@ -636,6 +636,91 @@ public class CatalogClickHouseClusterIT extends BaseIT {
TableChange.deleteIndex("idx_token_alter", false));
}
+ @Test
+ public void testVectorSimilarityIndexOnCluster() {
+ String tableName = GravitinoITUtils.genRandomName("ck_cluster_vector_idx");
+ NameIdentifier tableIdentifier = NameIdentifier.of(schemaName, tableName);
+ String vectorColumn = "embedding";
+ String customVectorColumn = "embedding_custom";
+ TableCatalog tableCatalog = catalog.asTableCatalog();
+ Map<String, String> defaultProperties =
+ Map.of("type", "hnsw", "distance_function", "L2Distance",
"dimensions", "3");
+
+ tableCatalog.createTable(
+ tableIdentifier,
+ new Column[] {
+ Column.of("id", Types.IntegerType.get(), "id", false, false,
DEFAULT_VALUE_NOT_SET),
+ Column.of(
+ vectorColumn,
+ Types.ExternalType.of("Array(Float32)"),
+ "embedding",
+ false,
+ false,
+ DEFAULT_VALUE_NOT_SET),
+ Column.of(
+ customVectorColumn,
+ Types.ExternalType.of("Array(Float32)"),
+ "custom embedding",
+ false,
+ false,
+ DEFAULT_VALUE_NOT_SET),
+ },
+ tableComment,
+ clusterMergeTreeProperties(),
+ Transforms.EMPTY_TRANSFORM,
+ Distributions.NONE,
+ getSortOrders("id"),
+ new Index[] {
+ Indexes.of(
+ Index.IndexType.DATA_SKIPPING_VECTOR_SIMILARITY,
+ "idx_vector_default",
+ new String[][] {{vectorColumn}},
+ defaultProperties)
+ });
+
+ Table loaded = tableCatalog.loadTable(tableIdentifier);
+ Index defaultIndex =
+ Arrays.stream(loaded.index())
+ .filter(index -> Objects.equals(index.name(),
"idx_vector_default"))
+ .findFirst()
+ .orElseThrow();
+ Assertions.assertEquals(Index.IndexType.DATA_SKIPPING_VECTOR_SIMILARITY,
defaultIndex.type());
+ Assertions.assertArrayEquals(new String[][] {{vectorColumn}},
defaultIndex.fieldNames());
+ Assertions.assertEquals(defaultProperties, defaultIndex.properties());
+
+ Map<String, String> customProperties =
+ Map.of(
+ "type", "hnsw",
+ "distance_function", "cosineDistance",
+ "dimensions", "3",
+ "quantization", "f16",
+ "hnsw_max_connections_per_layer", "16",
+ "hnsw_candidate_list_size_for_construction", "64",
+ "granularity", "7");
+ tableCatalog.alterTable(
+ tableIdentifier,
+ TableChange.addIndex(
+ Index.IndexType.DATA_SKIPPING_VECTOR_SIMILARITY,
+ "idx_vector_custom",
+ new String[][] {{customVectorColumn}},
+ customProperties));
+
+ Table altered = tableCatalog.loadTable(tableIdentifier);
+ Index customIndex =
+ Arrays.stream(altered.index())
+ .filter(index -> Objects.equals(index.name(), "idx_vector_custom"))
+ .findFirst()
+ .orElseThrow();
+ Assertions.assertEquals(Index.IndexType.DATA_SKIPPING_VECTOR_SIMILARITY,
customIndex.type());
+ Assertions.assertArrayEquals(new String[][] {{customVectorColumn}},
customIndex.fieldNames());
+ Assertions.assertEquals(customProperties, customIndex.properties());
+
+ tableCatalog.alterTable(
+ tableIdentifier,
+ TableChange.deleteIndex("idx_vector_default", false),
+ TableChange.deleteIndex("idx_vector_custom", false));
+ }
+
private void assertIndexMetadata(
Index[] indexes, String name, Index.IndexType type, Map<String, String>
properties) {
Index index =
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 09878fb407..c52f77715b 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
@@ -91,6 +91,7 @@ import org.apache.gravitino.utils.RandomNameUtils;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
@@ -1688,7 +1689,11 @@ public class CatalogClickHouseIT extends BaseIT {
Assertions.assertEquals(Types.DateType.get(), column.dataType());
break;
case "time_col":
- Assertions.assertEquals(Types.LongType.get(), column.dataType());
+ Assertions.assertEquals(
+
StringUtils.isBlank(System.getenv("GRAVITINO_CLICKHOUSE_TEST_IMAGE"))
+ ? Types.LongType.get()
+ : Types.ExternalType.of("Time"),
+ column.dataType());
break;
case "timestamp_col":
Assertions.assertEquals(Types.TimestampType.withoutTimeZone(0),
column.dataType());
@@ -3891,4 +3896,161 @@ public class CatalogClickHouseIT extends BaseIT {
normalizedCreateSql.contains("tokenbf_v1(256,2,0)"),
"SHOW CREATE TABLE should retain tokenbf_v1 parameters: " + createSql);
}
+
+ @Test
+ @Tag("gravitino-docker-test")
+ void testVectorSimilarityIndex() {
+ String tableName =
GravitinoITUtils.genRandomName("ch_vector_similarity_idx_");
+ NameIdentifier tableIdentifier = NameIdentifier.of(schemaName, tableName);
+ String vectorColumn = "embedding";
+ String customVectorColumn = "embedding_custom";
+ String zeroVectorColumn = "embedding_zero";
+ Column[] columns =
+ new Column[] {
+ Column.of(
+ CLICKHOUSE_COL_NAME1,
+ Types.IntegerType.get(),
+ "id",
+ false,
+ false,
+ DEFAULT_VALUE_NOT_SET),
+ Column.of(
+ vectorColumn,
+ Types.ExternalType.of("Array(Float32)"),
+ "embedding",
+ false,
+ false,
+ DEFAULT_VALUE_NOT_SET),
+ Column.of(
+ customVectorColumn,
+ Types.ExternalType.of("Array(Float32)"),
+ "custom embedding",
+ false,
+ false,
+ DEFAULT_VALUE_NOT_SET),
+ Column.of(
+ zeroVectorColumn,
+ Types.ExternalType.of("Array(Float32)"),
+ "zero parameter embedding",
+ false,
+ false,
+ DEFAULT_VALUE_NOT_SET),
+ };
+ Map<String, String> defaultProperties =
+ Map.of("type", "hnsw", "distance_function", "L2Distance",
"dimensions", "3");
+ TableCatalog tableCatalog = catalog.asTableCatalog();
+
+ tableCatalog.createTable(
+ tableIdentifier,
+ columns,
+ table_comment,
+ createProperties(),
+ Transforms.EMPTY_TRANSFORM,
+ Distributions.NONE,
+ getSortOrders(CLICKHOUSE_COL_NAME1),
+ new Index[] {
+ Indexes.of(
+ Index.IndexType.DATA_SKIPPING_VECTOR_SIMILARITY,
+ "idx_vector_default",
+ new String[][] {{vectorColumn}},
+ defaultProperties)
+ });
+
+ Table loaded = tableCatalog.loadTable(tableIdentifier);
+ Index defaultIndex =
+ Arrays.stream(loaded.index())
+ .filter(index -> Objects.equals(index.name(),
"idx_vector_default"))
+ .findFirst()
+ .orElseThrow();
+ Assertions.assertEquals(Index.IndexType.DATA_SKIPPING_VECTOR_SIMILARITY,
defaultIndex.type());
+ Assertions.assertArrayEquals(new String[][] {{vectorColumn}},
defaultIndex.fieldNames());
+ Assertions.assertEquals(defaultProperties, defaultIndex.properties());
+
+ Map<String, String> customProperties =
+ Map.of(
+ "type", "hnsw",
+ "distance_function", "cosineDistance",
+ "dimensions", "3",
+ "quantization", "i8",
+ "hnsw_max_connections_per_layer", "16",
+ "hnsw_candidate_list_size_for_construction", "64",
+ "granularity", "7");
+ tableCatalog.alterTable(
+ tableIdentifier,
+ TableChange.addIndex(
+ Index.IndexType.DATA_SKIPPING_VECTOR_SIMILARITY,
+ "idx_vector_custom",
+ new String[][] {{customVectorColumn}},
+ customProperties));
+
+ clickhouseService.executeQuery(
+ "ALTER TABLE `%s`.`%s` ADD INDEX `idx_vector_zero` `%s` TYPE "
+ .formatted(schemaName, tableName, zeroVectorColumn)
+ + "vector_similarity('hnsw', 'L2Distance', 3, 'bf16', 0, 0) "
+ + "GRANULARITY 100000000");
+
+ Table altered = tableCatalog.loadTable(tableIdentifier);
+ Index customIndex =
+ Arrays.stream(altered.index())
+ .filter(index -> Objects.equals(index.name(), "idx_vector_custom"))
+ .findFirst()
+ .orElseThrow();
+ Assertions.assertEquals(Index.IndexType.DATA_SKIPPING_VECTOR_SIMILARITY,
customIndex.type());
+ Assertions.assertArrayEquals(new String[][] {{customVectorColumn}},
customIndex.fieldNames());
+ Assertions.assertEquals(customProperties, customIndex.properties());
+
+ Index zeroValueIndex =
+ Arrays.stream(altered.index())
+ .filter(index -> Objects.equals(index.name(), "idx_vector_zero"))
+ .findFirst()
+ .orElseThrow();
+ Assertions.assertArrayEquals(new String[][] {{zeroVectorColumn}},
zeroValueIndex.fieldNames());
+ Assertions.assertEquals(
+ Map.of("type", "hnsw", "distance_function", "L2Distance",
"dimensions", "3"),
+ zeroValueIndex.properties());
+
+ String createSql =
+ clickhouseService.executeQueryForResult(
+ String.format("SHOW CREATE TABLE `%s`.`%s`", schemaName,
tableName));
+ String normalizedCreateSql = createSql.replaceAll("\\s+", "");
+ Assertions.assertTrue(
+
normalizedCreateSql.contains("vector_similarity('hnsw','L2Distance',3)"),
createSql);
+ Assertions.assertTrue(
+
normalizedCreateSql.contains("vector_similarity('hnsw','cosineDistance',3,'i8',16,64)"),
+ createSql);
+
Assertions.assertTrue(normalizedCreateSql.contains("GRANULARITY100000000"),
createSql);
+ Assertions.assertTrue(normalizedCreateSql.contains("GRANULARITY7"),
createSql);
+ }
+
+ @Test
+ @Tag("gravitino-docker-test")
+ void testLoadTableWithUnsupportedNativeDotProductVectorIndex() {
+ String[] version = clickhouseService.executeQueryForResult("SELECT
version()").split("\\.");
+ int majorVersion = Integer.parseInt(version[0]);
+ int minorVersion = Integer.parseInt(version[1]);
+ Assumptions.assumeTrue(
+ majorVersion > 26 || (majorVersion == 26 && minorVersion >= 4),
+ "Native dotProduct vector indexes require ClickHouse 26.4 or later");
+
+ String nativeTableName =
GravitinoITUtils.genRandomName("ch_native_dot_vector_idx_");
+ clickhouseService.executeQuery(
+ ("CREATE TABLE `%s`.`%s` ("
+ + "id Int32, embedding_l2 Array(Float32), embedding_dot
Array(Float32), "
+ + "INDEX idx_minmax id TYPE minmax GRANULARITY 1, "
+ + "INDEX idx_l2 embedding_l2 TYPE "
+ + "vector_similarity('hnsw', 'L2Distance', 3) GRANULARITY
100000000, "
+ + "INDEX idx_dot embedding_dot TYPE "
+ + "vector_similarity('hnsw', 'dotProduct', 3) GRANULARITY
100000000) "
+ + "ENGINE = MergeTree ORDER BY id")
+ .formatted(schemaName, nativeTableName));
+
+ Table loaded =
+ catalog.asTableCatalog().loadTable(NameIdentifier.of(schemaName,
nativeTableName));
+ Assertions.assertTrue(
+ Arrays.stream(loaded.index()).anyMatch(index ->
"idx_minmax".equals(index.name())));
+ Assertions.assertTrue(
+ Arrays.stream(loaded.index()).anyMatch(index ->
"idx_l2".equals(index.name())));
+ Assertions.assertFalse(
+ Arrays.stream(loaded.index()).anyMatch(index ->
"idx_dot".equals(index.name())));
+ }
}
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 fb915f4136..1759dce3df 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
@@ -94,6 +94,11 @@ public class TestClickHouseTableOperationsUnit {
}
String callGenerateCreateTableSql(JdbcColumn[] columns, Map<String,
String> properties) {
+ return callGenerateCreateTableSql(columns, properties,
Indexes.EMPTY_INDEXES);
+ }
+
+ String callGenerateCreateTableSql(
+ JdbcColumn[] columns, Map<String, String> properties, Index[] indexes)
{
return generateCreateTableSql(
"test_table",
columns,
@@ -101,7 +106,7 @@ public class TestClickHouseTableOperationsUnit {
properties,
Transforms.EMPTY_TRANSFORM,
Distributions.NONE,
- Indexes.EMPTY_INDEXES,
+ indexes,
getSortOrders("id"));
}
@@ -1360,6 +1365,85 @@ public class TestClickHouseTableOperationsUnit {
.isEmpty());
}
+ @Test
+ void testParseVectorSimilarityPropertiesAndNormalizesDefaults() {
+ Assertions.assertEquals(
+ Map.of("type", "hnsw", "distance_function", "L2Distance",
"dimensions", "3"),
+ ClickHouseTableOperations.parseVectorSimilarityProperties(
+ " vector_similarity ( 'hnsw' , 'L2Distance' , 003 ) ",
"idx_vector"));
+ Assertions.assertEquals(
+ Map.of(
+ "type", "hnsw",
+ "distance_function", "cosineDistance",
+ "dimensions", "768",
+ "quantization", "i8"),
+ ClickHouseTableOperations.parseVectorSimilarityProperties(
+ "vector_similarity('hnsw', 'cosineDistance', 768, 'i8')",
"idx_vector"));
+ Assertions.assertEquals(
+ Map.of(
+ "type", "hnsw",
+ "distance_function", "L2Distance",
+ "dimensions", "3",
+ "hnsw_max_connections_per_layer", "16"),
+ ClickHouseTableOperations.parseVectorSimilarityProperties(
+ "vector_similarity('hnsw', 'L2Distance', 3, 'bf16', 16)",
"idx_vector"));
+ for (String quantization : List.of("f64", "f32", "f16", "i8", "b1")) {
+ Assertions.assertEquals(
+ quantization,
+ ClickHouseTableOperations.parseVectorSimilarityProperties(
+ "vector_similarity('hnsw', 'L2Distance', 3, '" +
quantization + "')",
+ "idx_vector")
+ .get("quantization"));
+ }
+ Assertions.assertEquals(
+ Map.of("type", "hnsw", "distance_function", "L2Distance",
"dimensions", "3"),
+ ClickHouseTableOperations.parseVectorSimilarityProperties(
+ "vector_similarity('hnsw', 'L2Distance', 3, 'bf16', 0, 0)",
"idx_vector"));
+ ExposedClickHouseTableOperations ops = newOps();
+ Assertions.assertEquals(
+ Index.IndexType.DATA_SKIPPING_VECTOR_SIMILARITY,
+ ops.getClickHouseIndexType("vector_similarity"));
+ 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
+ void
testParseVectorSimilarityPropertiesRejectsMalformedAndUnsupportedParameters() {
+ List<String> invalidTypeFullValues =
+ List.of(
+ "vector_similarity",
+ "wrong_type('hnsw', 'L2Distance', 3)",
+ "vector_similarity('ivf', 'L2Distance', 3)",
+ "vector_similarity('hnsw', 'euclidean', 3)",
+ "vector_similarity('hnsw', 'dotProduct', 384, 'bf16', 16, 64)",
+ "vector_similarity('hnsw', , 3)",
+ "vector_similarity('hnsw', 'L2Distance', 0)",
+ "vector_similarity('hnsw', 'L2Distance', not_an_integer)",
+ "vector_similarity('hnsw', 'L2Distance', 3, 'float8')",
+ "vector_similarity('hnsw', 'L2Distance', 3, 'bf16', -1)",
+ "vector_similarity('hnsw', 'L2Distance', 3, 'bf16', 32, -1)",
+ "vector_similarity('hnsw', 'L2Distance', 3, 'bf16', 32, 128, 1)",
+ "vector_similarity('hnsw', 'L2Distance', 3, 'bf16', 32,
2147483648)",
+ "vector_similarity('hnsw', 'L2Distance', 3, 'bf16', 32",
+ "vector_similarity('hnsw', 'L2Distance', 3, 'bf16', 32, 128)
trailing");
+
+ for (String typeFull : invalidTypeFullValues) {
+ IllegalArgumentException exception =
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ ClickHouseTableOperations.parseVectorSimilarityProperties(
+ typeFull, "idx_vector_bad"),
+ typeFull);
+ Assertions.assertTrue(exception.getMessage().contains("idx_vector_bad"),
typeFull);
+ }
+ }
+
@Test
void testParseSetPropertiesAcceptsIntegerMaxValue() {
Assertions.assertEquals(
@@ -1636,6 +1720,373 @@ public class TestClickHouseTableOperationsUnit {
Assertions.assertTrue(bare.properties().isEmpty());
}
+ @Test
+ void testGetIndexesFallsBackAndReadsLegacyVectorSimilarityParameters()
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_vector", "idx_legacy_vector_custom",
"idx_legacy_vector_bare");
+ Mockito.when(legacySecondaryRs.getString("type"))
+ .thenReturn(
+ "vector_similarity('hnsw', 'cosineDistance', 3, 'i8', 16, 64)",
+ "vector_similarity('hnsw', 'L2Distance', 3)",
+ "vector_similarity");
+ Mockito.when(legacySecondaryRs.getString("expr"))
+ .thenReturn("embedding", "embedding_custom", "embedding_bare");
+ Mockito.when(legacySecondaryRs.getLong("granularity"))
+ .thenReturn(100_000_000L, 7L, 100_000_000L);
+
+ 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(2, indexes.size());
+ Index legacyIndex =
+ indexes.stream()
+ .filter(index -> "idx_legacy_vector".equals(index.name()))
+ .findFirst()
+ .orElseThrow();
+ Assertions.assertEquals(Index.IndexType.DATA_SKIPPING_VECTOR_SIMILARITY,
legacyIndex.type());
+ Assertions.assertArrayEquals(new String[][] {{"embedding"}},
legacyIndex.fieldNames());
+ Assertions.assertEquals(
+ Map.of(
+ "type", "hnsw",
+ "distance_function", "cosineDistance",
+ "dimensions", "3",
+ "quantization", "i8",
+ "hnsw_max_connections_per_layer", "16",
+ "hnsw_candidate_list_size_for_construction", "64"),
+ legacyIndex.properties());
+
+ Index customGranularityIndex =
+ indexes.stream()
+ .filter(index -> "idx_legacy_vector_custom".equals(index.name()))
+ .findFirst()
+ .orElseThrow();
+ Assertions.assertEquals(
+ Map.of(
+ "type", "hnsw",
+ "distance_function", "L2Distance",
+ "dimensions", "3",
+ "granularity", "7"),
+ customGranularityIndex.properties());
+ }
+
+ @Test
+ void
testGetIndexesReadsVectorSimilarityTypeFullAndUsesItsDefaultGranularity()
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, true, false);
+ Mockito.when(secondaryStmt.executeQuery()).thenReturn(secondaryRs);
+ Mockito.when(secondaryRs.getString("name"))
+ .thenReturn("idx_vector_default", "idx_vector_custom",
"idx_vector_expression");
+ Mockito.when(secondaryRs.getString("type"))
+ .thenReturn("vector_similarity", "vector_similarity",
"vector_similarity");
+ Mockito.when(secondaryRs.getString("type_full"))
+ .thenReturn(
+ "vector_similarity('hnsw', 'L2Distance', 3)",
+ "vector_similarity('hnsw', 'cosineDistance', 3, 'i8', 16, 64)",
+ "vector_similarity('hnsw', 'L2Distance', 3)");
+ Mockito.when(secondaryRs.getString("expr"))
+ .thenReturn("embedding", "embedding_custom", "lower(embedding)");
+ Mockito.when(secondaryRs.getLong("granularity")).thenReturn(100_000_000L,
7L, 100_000_000L);
+
+ 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(2, indexes.size());
+ Index defaultIndex =
+ indexes.stream()
+ .filter(index -> "idx_vector_default".equals(index.name()))
+ .findFirst()
+ .orElseThrow();
+ Assertions.assertEquals(Index.IndexType.DATA_SKIPPING_VECTOR_SIMILARITY,
defaultIndex.type());
+ Assertions.assertArrayEquals(new String[][] {{"embedding"}},
defaultIndex.fieldNames());
+ Assertions.assertEquals(
+ Map.of("type", "hnsw", "distance_function", "L2Distance",
"dimensions", "3"),
+ defaultIndex.properties());
+
+ Index customIndex =
+ indexes.stream()
+ .filter(index -> "idx_vector_custom".equals(index.name()))
+ .findFirst()
+ .orElseThrow();
+ Assertions.assertEquals(
+ Map.of(
+ "type", "hnsw",
+ "distance_function", "cosineDistance",
+ "dimensions", "3",
+ "quantization", "i8",
+ "hnsw_max_connections_per_layer", "16",
+ "hnsw_candidate_list_size_for_construction", "64",
+ "granularity", "7"),
+ customIndex.properties());
+ Assertions.assertFalse(
+ indexes.stream().anyMatch(index ->
"idx_vector_expression".equals(index.name())));
+ }
+
+ @Test
+ void
testGetIndexesSkipsUnsupportedVectorSimilarityWithoutDroppingOtherIndexes()
+ 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, true, false);
+ Mockito.when(secondaryStmt.executeQuery()).thenReturn(secondaryRs);
+ Mockito.when(secondaryRs.getString("name")).thenReturn("idx_dot",
"idx_l2", "idx_minmax");
+ Mockito.when(secondaryRs.getString("type"))
+ .thenReturn("vector_similarity", "vector_similarity", "minmax");
+ Mockito.when(secondaryRs.getString("type_full"))
+ .thenReturn(
+ "vector_similarity('hnsw', 'dotProduct', 3)",
+ "vector_similarity('hnsw', 'L2Distance', 3)",
+ "minmax");
+ Mockito.when(secondaryRs.getString("expr")).thenReturn("embedding_dot",
"embedding_l2", "id");
+ Mockito.when(secondaryRs.getLong("granularity")).thenReturn(100_000_000L,
100_000_000L, 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(2, indexes.size());
+ Assertions.assertEquals("idx_l2", indexes.get(0).name());
+ Assertions.assertEquals(Index.IndexType.DATA_SKIPPING_VECTOR_SIMILARITY,
indexes.get(0).type());
+ Assertions.assertEquals("idx_minmax", indexes.get(1).name());
+ Assertions.assertEquals(Index.IndexType.DATA_SKIPPING_MINMAX,
indexes.get(1).type());
+ }
+
+ @Test
+ void testGetIndexesRejectsMalformedVectorSimilarityMetadata() 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_malformed");
+
Mockito.when(secondaryRs.getString("type")).thenReturn("vector_similarity");
+ Mockito.when(secondaryRs.getString("type_full"))
+ .thenReturn("vector_similarity('hnsw', 'L2Distance', not_an_integer)");
+ Mockito.when(secondaryRs.getString("expr")).thenReturn("embedding");
+ Mockito.when(secondaryRs.getLong("granularity")).thenReturn(100_000_000L);
+
+ 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_malformed"));
+ Assertions.assertTrue(exception.getMessage().contains("type_full"));
+ }
+
+ @Test
+ void testVectorSimilarityCreateAndAlterDdlUseTheSameTypeClause() {
+ JdbcColumn[] columns =
+ new JdbcColumn[] {
+ JdbcColumn.builder()
+ .withName("id")
+ .withType(Types.IntegerType.get())
+ .withNullable(false)
+ .build(),
+ JdbcColumn.builder()
+ .withName("embedding")
+ .withType(Types.StringType.get())
+ .withNullable(true)
+ .build(),
+ };
+ Map<String, String> defaultProperties =
+ Map.of("type", "hnsw", "distance_function", "L2Distance",
"dimensions", "3");
+ Map<String, String> customProperties =
+ Map.of(
+ "type", "hnsw",
+ "distance_function", "cosineDistance",
+ "dimensions", "3",
+ "quantization", "i8",
+ "hnsw_candidate_list_size_for_construction", "64",
+ "granularity", "7");
+ Map<String, String> zeroHnswProperties =
+ Map.of(
+ "type", "hnsw",
+ "distance_function", "L2Distance",
+ "dimensions", "3",
+ "hnsw_max_connections_per_layer", "0",
+ "hnsw_candidate_list_size_for_construction", "0");
+
+ String createSql =
+ newOps()
+ .callGenerateCreateTableSql(
+ columns,
+ Map.of(),
+ new Index[] {
+ Indexes.of(
+ Index.IndexType.DATA_SKIPPING_VECTOR_SIMILARITY,
+ "idx_vector_default",
+ new String[][] {{"embedding"}},
+ defaultProperties),
+ Indexes.of(
+ Index.IndexType.DATA_SKIPPING_MINMAX, "idx_minmax", new
String[][] {{"id"}})
+ });
+ ExposedClickHouseTableOperations alterOps = newAlterOps(Map.of());
+ String alterSql =
+ alterOps.callGenerateAlterTableSql(
+ TableChange.addIndex(
+ Index.IndexType.DATA_SKIPPING_VECTOR_SIMILARITY,
+ "idx_vector_custom",
+ new String[][] {{"embedding"}},
+ customProperties));
+ String zeroHnswAlterSql =
+ alterOps.callGenerateAlterTableSql(
+ TableChange.addIndex(
+ Index.IndexType.DATA_SKIPPING_VECTOR_SIMILARITY,
+ "idx_vector_zero_hnsw",
+ new String[][] {{"embedding"}},
+ zeroHnswProperties));
+
+ Assertions.assertTrue(
+ createSql.contains(
+ "INDEX `idx_vector_default` `embedding` TYPE "
+ + "vector_similarity('hnsw', 'L2Distance', 3) GRANULARITY
100000000"),
+ createSql);
+ Assertions.assertTrue(
+ createSql.contains("INDEX `idx_minmax` `id` TYPE minmax GRANULARITY
1"), createSql);
+ Assertions.assertTrue(
+ alterSql.contains(
+ "ADD INDEX `idx_vector_custom` `embedding` TYPE "
+ + "vector_similarity('hnsw', 'cosineDistance', 3, 'i8', 32,
64) GRANULARITY 7"),
+ alterSql);
+ Assertions.assertTrue(
+ zeroHnswAlterSql.contains(
+ "ADD INDEX `idx_vector_zero_hnsw` `embedding` TYPE "
+ + "vector_similarity('hnsw', 'L2Distance', 3) GRANULARITY
100000000"),
+ zeroHnswAlterSql);
+ }
+
+ @Test
+ void testVectorSimilarityInvalidPropertiesFailBeforeCreateOrAlterDdl() {
+ Map<String, String> missingRequiredProperty = Map.of("type", "hnsw",
"dimensions", "3");
+ Map<String, String> unsupportedDistance =
+ Map.of("type", "hnsw", "distance_function", "dotProduct",
"dimensions", "3");
+ Map<String, String> unsupportedQuantization =
+ Map.of(
+ "type", "hnsw",
+ "distance_function", "L2Distance",
+ "dimensions", "3",
+ "quantization", "float8");
+ Map<String, String> negativeHnswValue =
+ Map.of(
+ "type", "hnsw",
+ "distance_function", "L2Distance",
+ "dimensions", "3",
+ "hnsw_max_connections_per_layer", "-1");
+ Map<String, String> zeroGranularity =
+ Map.of(
+ "type", "hnsw",
+ "distance_function", "L2Distance",
+ "dimensions", "3",
+ "granularity", "0");
+ Map<String, String> unknownProperty =
+ Map.of(
+ "type", "hnsw",
+ "distance_function", "L2Distance",
+ "dimensions", "3",
+ "unexpected_property", "value");
+
+ List<Map.Entry<String, Map<String, String>>> invalidCases =
+ List.of(
+ Map.entry("distance_function", missingRequiredProperty),
+ Map.entry("distance_function", unsupportedDistance),
+ Map.entry("quantization", unsupportedQuantization),
+ Map.entry("hnsw_max_connections_per_layer", negativeHnswValue),
+ Map.entry("granularity", zeroGranularity),
+ Map.entry("unexpected_property", unknownProperty));
+ for (Map.Entry<String, Map<String, String>> invalid : invalidCases) {
+ Map<String, String> properties = invalid.getValue();
+ Index[] indexes =
+ new Index[] {
+ Indexes.of(
+ Index.IndexType.DATA_SKIPPING_VECTOR_SIMILARITY,
+ "idx_vector_invalid",
+ new String[][] {{"embedding"}},
+ properties)
+ };
+ IllegalArgumentException createException =
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ newOps()
+ .callGenerateCreateTableSql(
+ new JdbcColumn[] {
+ JdbcColumn.builder()
+ .withName("id")
+ .withType(Types.IntegerType.get())
+ .withNullable(false)
+ .build(),
+ JdbcColumn.builder()
+ .withName("embedding")
+ .withType(Types.StringType.get())
+ .withNullable(true)
+ .build(),
+ },
+ Map.of(),
+ indexes));
+
Assertions.assertTrue(createException.getMessage().contains("idx_vector_invalid"));
+
Assertions.assertTrue(createException.getMessage().contains(invalid.getKey()));
+
+ IllegalArgumentException alterException =
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ newAlterOps(Map.of())
+ .callGenerateAlterTableSql(
+ TableChange.addIndex(
+ Index.IndexType.DATA_SKIPPING_VECTOR_SIMILARITY,
+ "idx_vector_invalid",
+ new String[][] {{"embedding"}},
+ properties)));
+
Assertions.assertTrue(alterException.getMessage().contains("idx_vector_invalid"));
+
Assertions.assertTrue(alterException.getMessage().contains(invalid.getKey()));
+ }
+ }
+
@Test
void testGetIndexesDoesNotFallbackForOtherSqlErrors() throws Exception {
ExposedClickHouseTableOperations ops = newOps();
diff --git a/clients/client-python/gravitino/api/rel/indexes/index.py
b/clients/client-python/gravitino/api/rel/indexes/index.py
index 777e3b4813..d55929a040 100644
--- a/clients/client-python/gravitino/api/rel/indexes/index.py
+++ b/clients/client-python/gravitino/api/rel/indexes/index.py
@@ -24,14 +24,14 @@ from typing import List, Optional
class Index(ABC):
"""The Index interface defines methods for implementing table index
columns.
- Currently, settings for `PRIMARY_KEY` and `UNIQUE_KEY` are provided.
+ Catalogs may provide key and connector-specific index types.
"""
@unique
class IndexType(str, Enum):
"""The enum IndexType defines the type of the index.
- Currently, `PRIMARY_KEY` and `UNIQUE_KEY` are supported.
+ Catalogs may support generic key types and connector-specific index
types.
"""
PRIMARY_KEY = "PRIMARY_KEY"
@@ -50,6 +50,9 @@ 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_VECTOR_SIMILARITY = "DATA_SKIPPING_VECTOR_SIMILARITY"
+ """ClickHouse vector similarity data skipping index."""
+
@abstractmethod
def type(self) -> IndexType:
"""Returns the type of the index. eg: `PRIMARY_KEY` and `UNIQUE_KEY`.
@@ -79,3 +82,7 @@ class Index(ABC):
List[List[str]]: The field name under the table contained in the
index.
"""
pass # pragma: no cover
+
+ def properties(self) -> dict[str, str]:
+ """Returns the index properties, or an empty map when none are
defined."""
+ return {}
diff --git a/clients/client-python/gravitino/dto/rel/indexes/index_dto.py
b/clients/client-python/gravitino/dto/rel/indexes/index_dto.py
index 4842806e07..dbfa3240c3 100644
--- a/clients/client-python/gravitino/dto/rel/indexes/index_dto.py
+++ b/clients/client-python/gravitino/dto/rel/indexes/index_dto.py
@@ -70,10 +70,13 @@ class IndexDTO(Index):
self._index_type is other.type()
and self._name == other.name()
and self._field_names == other.field_names()
+ and self._properties == other.properties()
)
def __hash__(self) -> int:
- initial_hash = hash((self._index_type, self._name))
+ initial_hash = hash(
+ (self._index_type, self._name, frozenset(self._properties.items()))
+ )
return reduce(
lambda result, field_name: 31 * result + hash(tuple(field_name)),
self._field_names,
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 6110a09409..eeca69bca2 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
@@ -31,6 +31,8 @@ class IndexSerdes(SerdesUtilsBase, JsonSerializable[Index]):
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()
return result
@@ -51,5 +53,8 @@ class IndexSerdes(SerdesUtilsBase, JsonSerializable[Index]):
index_type = Index.IndexType(data[cls.INDEX_TYPE].upper())
return IndexDTO(
- index_type, data.get(cls.INDEX_NAME), data[cls.INDEX_FIELD_NAMES]
+ index_type,
+ data.get(cls.INDEX_NAME),
+ data[cls.INDEX_FIELD_NAMES],
+ data.get("properties"),
)
diff --git a/clients/client-python/tests/unittests/dto/rel/test_index_dto.py
b/clients/client-python/tests/unittests/dto/rel/test_index_dto.py
index 9a17862a23..a28926c1cb 100644
--- a/clients/client-python/tests/unittests/dto/rel/test_index_dto.py
+++ b/clients/client-python/tests/unittests/dto/rel/test_index_dto.py
@@ -70,3 +70,29 @@ class TestIndexDTO(unittest.TestCase):
dto_dict[slack_dto] = len(names)
self.assertEqual(len(dto_dict), len(names))
self.assertEqual(dto_dict[slack_dto], len(names))
+
+ def test_index_dto_properties_are_part_of_equality_and_hash(self):
+ field_names = [["embedding"]]
+ l2_index = IndexDTO(
+ Index.IndexType.DATA_SKIPPING_VECTOR_SIMILARITY,
+ "vector_index",
+ field_names,
+ {"distance_function": "L2Distance", "dimensions": "3"},
+ )
+ cosine_index = IndexDTO(
+ Index.IndexType.DATA_SKIPPING_VECTOR_SIMILARITY,
+ "vector_index",
+ field_names,
+ {"distance_function": "cosineDistance", "dimensions": "3"},
+ )
+ same_l2_index = IndexDTO(
+ Index.IndexType.DATA_SKIPPING_VECTOR_SIMILARITY,
+ "vector_index",
+ field_names,
+ {"dimensions": "3", "distance_function": "L2Distance"},
+ )
+
+ self.assertNotEqual(l2_index, cosine_index)
+ self.assertEqual(l2_index, same_l2_index)
+ self.assertEqual(hash(l2_index), hash(same_l2_index))
+ self.assertEqual(len({l2_index, cosine_index}), 2)
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 790f102a24..18b960b87c 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
@@ -116,3 +116,32 @@ class TestIndexSerdes(unittest.TestCase):
json_dict = json.loads(json_string)
serialized_dict = json.loads(mock_data_class.to_json())
self.assertDictEqual(json_dict, serialized_dict)
+
+ def test_vector_similarity_index_properties_round_trip(self):
+ json_string = """
+ {
+ "indexes": [
+ {
+ "indexType": "DATA_SKIPPING_VECTOR_SIMILARITY",
+ "name": "idx_vector",
+ "fieldNames": [["embedding"]],
+ "properties": {
+ "type": "hnsw",
+ "distance_function": "L2Distance",
+ "dimensions": "3"
+ }
+ }
+ ]
+ }
+ """
+
+ index = MockDataClass.from_json(json_string).indexes[0]
+ self.assertEqual(index.type(),
Index.IndexType.DATA_SKIPPING_VECTOR_SIMILARITY)
+ self.assertEqual(
+ index.properties(),
+ {"type": "hnsw", "distance_function": "L2Distance", "dimensions":
"3"},
+ )
+ self.assertDictEqual(
+ json.loads(json_string),
+ json.loads(MockDataClass(indexes=[index]).to_json()),
+ )
diff --git a/clients/client-python/tests/unittests/test_relational_catalog.py
b/clients/client-python/tests/unittests/test_relational_catalog.py
index 86a3edc269..21e779980d 100644
--- a/clients/client-python/tests/unittests/test_relational_catalog.py
+++ b/clients/client-python/tests/unittests/test_relational_catalog.py
@@ -15,6 +15,7 @@
# specific language governing permissions and limitations
# under the License.
+import json
import unittest
from http.client import HTTPResponse
from unittest.mock import Mock, patch
@@ -22,6 +23,7 @@ from unittest.mock import Mock, patch
from gravitino.api.authorization.privileges import Privilege
from gravitino.api.rel.column import Column
from gravitino.api.rel.dialects import Dialects
+from gravitino.api.rel.indexes.index import Index
from gravitino.api.rel.sql_representation import SQLRepresentation
from gravitino.api.rel.table_change import TableChange
from gravitino.api.rel.types.types import Types
@@ -151,6 +153,37 @@ class TestRelationalCatalog(unittest.TestCase):
table = self.catalog.load_table(self.table_identifier)
self.assertEqual(table.name(), self.table_dto.name())
+ def test_load_table_with_vector_similarity_index(self):
+ response_json = json.loads(TableResponse(0, self.table_dto).to_json())
+ response_json["table"]["indexes"] = [
+ {
+ "indexType": "DATA_SKIPPING_VECTOR_SIMILARITY",
+ "name": "idx_vector",
+ "fieldNames": [["id"]],
+ "properties": {
+ "type": "hnsw",
+ "distance_function": "L2Distance",
+ "dimensions": "3",
+ },
+ }
+ ]
+ mock_resp = self._get_mock_http_resp(json.dumps(response_json))
+
+ with patch(
+ "gravitino.utils.http_client.HTTPClient.get",
return_value=mock_resp
+ ):
+ table = self.catalog.load_table(self.table_identifier)
+
+ self.assertEqual(table.name(), self.table_dto.name())
+ self.assertEqual(len(table.index()), 1)
+ self.assertEqual(
+ table.index()[0].type(),
Index.IndexType.DATA_SKIPPING_VECTOR_SIMILARITY
+ )
+ self.assertEqual(
+ table.index()[0].properties(),
+ {"type": "hnsw", "distance_function": "L2Distance", "dimensions":
"3"},
+ )
+
def test_load_table_with_required_privilege_names(self):
resp_body = TableResponse(0, self.table_dto)
mock_resp = self._get_mock_http_resp(resp_body.to_json())
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 2649667c4b..4d836e4bf6 100644
--- a/common/src/test/java/org/apache/gravitino/json/TestJsonUtils.java
+++ b/common/src/test/java/org/apache/gravitino/json/TestJsonUtils.java
@@ -511,6 +511,22 @@ public class TestJsonUtils {
Assertions.assertEquals(
objectMapper.readValue(expected, IndexDTO.class),
objectMapper.readValue(jsonValue, IndexDTO.class));
+
+ Index vectorIndex =
+ IndexDTO.builder()
+ .withIndexType(Index.IndexType.DATA_SKIPPING_VECTOR_SIMILARITY)
+ .withName("idx_vector")
+ .withFieldNames(new String[][] {{"embedding"}})
+ .withProperties(
+ Map.of("type", "hnsw", "distance_function", "L2Distance",
"dimensions", "3"))
+ .build();
+ jsonValue = JsonUtils.objectMapper().writeValueAsString(vectorIndex);
+ expected =
+
"{\"indexType\":\"DATA_SKIPPING_VECTOR_SIMILARITY\",\"name\":\"idx_vector\","
+ +
"\"fieldNames\":[[\"embedding\"]],\"properties\":{\"dimensions\":\"3\","
+ + "\"distance_function\":\"L2Distance\",\"type\":\"hnsw\"}}";
+ Assertions.assertEquals(expected, jsonValue);
+ Assertions.assertEquals(vectorIndex, objectMapper.readValue(jsonValue,
IndexDTO.class));
}
@Test
diff --git a/docs/jdbc-clickhouse-catalog.md b/docs/jdbc-clickhouse-catalog.md
index abbf4d6b8b..fc86b677aa 100644
--- a/docs/jdbc-clickhouse-catalog.md
+++ b/docs/jdbc-clickhouse-catalog.md
@@ -37,6 +37,8 @@ ClickHouse catalog is not included in the standard Gravitino
server distribution
|--------------------------------|-------------------------------------------|
| `24.8.x` (including `24.8.14`) | `0.7.1 ~ 0.8.4` |
+The `DATA_SKIPPING_VECTOR_SIMILARITY` index requires ClickHouse `25.8` or
later. This feature-specific requirement does not change the compatibility
baseline for other catalog capabilities.
+
:::tip
For other ClickHouse versions (not 24.8.x), the required JDBC driver version
may be different.
Use your staging validation results and the official ClickHouse documentation
as the final reference.
@@ -172,7 +174,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`, and `DATA_SKIPPING_TOKENBFV1` (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()`).
[...]
| 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.
|
@@ -291,11 +293,20 @@ The `engine_parameters` property applies to
`ReplacingMergeTree`, `SummingMergeT
- `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()`)
+ - `DATA_SKIPPING_VECTOR_SIMILARITY` (requires ClickHouse 25.8+; default
`GRANULARITY 100000000`; requires `type=hnsw`, `distance_function`, and
`dimensions` in `Index.properties()`)
+
+ `DATA_SKIPPING_VECTOR_SIMILARITY` supports `distance_function` values
`L2Distance` and `cosineDistance` on ClickHouse 25.8+. Optional properties are
`quantization` (`f64`, `f32`, `f16`, `bf16`, `i8`, or `b1`; default `bf16`),
`hnsw_max_connections_per_layer` (default 32), and
`hnsw_candidate_list_size_for_construction` (default 128). A value of 0 for
either HNSW numeric property requests the ClickHouse default. Custom
`granularity` must be a positive integer. See the [ClickHouse vector [...]
+
+ 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.
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.
+ 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.
### Partitioning, Sorting, and Distribution
diff --git a/docs/open-api/indexes.yaml b/docs/open-api/indexes.yaml
index af250b8967..cbcdbde96a 100644
--- a/docs/open-api/indexes.yaml
+++ b/docs/open-api/indexes.yaml
@@ -40,6 +40,7 @@ components:
- "data_skipping_set"
- "data_skipping_ngrambfv1"
- "data_skipping_tokenbfv1"
+ - "data_skipping_vector_similarity"
name:
type: string
description: The name of the index
diff --git a/docs/table-partitioning-distribution-sort-order-indexes.md
b/docs/table-partitioning-distribution-sort-order-indexes.md
index 65025ec836..6ce5b419ec 100644
--- a/docs/table-partitioning-distribution-sort-order-indexes.md
+++ b/docs/table-partitioning-distribution-sort-order-indexes.md
@@ -253,6 +253,7 @@ To define an indexed table, you should utilize the
following four components to
|---------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------|---------------------------|
| PRIMARY_KEY | The PRIMARY KEY is a column or set of columns that uniquely
identifies each row in a table. It enforces uniqueness and ensures that no two
rows have the same values in the specified columns. Additionally, the PRIMARY
KEY constraint automatically creates a unique index on the specified columns. |
`PRIMARY_KEY` | `IndexType.PRIMARY_KEY` |
| UNIQUE_KEY | The UNIQUE KEY constraint ensures that all values in a
specified column or set of columns are unique across the entire table. Unlike
the PRIMARY KEY constraint, a table can have multiple UNIQUE KEY constraints,
allowing for unique values in multiple columns or sets of columns.
| `UNIQUE_KEY` | `IndexType.UNIQUE_KEY` |
+| DATA_SKIPPING_VECTOR_SIMILARITY | ClickHouse HNSW index for approximate
nearest-neighbor search. See the [ClickHouse catalog
guide](./jdbc-clickhouse-catalog.md#table-indexes) for its properties and
server-version requirement. | `DATA_SKIPPING_VECTOR_SIMILARITY` |
`IndexType.DATA_SKIPPING_VECTOR_SIMILARITY` |
- Name. It defines the name of the index.
diff --git
a/integration-test-common/src/test/java/org/apache/gravitino/integration/test/container/ContainerSuite.java
b/integration-test-common/src/test/java/org/apache/gravitino/integration/test/container/ContainerSuite.java
index 9adff2730d..88e4cae939 100644
---
a/integration-test-common/src/test/java/org/apache/gravitino/integration/test/container/ContainerSuite.java
+++
b/integration-test-common/src/test/java/org/apache/gravitino/integration/test/container/ContainerSuite.java
@@ -29,12 +29,15 @@ import com.google.common.collect.ImmutableSet;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
+import java.io.InputStream;
import java.net.InetAddress;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileSystems;
import java.nio.file.Files;
+import java.nio.file.Path;
import java.nio.file.Paths;
+import java.nio.file.StandardCopyOption;
import java.nio.file.attribute.PosixFilePermission;
import java.util.ArrayList;
import java.util.EnumMap;
@@ -66,6 +69,7 @@ public class ContainerSuite implements Closeable {
private static final String NETWORK_NAME = "gravitino-ci-network";
private static final int CLICKHOUSE_CLUSTER_SIZE = 3;
private static final String CLICKHOUSE_CLUSTER_HOST_PREFIX =
"gravitino-ci-clickhouse-cluster-";
+ private static final String CLICKHOUSE_TEST_IMAGE_ENV =
"GRAVITINO_CLICKHOUSE_TEST_IMAGE";
private static Network network = null;
private static volatile HiveContainer hiveContainer;
@@ -585,6 +589,8 @@ public class ContainerSuite implements Closeable {
// Start ClickHouse container
ClickHouseContainer.Builder clickHouseBuilder =
ClickHouseContainer.builder()
+ .withImage(resolveClickHouseImage())
+ .withFilesToMount(resolveClickHouseTestFiles())
.withHostName("gravitino-ci-clickhouse")
.withEnvVars(
ImmutableMap.<String, String>builder()
@@ -617,6 +623,8 @@ public class ContainerSuite implements Closeable {
for (int node = 1; node <= CLICKHOUSE_CLUSTER_SIZE; node++) {
ClickHouseContainer.Builder clickHouseBuilder =
ClickHouseContainer.builder()
+ .withImage(resolveClickHouseImage())
+ .withFilesToMount(resolveClickHouseTestFiles())
.withHostName(CLICKHOUSE_CLUSTER_HOST_PREFIX + node)
.withEnvVars(
ImmutableMap.<String, String>builder()
@@ -982,4 +990,31 @@ public class ContainerSuite implements Closeable {
LOG.error("Failed to close ContainerEnvironment", e);
}
}
+
+ private static String resolveClickHouseImage() {
+ String configuredImage = System.getenv(CLICKHOUSE_TEST_IMAGE_ENV);
+ return configuredImage == null || configuredImage.isBlank()
+ ? ClickHouseContainer.DEFAULT_IMAGE
+ : configuredImage.trim();
+ }
+
+ private static Map<String, String> resolveClickHouseTestFiles() {
+ // The feature-specific ClickHouse test image is 25.8+, where Time types
require this setting.
+ String configuredImage = System.getenv(CLICKHOUSE_TEST_IMAGE_ENV);
+ if (configuredImage == null || configuredImage.isBlank()) {
+ return ImmutableMap.of();
+ }
+
+ try (InputStream settings =
+ Objects.requireNonNull(
+
ContainerSuite.class.getResourceAsStream("/clickhouse-test-users.xml"))) {
+ Path settingsPath =
Files.createTempFile("gravitino-clickhouse-test-users-", ".xml");
+ Files.copy(settings, settingsPath, StandardCopyOption.REPLACE_EXISTING);
+ settingsPath.toFile().deleteOnExit();
+ return ImmutableMap.of(
+ "/etc/clickhouse-server/users.d/gravitino-test-settings.xml",
settingsPath.toString());
+ } catch (IOException e) {
+ throw new IllegalStateException("Failed to resolve ClickHouse test user
settings", e);
+ }
+ }
}
diff --git
a/integration-test-common/src/test/resources/clickhouse-test-users.xml
b/integration-test-common/src/test/resources/clickhouse-test-users.xml
new file mode 100644
index 0000000000..8cd8901c74
--- /dev/null
+++ b/integration-test-common/src/test/resources/clickhouse-test-users.xml
@@ -0,0 +1,25 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+-->
+<clickhouse>
+ <profiles>
+ <default>
+ <enable_time_time64_type>1</enable_time_time64_type>
+ </default>
+ </profiles>
+</clickhouse>