diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 303bba5d8..dec60bc76 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -4,33 +4,118 @@ on: branches: - main pull_request: +concurrency: + group: tests-${{ github.ref }} + cancel-in-progress: true + +env: + IMG2VEC: cr.weaviate.io/semitechnologies/img2vec-pytorch:resnet50 + MINIO: minio/minio + MODEL2VEC: cr.weaviate.io/semitechnologies/model2vec-inference:minishlab-potion-base-4M + DOCKER_IMAGES_TAR: docker-images.tar jobs: - tests: - name: Tests + docker-cache: + name: Cache shared Docker images runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - with: - ref: ${{ github.sha }} - name: Login to Docker Hub if: ${{ !github.event.pull_request.head.repo.fork }} - uses: docker/login-action@v2 + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + - id: cache-check + uses: actions/cache/restore@v4 + env: + DOCKER_CACHE_KEY: docker-images-${{ env.IMG2VEC }}-${{ env.MINIO }}-${{ env.MODEL2VEC }} + with: + path: ${{ env.DOCKER_IMAGES_TAR }} + key: ${{ env.DOCKER_CACHE_KEY }} + lookup-only: true # Only check if cache exists, don't download + - name: Free Disk Space (Ubuntu) + uses: jlumbroso/free-disk-space@v1.3.1 + with: + tool-cache: false + android: true + dotnet: true + haskell: true + large-packages: true + docker-images: false + swap-storage: false + - name: Pull images + if: steps.cache-check.outputs.cache-hit != 'true' + run: | + docker pull $IMG2VEC + docker pull $MINIO + # docker pull $MODEL2VEC + docker save $IMG2VEC $MINIO -o $DOCKER_IMAGES_TAR + - name: Cache images + if: steps.cache-check.outputs.cache-hit != 'true' + uses: actions/cache/save@v4 + env: + DOCKER_CACHE_KEY: docker-images-${{ env.IMG2VEC }}-${{ env.MINIO }}-${{ env.MODEL2VEC }} + with: + path: ${{ env.DOCKER_IMAGES_TAR }} + key: ${{ env.DOCKER_CACHE_KEY }} + + maven-cache: + name: Cache Maven dependencies + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 + with: + distribution: 'zulu' + java-version: '17' + cache: 'maven' + - run: mvn dependency:go-offline + + test: + name: Test + runs-on: ubuntu-latest + needs: [ docker-cache, maven-cache] + strategy: + fail-fast: false + matrix: + WEAVIATE_VERSION: ["1.32.16", "1.33.4", "1.34.0"] + steps: + - uses: actions/checkout@v4 + + - uses: actions/cache/restore@v4 + env: + DOCKER_CACHE_KEY: docker-images-${{ env.IMG2VEC }}-${{ env.MINIO }}-${{ env.MODEL2VEC }} with: - username: ${{secrets.DOCKER_USERNAME}} - password: ${{secrets.DOCKER_PASSWORD}} - - name: Setup JDK - uses: actions/setup-java@v4 + path: ${{ env.DOCKER_IMAGES_TAR }} + key: ${{ env.DOCKER_CACHE_KEY }} + - name: Free Disk Space (Ubuntu) + uses: jlumbroso/free-disk-space@v1.3.1 + with: + tool-cache: false + android: true + dotnet: true + haskell: true + large-packages: true + docker-images: false + swap-storage: false + - name: Load Docker images + run: | + if [ -f $DOCKER_IMAGES_TAR ]; then + docker load -i $DOCKER_IMAGES_TAR + fi + - uses: actions/setup-java@v4 + name: Setup JDK with: distribution: 'zulu' java-version: '17' - - name: Run Build - run: mvn -DskipTests clean package - - name: Run Tests + cache: 'maven' + + - name: Run Tests (v${{ matrix.WEAVIATE_VERSION }}) env: OKTA_DUMMY_CI_PW: ${{ secrets.OKTA_DUMMY_CI_PW }} WCS_DUMMY_CI_PW: ${{ secrets.WCS_DUMMY_CI_PW }} OKTA_CLIENT_SECRET: ${{ secrets.OKTA_CLIENT_SECRET }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} OPENAI_APIKEY: ${{ secrets.OPENAI_APIKEY }} - run: mvn clean test + WEAVIATE_VERSION: ${{ matrix.WEAVIATE_VERSION }} + run: mvn verify -Dgpg.skip diff --git a/.gitignore b/.gitignore index 052460830..ee565b6c5 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,5 @@ target/ # maven-lombok-plugin .factorypath +# Surefire statistics for optimized execution time +.surefire-* diff --git a/pom.xml b/pom.xml index 68630eefb..a28dee7eb 100644 --- a/pom.xml +++ b/pom.xml @@ -52,7 +52,6 @@ 17 17 17 - 1.18.42 2.13.2 5.5.1 3.19.0 @@ -112,11 +111,6 @@ httpclient5 ${httpclient.version} - - org.projectlombok - lombok - ${lombok.version} - com.google.code.gson gson @@ -229,72 +223,17 @@ maven-surefire-plugin - 2.22.2 + 3.5.4 false - - - --add-opens=java.base/java.lang=ALL-UNNAMED - - - - listener - io.weaviate.containers.TestListener - - + classes + 4 + true + 1 + true + balanced - - org.projectlombok - lombok-maven-plugin - 1.18.20.0 - - - delombok-sources - generate-sources - - delombok - - - src/main/java - ${project.build.directory}/delombok - false - UTF-8 - - - - - - org.projectlombok - lombok - ${lombok.version} - - - - - org.apache.maven.plugins - maven-antrun-plugin - 3.0.0 - - - generate-delomboked-sources-jar - package - - run - - - - - - - - - org.codehaus.mojo versions-maven-plugin @@ -317,22 +256,6 @@ build-helper-maven-plugin 3.2.0 - - attach-delomboked-sources-jar - package - - attach-artifact - - - - - ${project.build.directory}/${project.build.finalName}-sources.jar - jar - sources - - - - add-test-source generate-test-sources @@ -384,7 +307,6 @@ - ${project.build.directory}/delombok all,-missing @@ -512,10 +434,6 @@ - - org.projectlombok - lombok-maven-plugin - org.xolstice.maven.plugins protobuf-maven-plugin diff --git a/src/it/java/io/weaviate/ConcurrentTest.java b/src/it/java/io/weaviate/ConcurrentTest.java index 57584e047..6e783ff06 100644 --- a/src/it/java/io/weaviate/ConcurrentTest.java +++ b/src/it/java/io/weaviate/ConcurrentTest.java @@ -10,9 +10,13 @@ import org.apache.commons.lang3.RandomStringUtils; import org.assertj.core.api.Assertions; +import org.assertj.core.api.Assumptions; import org.junit.Rule; import org.junit.rules.TestName; +import io.weaviate.client6.v1.internal.VersionSupport.SemanticVersion; +import io.weaviate.containers.Weaviate; + /** * ConcurrentTest is the base class for integration tests, which provides * utility methods to uniqualize collections and objects created in the @@ -110,4 +114,22 @@ public static void eventually(Callable cond, int intervalMillis, int ti throw new RuntimeException(ex); } } + + /** + * Skip the test if the version that the {@link Weaviate} + * container is running is older than the required one. + */ + public static void requireAtLeast(Weaviate.Version required) { + var actual = SemanticVersion.of(Weaviate.VERSION); + Assumptions.assumeThat(actual) + .as("requires at least %s, but running %s", required.semver, actual) + .isGreaterThanOrEqualTo(required.semver); + } + + public static void requireAtLeast(Weaviate.Version required, Runnable r) { + var actual = SemanticVersion.of(Weaviate.VERSION); + if (actual.compareTo(required.semver) >= 0) { + r.run(); + } + } } diff --git a/src/it/java/io/weaviate/containers/Container.java b/src/it/java/io/weaviate/containers/Container.java index 7c71ed980..304dbea91 100644 --- a/src/it/java/io/weaviate/containers/Container.java +++ b/src/it/java/io/weaviate/containers/Container.java @@ -18,18 +18,6 @@ public class Container { public static final Img2VecNeural IMG2VEC_NEURAL = Img2VecNeural.createDefault(); public static final MinIo MINIO = MinIo.createDefault(); - /** - * Stop all shared Testcontainers created in {@link #startAll}. - *

- * Testcontainer's Ryuk will reap any dangling containers after the tests - * finish. However, since {@link Weaviate} instances also hold a - * {@link WeaviateClient}, we want to stop them proactively to - * close client connections. - */ - static void stopAll() { - WEAVIATE.stop(); - } - public static ContainerGroup compose(Weaviate weaviate, GenericContainer... containers) { return new ContainerGroup(weaviate, containers); } diff --git a/src/it/java/io/weaviate/containers/TestListener.java b/src/it/java/io/weaviate/containers/TestListener.java deleted file mode 100644 index 72889125b..000000000 --- a/src/it/java/io/weaviate/containers/TestListener.java +++ /dev/null @@ -1,14 +0,0 @@ -package io.weaviate.containers; - -import org.junit.runner.Result; -import org.junit.runner.notification.RunListener; - -public class TestListener extends RunListener { - - @Override - public void testRunFinished(Result result) throws Exception { - Container.stopAll(); - super.testRunFinished(result); - } - -} diff --git a/src/it/java/io/weaviate/containers/Weaviate.java b/src/it/java/io/weaviate/containers/Weaviate.java index 1c7fc259e..4c063e0ad 100644 --- a/src/it/java/io/weaviate/containers/Weaviate.java +++ b/src/it/java/io/weaviate/containers/Weaviate.java @@ -16,18 +16,41 @@ import org.testcontainers.lifecycle.Startable; import org.testcontainers.weaviate.WeaviateContainer; +import io.weaviate.ConcurrentTest; import io.weaviate.client6.v1.api.Config; import io.weaviate.client6.v1.api.WeaviateClient; import io.weaviate.client6.v1.internal.ObjectBuilder; +import io.weaviate.client6.v1.internal.VersionSupport.SemanticVersion; public class Weaviate extends WeaviateContainer { - public static final String VERSION = "1.33.0"; public static final String DOCKER_IMAGE = "semitechnologies/weaviate"; + public static final String LATEST_VERSION = Version.V134.semver.toString(); + public static final String VERSION; + + static { + VERSION = System.getenv().getOrDefault("WEAVIATE_VERSION", LATEST_VERSION); + } public static String OIDC_ISSUER = "https://auth.wcs.api.weaviate.io/auth/realms/SeMI"; private volatile SharedClient clientInstance; private final String containerName; + public enum Version { + V132(1, 32), + V133(1, 33), + V134(1, 34); + + public final SemanticVersion semver; + + private Version(int major, int minor) { + this.semver = new SemanticVersion(major, minor); + } + + public void orSkip() { + ConcurrentTest.requireAtLeast(this); + } + } + /** * By default, testcontainer's name is only available after calling * {@link #start}. @@ -71,6 +94,22 @@ public WeaviateClient getClient() { return clientInstance; } + /** + * Get client that is not shared with other tests / callers. + * The returned client is not wrapped in an instance of {@link SharedClient}, + * so it can be auto-closed by the try-with-resources statement when it exists. + */ + public WeaviateClient getBareClient() { + if (!isRunning()) { + start(); + } + try { + return new WeaviateClient(Config.of(defaultConfigFn())); + } catch (Exception e) { + throw new RuntimeException("create WeaviateClient for Weaviate container", e); + } + } + /** * Create a new instance of WeaviateClient connected to this container. * Prefer using {@link #getClient} unless your test needs the initialization @@ -377,6 +416,7 @@ private void bindNodes(int gossip, int data, int raft) { nodes.forEach(node -> node .withEnv("CLUSTER_GOSSIP_BIND_PORT", String.valueOf(gossip)) .withEnv("CLUSTER_DATA_BIND_PORT", String.valueOf(data)) + .withEnv("REPLICA_MOVEMENT_ENABLED", "true") .withEnv("RAFT_PORT", String.valueOf(raft)) .withEnv("RAFT_BOOTSTRAP_EXPECT", "1")); diff --git a/src/it/java/io/weaviate/integration/AliasITest.java b/src/it/java/io/weaviate/integration/AliasITest.java index ab180d6f0..eb5bca8eb 100644 --- a/src/it/java/io/weaviate/integration/AliasITest.java +++ b/src/it/java/io/weaviate/integration/AliasITest.java @@ -4,16 +4,23 @@ import java.util.List; import org.assertj.core.api.Assertions; +import org.junit.BeforeClass; import org.junit.Test; import io.weaviate.ConcurrentTest; import io.weaviate.client6.v1.api.WeaviateClient; import io.weaviate.client6.v1.api.alias.Alias; import io.weaviate.containers.Container; +import io.weaviate.containers.Weaviate; public class AliasITest extends ConcurrentTest { private static final WeaviateClient client = Container.WEAVIATE.getClient(); + @BeforeClass + public static void __() { + Weaviate.Version.V132.orSkip(); + } + @Test public void test_aliasLifecycle() throws IOException { // Arrange diff --git a/src/it/java/io/weaviate/integration/BackupITest.java b/src/it/java/io/weaviate/integration/BackupITest.java index bf0997930..c540afaae 100644 --- a/src/it/java/io/weaviate/integration/BackupITest.java +++ b/src/it/java/io/weaviate/integration/BackupITest.java @@ -29,6 +29,8 @@ public class BackupITest extends ConcurrentTest { @Test public void test_lifecycle() throws IOException, TimeoutException { + Weaviate.Version.V132.orSkip(); // List backups not implemented in earlier versions + // Arrange String nsA = ns("A"), nsB = ns("B"), nsC = ns("C"), nsBig = ns("Big"); String backup_1 = ns("backup_1").toLowerCase(); @@ -122,6 +124,8 @@ public void test_lifecycle() throws IOException, TimeoutException { @Test public void test_lifecycle_async() throws ExecutionException, InterruptedException, Exception { + Weaviate.Version.V132.orSkip(); // List backups not implemented in earlier versions + // Arrange String nsA = ns("A"), nsB = ns("B"), nsC = ns("C"), nsBig = ns("Big"); String backup_1 = ns("backup_1").toLowerCase(); diff --git a/src/it/java/io/weaviate/integration/ClusterITest.java b/src/it/java/io/weaviate/integration/ClusterITest.java index b7716717e..baea17f70 100644 --- a/src/it/java/io/weaviate/integration/ClusterITest.java +++ b/src/it/java/io/weaviate/integration/ClusterITest.java @@ -14,6 +14,7 @@ import io.weaviate.client6.v1.api.cluster.replication.ReplicationState; import io.weaviate.client6.v1.api.cluster.replication.ReplicationType; import io.weaviate.containers.Weaviate; +import io.weaviate.containers.Weaviate.Version; public class ClusterITest extends ConcurrentTest { private static final WeaviateClient client = Weaviate.cluster(3).getClient(); @@ -58,6 +59,8 @@ public void test_listNodes() throws IOException { @Test public void test_replicateLifecycle() throws IOException { + Version.V132.orSkip(); + // Arrange // We must create the collection first before any shards exist on the nodes. diff --git a/src/it/java/io/weaviate/integration/CollectionsITest.java b/src/it/java/io/weaviate/integration/CollectionsITest.java index 4906d48d5..c28890430 100644 --- a/src/it/java/io/weaviate/integration/CollectionsITest.java +++ b/src/it/java/io/weaviate/integration/CollectionsITest.java @@ -22,6 +22,7 @@ import io.weaviate.client6.v1.api.collections.vectorindex.Hnsw; import io.weaviate.client6.v1.api.collections.vectorizers.SelfProvidedVectorizer; import io.weaviate.containers.Container; +import io.weaviate.containers.Weaviate; public class CollectionsITest extends ConcurrentTest { private static WeaviateClient client = Container.WEAVIATE.getClient(); @@ -96,31 +97,34 @@ public void testCrossReferences() throws IOException { } @Test - public void testListDeleteAll() throws IOException { - var nsA = ns("A"); - var nsB = ns("B"); - var nsC = ns("C"); - - client.collections.create(nsA); - client.collections.create(nsB); - client.collections.create(nsC); - - Assertions.assertThat(client.collections.exists(nsA)).isTrue(); - Assertions.assertThat(client.collections.exists(nsB)).isTrue(); - Assertions.assertThat(client.collections.exists(nsC)).isTrue(); - Assertions.assertThat(client.collections.exists(ns("X"))).isFalse(); - - var all = client.collections.list(); - Assertions.assertThat(all) - .hasSizeGreaterThanOrEqualTo(3) - .extracting(CollectionConfig::collectionName) - .contains(nsA, nsB, nsC); - - client.collections.deleteAll(); - - all = client.collections.list(); - Assertions.assertThat(all.isEmpty()); - + public void testListDeleteAll() throws Exception { + // Use a separate container for this test so as not to interfere + // with other tests. + try (final var _client = Weaviate.createDefault().getBareClient()) { + var nsA = ns("A"); + var nsB = ns("B"); + var nsC = ns("C"); + + _client.collections.create(nsA); + _client.collections.create(nsB); + _client.collections.create(nsC); + + Assertions.assertThat(_client.collections.exists(nsA)).isTrue(); + Assertions.assertThat(_client.collections.exists(nsB)).isTrue(); + Assertions.assertThat(_client.collections.exists(nsC)).isTrue(); + Assertions.assertThat(_client.collections.exists(ns("X"))).isFalse(); + + var all = _client.collections.list(); + Assertions.assertThat(all) + .hasSizeGreaterThanOrEqualTo(3) + .extracting(CollectionConfig::collectionName) + .contains(nsA, nsB, nsC); + + _client.collections.deleteAll(); + + all = _client.collections.list(); + Assertions.assertThat(all.isEmpty()); + } } @Test diff --git a/src/it/java/io/weaviate/integration/RbacITest.java b/src/it/java/io/weaviate/integration/RbacITest.java index 9bdbed2e9..ea61b96e7 100644 --- a/src/it/java/io/weaviate/integration/RbacITest.java +++ b/src/it/java/io/weaviate/integration/RbacITest.java @@ -1,7 +1,8 @@ package io.weaviate.integration; import java.io.IOException; -import java.util.Arrays; +import java.util.ArrayList; +import java.util.List; import org.assertj.core.api.Assertions; import org.assertj.core.api.InstanceOfAssertFactories; @@ -64,20 +65,29 @@ public void test_roles_Lifecycle() throws IOException { var myCollection = "Things"; var nsRole = ns("VectorOwner"); - Permission[] permissions = new Permission[] { - Permission.aliases("ThingsAlias", myCollection, AliasesPermission.Action.CREATE), - Permission.backups(myCollection, BackupsPermission.Action.MANAGE), - Permission.cluster(ClusterPermission.Action.READ), - Permission.nodes(myCollection, NodesPermission.Action.READ), - Permission.roles(VIEWER_ROLE, Scope.MATCH, RolesPermission.Action.CREATE), - Permission.collections(myCollection, CollectionsPermission.Action.CREATE), - Permission.data(myCollection, DataPermission.Action.UPDATE), - Permission.groups("my-group", GroupType.OIDC, GroupsPermission.Action.READ), - Permission.tenants(myCollection, "my-tenant", TenantsPermission.Action.DELETE), - Permission.users("my-user", UsersPermission.Action.READ), - Permission.replicate(myCollection, "my-shard", ReplicatePermission.Action.READ), + List permissions = new ArrayList<>() { + { + add(Permission.backups(myCollection, BackupsPermission.Action.MANAGE)); + add(Permission.cluster(ClusterPermission.Action.READ)); + add(Permission.nodes(myCollection, NodesPermission.Action.READ)); + add(Permission.roles(VIEWER_ROLE, Scope.MATCH, RolesPermission.Action.CREATE)); + add(Permission.collections(myCollection, CollectionsPermission.Action.CREATE)); + add(Permission.data(myCollection, DataPermission.Action.UPDATE)); + add(Permission.tenants(myCollection, "my-tenant", TenantsPermission.Action.DELETE)); + add(Permission.users("my-user", UsersPermission.Action.READ)); + add(Permission.replicate(myCollection, "my-shard", ReplicatePermission.Action.READ)); + } }; + requireAtLeast(Weaviate.Version.V132, () -> { + permissions.add( + Permission.aliases("ThingsAlias", myCollection, AliasesPermission.Action.CREATE)); + }); + requireAtLeast(Weaviate.Version.V133, () -> { + permissions.add( + Permission.groups("my-group", GroupType.OIDC, GroupsPermission.Action.READ)); + }); + // Act: create role client.roles.create(nsRole, permissions); @@ -86,7 +96,7 @@ public void test_roles_Lifecycle() throws IOException { .as("created role") .returns(nsRole, Role::name) .extracting(Role::permissions, InstanceOfAssertFactories.list(Permission.class)) - .containsAll(Arrays.asList(permissions)); + .containsAll(permissions); // Act:: add extra permissions var extra = new Permission[] { @@ -150,6 +160,8 @@ public void test_roles_userAssignments() throws IOException { @Test public void test_groups() throws IOException { + Weaviate.Version.V133.orSkip(); + var mediaGroup = "./media-group"; var friendGroup = "./friend-group"; diff --git a/src/main/java/io/weaviate/client6/v1/api/WeaviateClient.java b/src/main/java/io/weaviate/client6/v1/api/WeaviateClient.java index a4c51cbe9..63e14c3bf 100644 --- a/src/main/java/io/weaviate/client6/v1/api/WeaviateClient.java +++ b/src/main/java/io/weaviate/client6/v1/api/WeaviateClient.java @@ -13,6 +13,7 @@ import io.weaviate.client6.v1.internal.ObjectBuilder; import io.weaviate.client6.v1.internal.Timeout; import io.weaviate.client6.v1.internal.TokenProvider; +import io.weaviate.client6.v1.internal.VersionSupport; import io.weaviate.client6.v1.internal.grpc.DefaultGrpcTransport; import io.weaviate.client6.v1.internal.grpc.GrpcChannelOptions; import io.weaviate.client6.v1.internal.grpc.GrpcTransport; @@ -105,6 +106,10 @@ public WeaviateClient(Config config) { throw ex; } + if (!VersionSupport.isSupported(meta.version())) { + throw new WeaviateUnsupportedVersionException(meta.version()); + } + if (meta.grpcMaxMessageSize() != null) { grpcOpt = grpcOpt.withMaxMessageSize(meta.grpcMaxMessageSize()); } diff --git a/src/main/java/io/weaviate/client6/v1/api/WeaviateClientAsync.java b/src/main/java/io/weaviate/client6/v1/api/WeaviateClientAsync.java index 7858300ec..1e5b07ae3 100644 --- a/src/main/java/io/weaviate/client6/v1/api/WeaviateClientAsync.java +++ b/src/main/java/io/weaviate/client6/v1/api/WeaviateClientAsync.java @@ -15,6 +15,7 @@ import io.weaviate.client6.v1.internal.ObjectBuilder; import io.weaviate.client6.v1.internal.Timeout; import io.weaviate.client6.v1.internal.TokenProvider; +import io.weaviate.client6.v1.internal.VersionSupport; import io.weaviate.client6.v1.internal.grpc.DefaultGrpcTransport; import io.weaviate.client6.v1.internal.grpc.GrpcChannelOptions; import io.weaviate.client6.v1.internal.grpc.GrpcTransport; @@ -108,6 +109,10 @@ public WeaviateClientAsync(Config config) { throw ex; } + if (!VersionSupport.isSupported(meta.version())) { + throw new WeaviateUnsupportedVersionException(meta.version()); + } + if (meta.grpcMaxMessageSize() != null) { grpcOpt = grpcOpt.withMaxMessageSize(meta.grpcMaxMessageSize()); } diff --git a/src/main/java/io/weaviate/client6/v1/api/WeaviateUnsupportedVersionException.java b/src/main/java/io/weaviate/client6/v1/api/WeaviateUnsupportedVersionException.java new file mode 100644 index 000000000..1e4e27580 --- /dev/null +++ b/src/main/java/io/weaviate/client6/v1/api/WeaviateUnsupportedVersionException.java @@ -0,0 +1,17 @@ +package io.weaviate.client6.v1.api; + +import io.weaviate.client6.v1.internal.VersionSupport; + +/** + * This exception is thrown when the client refuses to talk to an unsupported + * version of the server, see {@link VersionSupport#MINIMAL_SUPPORTED_VERSION}. + */ +public class WeaviateUnsupportedVersionException extends WeaviateException { + public WeaviateUnsupportedVersionException(String actual) { + this(VersionSupport.MINIMAL_SUPPORTED_VERSION.toString(), actual); + } + + public WeaviateUnsupportedVersionException(String minimal, String actual) { + super("Server version %s is not supported. Earliest supported version is %s.".formatted(actual, minimal)); + } +} diff --git a/src/main/java/io/weaviate/client6/v1/api/backup/Backup.java b/src/main/java/io/weaviate/client6/v1/api/backup/Backup.java index 066fe2e08..9fa1c1025 100644 --- a/src/main/java/io/weaviate/client6/v1/api/backup/Backup.java +++ b/src/main/java/io/weaviate/client6/v1/api/backup/Backup.java @@ -34,7 +34,7 @@ public record Backup( /** Time at which the backup was completed, successfully or otherwise. */ @SerializedName("completedAt") OffsetDateTime completedAt, /** Backup size in GiB. */ - @SerializedName("size") Integer sizeGiB, + @SerializedName("size") Float sizeGiB, /** * This value indicates if a backup is being created or restored from. * For operations like LIST this value is null. diff --git a/src/main/java/io/weaviate/client6/v1/api/collections/data/ReferenceAddManyResponse.java b/src/main/java/io/weaviate/client6/v1/api/collections/data/ReferenceAddManyResponse.java index d0fc89ace..d7c70d7fa 100644 --- a/src/main/java/io/weaviate/client6/v1/api/collections/data/ReferenceAddManyResponse.java +++ b/src/main/java/io/weaviate/client6/v1/api/collections/data/ReferenceAddManyResponse.java @@ -24,11 +24,17 @@ public ReferenceAddManyResponse deserialize(JsonElement json, Type typeOfT, Json int i = 0; for (var el : json.getAsJsonArray()) { var result = el.getAsJsonObject().get("result").getAsJsonObject(); - if (result.get("status").getAsString().equals("FAILED")) { - var errorMsg = result - .get("errors").getAsJsonObject() - .get("error").getAsJsonArray() - .get(0).getAsString(); + if (result.get("status").getAsString().equals("FAILED") + && result.has("errors")) { + String errorMsg; + try { + errorMsg = result + .get("errors").getAsJsonObject() + .get("error").getAsJsonArray() + .get(0).getAsString(); + } catch (Exception e) { + errorMsg = result.get("errors").toString(); + } var batchErr = new BatchError(errorMsg, null, i); errors.add(batchErr); diff --git a/src/main/java/io/weaviate/client6/v1/api/rbac/roles/WeaviateRolesClient.java b/src/main/java/io/weaviate/client6/v1/api/rbac/roles/WeaviateRolesClient.java index b1120465a..4cea55f8b 100644 --- a/src/main/java/io/weaviate/client6/v1/api/rbac/roles/WeaviateRolesClient.java +++ b/src/main/java/io/weaviate/client6/v1/api/rbac/roles/WeaviateRolesClient.java @@ -29,6 +29,21 @@ public WeaviateRolesClient(RestTransport restTransport) { * or the server being unavailable. */ public void create(String roleName, Permission... permissions) throws IOException { + create(roleName, Arrays.asList(permissions)); + } + + /** + * Create a new role. + * + * @param roleName Role name. + * @param permissions Permissions granted to the role. + * @throws WeaviateApiException in case the server returned with an + * error status code. + * @throws IOException in case the request was not sent successfully + * due to a malformed request, a networking error + * or the server being unavailable. + */ + public void create(String roleName, List permissions) throws IOException { var role = new Role(roleName, permissions); this.restTransport.performRequest(new CreateRoleRequest(role), CreateRoleRequest._ENDPOINT); } diff --git a/src/main/java/io/weaviate/client6/v1/internal/VersionSupport.java b/src/main/java/io/weaviate/client6/v1/internal/VersionSupport.java new file mode 100644 index 000000000..05763677c --- /dev/null +++ b/src/main/java/io/weaviate/client6/v1/internal/VersionSupport.java @@ -0,0 +1,48 @@ +package io.weaviate.client6.v1.internal; + +import java.util.Arrays; + +public final class VersionSupport { + public static final SemanticVersion MINIMAL_SUPPORTED_VERSION = new SemanticVersion(1, 32); + + /** + * Returns true if the {@code version} is the same as or older than the + * {@link VersionSupport#MINIMAL_SUPPORTED_VERSION}. + */ + public static boolean isSupported(String version) { + var semver = SemanticVersion.of(version); + return semver.compareTo(MINIMAL_SUPPORTED_VERSION) >= 0; + } + + public record SemanticVersion(int major, int minor, String patch) implements Comparable { + + public SemanticVersion(int major, int minor) { + this(major, minor, null); + } + + /** + * Parse semantic version from a formatted string, + * e.g. {@code "(v)1.23.6-rc.1"}. + */ + public static SemanticVersion of(String version) { + var parts = version.replaceFirst("v", "").split("\\."); + var major = Integer.valueOf(parts[0].replaceAll("[^0-9]", "")); + var minor = Integer.valueOf(parts[1].replaceAll("[^0-9]", "")); + var patch = parts.length > 2 + ? String.join(".", Arrays.stream(parts, 2, parts.length).toList()) + : null; + return new SemanticVersion(major, minor, patch); + } + + @Override + public int compareTo(SemanticVersion that) { + var this_v = Integer.valueOf("%d%d".formatted(this.major, this.minor)); + var that_v = Integer.valueOf("%d%d".formatted(that.major, that.minor)); + return this_v.compareTo(that_v); + } + + public String toString() { + return String.join(".", String.valueOf(major), String.valueOf(minor), patch != null ? patch : ""); + } + } +} diff --git a/src/test/java/io/weaviate/client6/v1/internal/VersionSupportTest.java b/src/test/java/io/weaviate/client6/v1/internal/VersionSupportTest.java new file mode 100644 index 000000000..e735f6738 --- /dev/null +++ b/src/test/java/io/weaviate/client6/v1/internal/VersionSupportTest.java @@ -0,0 +1,40 @@ +package io.weaviate.client6.v1.internal; + +import org.assertj.core.api.Assertions; +import org.junit.Test; +import org.junit.runner.RunWith; + +import com.jparams.junit4.JParamsTestRunner; +import com.jparams.junit4.data.DataMethod; + +import io.weaviate.client6.v1.internal.VersionSupport.SemanticVersion; + +@RunWith(JParamsTestRunner.class) +public class VersionSupportTest { + public static Object[][] testCases() { + return new Object[][] { + { "1.31.6", "v1.32.1", true }, // can have a leading v + { "v1.33.0", "1.32.1", false }, // can have a leading v + { "2.36.2", "2.36.0-rc.3", true }, // patch ignored + { "1.12", "1.11", false }, // omit patch + { "0.55.6", "0.1.0", false }, // can start with zero + }; + } + + @Test + @DataMethod(source = VersionSupportTest.class, method = "testCases") + public void test_isSupported(String minimal, String actual, boolean isSupported) { + var v_minimal = SemanticVersion.of(minimal); + var v_actual = SemanticVersion.of(actual); + + if (isSupported) { + Assertions.assertThat(v_actual) + .describedAs("%s supported (minimal=%s)", actual, minimal) + .isGreaterThanOrEqualTo(v_minimal); + } else { + Assertions.assertThat(v_actual) + .describedAs("%s not supported (minimal=%s)", actual, minimal) + .isLessThan(v_minimal); + } + } +}