diff --git a/.github/workflows/docs_tests.yml b/.github/workflows/docs_tests.yml index 443ec7dc..4bba4c7a 100644 --- a/.github/workflows/docs_tests.yml +++ b/.github/workflows/docs_tests.yml @@ -29,10 +29,15 @@ on: description: 'C# client branch (leave empty to use the workflow default below)' required: false default: '' + go_client_branch: + description: 'Go client branch (leave empty to use the workflow default below)' + required: false + default: '' env: # Centralize versions for easier maintenance PYTHON_VERSION: "3.10" + GO_VERSION: "1.25" WEAVIATE_VERSION: "1.35.0" OLLAMA_VERSION: "0.9.6" CLIP_MODEL_TAG: "sentence-transformers-clip-ViT-B-32" @@ -41,6 +46,7 @@ env: jobs: test-agents: name: Test Agents + if: github.ref != 'refs/heads/docs/go-v6-scaffold' runs-on: ubuntu-latest timeout-minutes: 30 @@ -93,6 +99,7 @@ jobs: test-python: name: Test Python + if: github.ref != 'refs/heads/docs/go-v6-scaffold' runs-on: ubuntu-latest timeout-minutes: 30 @@ -256,6 +263,7 @@ jobs: test-typescript: name: Test TypeScript + if: github.ref != 'refs/heads/docs/go-v6-scaffold' runs-on: ubuntu-latest timeout-minutes: 30 @@ -426,6 +434,7 @@ jobs: test-java: name: Test Java + if: github.ref != 'refs/heads/docs/go-v6-scaffold' runs-on: ubuntu-latest timeout-minutes: 30 @@ -592,8 +601,137 @@ jobs: done docker system prune -f --volumes 2>/dev/null || true + test-go: + name: Test Go + runs-on: ubuntu-latest + timeout-minutes: 30 + + # Smoke lane for the Go v6 client (_includes/code/go-v6). Mirrors "Test Java" + # but intentionally omits the Ollama-model and Keycloak steps: the smoke set + # (connect / create-collection / insert / basic fetch) needs neither an + # embedding provider nor OIDC. Broaden this when the Go docs breadth lands. + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Free disk space + run: | + sudo rm -rf /usr/share/dotnet & + sudo rm -rf /opt/ghc & + sudo rm -rf /usr/local/share/boost & + sudo rm -rf "$AGENT_TOOLSDIRECTORY" & + sudo rm -rf /usr/local/lib/android & + wait + + - name: Set up test environment + uses: ./.github/actions/setup-test-env + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + cache-dependency-path: _includes/code/go-v6/go.sum + + - name: Clone Go client and wire replace + run: | + GO_BRANCH="${{ inputs.go_client_branch || 'v6' }}" + echo "๐Ÿ“ฆ Wiring Go client from branch: $GO_BRANCH" + # v6 is not published to the module proxy yet, so the committed go.mod + # requires a placeholder version and CI redirects it to a fresh clone. + git clone --depth 1 -b "$GO_BRANCH" https://github.com/weaviate/weaviate-go-client.git /tmp/go-client + cd _includes/code/go-v6 + go mod edit -replace github.com/weaviate/weaviate-go-client/v6=/tmp/go-client + go mod tidy + echo "โœ… Go client wired via replace directive" + + - name: Start services + run: | + echo "๐Ÿš€ Starting Weaviate and Ollama services..." + mkdir -p ~/.ollama + ./tests/start-weaviate.sh + + sleep 5 + echo "โœ… Services started:" + docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" + + - name: Wait for all services to be ready + run: | + echo "โณ Waiting for services to be ready..." + + declare -A services=( + ["8099"]="Weaviate Main" + ["8580"]="Weaviate RBAC" + ["8080"]="Weaviate Instance 1" + ["8090"]="Weaviate Instance 2" + ["8280"]="Weaviate Instance 3" + ["8180"]="Weaviate Instance 4" + ["8181"]="Weaviate Instance 5" + ["8182"]="Weaviate Instance 6" + ) + + for port in "${!services[@]}"; do + service_name="${services[$port]}" + echo -n "Checking $service_name (port $port)... " + + if timeout 60 bash -c "until curl -sf http://localhost:$port/v1/.well-known/ready &>/dev/null; do sleep 2; done"; then + echo "โœ… Ready" + else + echo "โš ๏ธ Not responding (may be optional)" + fi + done + + echo "โœ… Service check completed" + + - name: Record test start time + run: echo "TEST_START_TIME=$(date +%s)" >> $GITHUB_ENV + + - name: Run Go tests + id: run-tests + continue-on-error: true + env: + DOCS_GITHUB_ENV: "true" + WEAVIATE_URL: ${{ secrets.WEAVIATE_URL }} + WEAVIATE_HOST: ${{ secrets.WEAVIATE_HOST }} + WEAVIATE_HOSTNAME: ${{ secrets.WEAVIATE_HOSTNAME }} + WEAVIATE_HTTP_HOST: ${{ secrets.WEAVIATE_HTTP_HOST }} + WEAVIATE_GRPC_HOST: ${{ secrets.WEAVIATE_GRPC_HOST }} + WEAVIATE_API_KEY: ${{ secrets.WEAVIATE_API_KEY }} + WEAVIATE_LOCAL_API_KEY: ${{ secrets.WEAVIATE_LOCAL_API_KEY }} + run: | + echo "๐Ÿงช Running Go tests..." + uv run pytest -m "go" -v --tb=short --durations=10 -s --junitxml=pytest-results.xml + echo "โœ… Tests completed" + + - name: Handle test results + if: always() + uses: ./.github/actions/handle-test-results + with: + test-outcome: ${{ steps.run-tests.outcome }} + test-type: 'Go' + languages-tested: 'Go' + slack-bot-token: ${{ secrets.TESTING_CI_SLACK_BOT }} + + - name: Stop services + if: always() + run: | + echo "๐Ÿ›‘ Stopping services..." + ./tests/stop-weaviate.sh 2>/dev/null || echo "โš ๏ธ Some services may have already stopped" + + - name: Cleanup Docker resources + if: always() + run: | + for compose_file in tests/docker-compose*.yml; do + if [[ -f "$compose_file" ]]; then + docker compose -f "$compose_file" down -v --remove-orphans 2>/dev/null || true + fi + done + docker system prune -f --volumes 2>/dev/null || true + test-csharp: name: Test C# + if: github.ref != 'refs/heads/docs/go-v6-scaffold' runs-on: ubuntu-latest timeout-minutes: 30 diff --git a/_includes/code/connections/oidc-connect.mdx b/_includes/code/connections/oidc-connect.mdx index d05b5b98..789c2d92 100644 --- a/_includes/code/connections/oidc-connect.mdx +++ b/_includes/code/connections/oidc-connect.mdx @@ -5,6 +5,7 @@ import FilteredTextBlock from '@site/src/components/Documentation/FilteredTextBl import PyV4Code from '!!raw-loader!/_includes/code/connections/connect-python-v4.py'; import TsV3Code from '!!raw-loader!/_includes/code/connections/connect-ts-v3.ts'; import GoCode from '!!raw-loader!/_includes/code/connections/connect.go'; +import GoV6Code from '!!raw-loader!/_includes/code/go-v6/connect_test.go'; import JavaV6Code from '!!raw-loader!/_includes/code/java-v6/src/test/java/ConnectionTest.java'; import CSharpCode from '!!raw-loader!/_includes/code/csharp/ConnectionTest.cs'; @@ -33,6 +34,14 @@ import CSharpCode from '!!raw-loader!/_includes/code/csharp/ConnectionTest.cs'; language="go" /> + + + collection %q", a.Alias, a.Collection) + } + // END ListAllAliases +} + +// TestListCollectionAliases keeps only the aliases that point at one collection. +func TestListCollectionAliases(t *testing.T) { + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + setupArticleForAliases(t, client) + ensureAlias(t, client, "ArticlesProd", "Article") + defer cleanupAliases(ctx, client) + + // START ListCollectionAliases + aliases, err := client.Alias.List(ctx) + if err != nil { + t.Fatal(err) + } + // The client lists all aliases; filter by the target collection. + for _, a := range aliases { + if a.Collection == "Article" { + t.Logf("alias %q -> collection %q", a.Alias, a.Collection) + } + } + // END ListCollectionAliases +} + +// TestGetAlias fetches a single alias by name. +func TestGetAlias(t *testing.T) { + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + setupArticleForAliases(t, client) + ensureAlias(t, client, "ArticlesProd", "Article") + defer cleanupAliases(ctx, client) + + // START GetAlias + alias, err := client.Alias.Get(ctx, "ArticlesProd") + if err != nil { + t.Fatal(err) + } + t.Logf("alias %q -> collection %q", alias.Alias, alias.Collection) + // END GetAlias +} + +// TestUpdateAlias re-points an existing alias at a different collection. +func TestUpdateAlias(t *testing.T) { + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + setupArticleForAliases(t, client) + ensureAlias(t, client, "ArticlesProd", "Article") + defer cleanupAliases(ctx, client) + + // START UpdateAlias + err := client.Alias.Update(ctx, collections.Alias{ + Alias: "ArticlesProd", + Collection: "ArticlesV2", + }) + // END UpdateAlias + if err != nil { + t.Fatal(err) + } +} + +// TestDeleteAlias removes an alias. The underlying collection is untouched. +func TestDeleteAlias(t *testing.T) { + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + setupArticleForAliases(t, client) + ensureAlias(t, client, "ArticlesProd", "Article") + defer cleanupAliases(ctx, client) + + // START DeleteAlias + err := client.Alias.Delete(ctx, "ArticlesProd") + // END DeleteAlias + if err != nil { + t.Fatal(err) + } +} + +// TestUseAlias queries through an alias. Anywhere a collection name is expected, +// an alias name can be used in its place. +func TestUseAlias(t *testing.T) { + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + setupArticleForAliases(t, client) + ensureAlias(t, client, "ArticlesProd", "Article") + defer cleanupAliases(ctx, client) + + // START UseAlias + // "ArticlesProd" is an alias; the query runs against its target collection. + articles := client.Collections.Use("ArticlesProd") + response, err := articles.Query.OverAll(ctx, query.OverAll{ + Limit: 2, + }) + if err != nil { + t.Fatal(err) + } + for _, obj := range response.Objects { + t.Logf("%v", obj.Properties) + } + // END UseAlias +} diff --git a/_includes/code/go-v6/backup_test.go b/_includes/code/go-v6/backup_test.go new file mode 100644 index 00000000..d22fad6b --- /dev/null +++ b/_includes/code/go-v6/backup_test.go @@ -0,0 +1,119 @@ +package main + +import ( + "context" + "testing" + "time" + + "github.com/weaviate/weaviate-go-client/v6/backup" +) + +// The backup snippets below require a configured backup backend (for example the +// filesystem module). They are kept out of the CI run set (compile-only) and +// skip when executed directly. + +// TestCreateBackup starts a backup and waits for it to complete. +func TestCreateBackup(t *testing.T) { + t.Skip("requires a Weaviate instance with a backup backend module enabled") + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + // START CreateBackup + info, err := client.Backup.Create(ctx, backup.CreateOptions{ + Backend: "filesystem", + ID: "my-backup", + }) + if err != nil { + t.Fatal(err) + } + + // Block until the backup finishes. The default polling interval is one + // second; override it with backup.WithPollingInterval. + info, err = backup.AwaitCompletion(ctx, info, backup.WithPollingInterval(2*time.Second)) + if err != nil { + t.Fatal(err) + } + t.Logf("backup %q finished with status %s", info.ID, info.Status) + // END CreateBackup +} + +// TestStatusCreateBackup polls the status of an in-progress backup creation. +func TestStatusCreateBackup(t *testing.T) { + t.Skip("requires a Weaviate instance with a backup backend module enabled") + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + // START StatusCreateBackup + info, err := client.Backup.GetCreateStatus(ctx, backup.GetStatusOptions{ + Backend: "filesystem", + ID: "my-backup", + }) + if err != nil { + t.Fatal(err) + } + t.Logf("backup %q status: %s", info.ID, info.Status) + // END StatusCreateBackup +} + +// TestCancelBackup cancels an in-progress backup creation. +func TestCancelBackup(t *testing.T) { + t.Skip("requires a Weaviate instance with a backup backend module enabled") + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + // START CancelBackup + err := client.Backup.CancelCreate(ctx, backup.CancelOptions{ + Backend: "filesystem", + ID: "my-backup", + }) + // END CancelBackup + if err != nil { + t.Fatal(err) + } +} + +// TestRestoreBackup restores a backup and waits for it to complete. +func TestRestoreBackup(t *testing.T) { + t.Skip("requires a Weaviate instance with a backup backend module enabled") + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + // START RestoreBackup + info, err := client.Backup.Restore(ctx, backup.RestoreOptions{ + Backend: "filesystem", + ID: "my-backup", + }) + if err != nil { + t.Fatal(err) + } + + info, err = backup.AwaitCompletion(ctx, info) + if err != nil { + t.Fatal(err) + } + t.Logf("restore of %q finished with status %s", info.ID, info.Status) + // END RestoreBackup +} + +// TestStatusRestoreBackup polls the status of an in-progress backup restore. +func TestStatusRestoreBackup(t *testing.T) { + t.Skip("requires a Weaviate instance with a backup backend module enabled") + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + // START StatusRestoreBackup + info, err := client.Backup.GetRestoreStatus(ctx, backup.GetStatusOptions{ + Backend: "filesystem", + ID: "my-backup", + }) + if err != nil { + t.Fatal(err) + } + t.Logf("restore of %q status: %s", info.ID, info.Status) + // END StatusRestoreBackup +} diff --git a/_includes/code/go-v6/collection_config_test.go b/_includes/code/go-v6/collection_config_test.go new file mode 100644 index 00000000..7cbf79d7 --- /dev/null +++ b/_includes/code/go-v6/collection_config_test.go @@ -0,0 +1,129 @@ +package main + +import ( + "context" + "testing" + "time" + + "github.com/weaviate/weaviate-go-client/v6/collections" +) + +// The inverted-index, replication, and sharding snippets below run against a +// live server. They are kept out of the CI run set (compile-only) and skip +// when executed directly. + +// TestEnableInvertedIndex turns on property-level inverted indexes for +// filtering, searching, and range filtering. +func TestEnableInvertedIndex(t *testing.T) { + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + _ = client.Collections.Delete(ctx, "Article") + defer client.Collections.Delete(ctx, "Article") + + // START EnableInvertedIndex + _, err := client.Collections.Create(ctx, collections.Collection{ + Name: "Article", + Properties: []collections.Property{ + {Name: "title", DataType: collections.DataTypeText, IndexFilterable: true, IndexSearchable: true}, + {Name: "wordCount", DataType: collections.DataTypeInt, IndexRangeFilters: true}, + }, + }) + // END EnableInvertedIndex + if err != nil { + t.Fatal(err) + } +} + +// TestSetInvertedIndexParams configures collection-level inverted index +// parameters, including BM25 tuning and stopwords. +func TestSetInvertedIndexParams(t *testing.T) { + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + _ = client.Collections.Delete(ctx, "Article") + defer client.Collections.Delete(ctx, "Article") + + // START SetInvertedIndexParams + invertedIndex := &collections.InvertedIndexConfig{ + BM25: &collections.BM25Config{ + B: 0.75, + K1: 1.2, + }, + Stopwords: &collections.StopwordConfig{ + Preset: "en", + Additions: []string{"star", "nebula"}, + Removals: []string{"a", "the"}, + }, + IndexNullState: true, + IndexPropertyLength: true, + IndexTimestamps: true, + } + _, err := client.Collections.Create(ctx, collections.Collection{ + Name: "Article", + Properties: []collections.Property{ + {Name: "title", DataType: collections.DataTypeText}, + }, + InvertedIndex: invertedIndex, + }) + // END SetInvertedIndexParams + if err != nil { + t.Fatal(err) + } +} + +// TestAllReplicationSettings configures replication, including the deletion +// resolution strategy and async replication tuning. +func TestAllReplicationSettings(t *testing.T) { + t.Skip("requires a running Weaviate instance") + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + _ = client.Collections.Delete(ctx, "Article") + defer client.Collections.Delete(ctx, "Article") + + // START AllReplicationSettings + replication := &collections.ReplicationConfig{ + Factor: 3, + DeletionStrategy: collections.TimeBasedResolution, + AsyncReplication: &collections.AsyncReplicationConfig{ + HashTreeHeight: 16, + ReplicationFrequency: 30 * time.Second, + }, + } + _, err := client.Collections.Create(ctx, collections.Collection{ + Name: "Article", + Replication: replication, + }) + // END AllReplicationSettings + if err != nil { + t.Fatal(err) + } +} + +// TestShardingSettings configures sharding for the collection. +func TestShardingSettings(t *testing.T) { + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + _ = client.Collections.Delete(ctx, "Article") + defer client.Collections.Delete(ctx, "Article") + + // START ShardingSettings + _, err := client.Collections.Create(ctx, collections.Collection{ + Name: "Article", + Sharding: &collections.ShardingConfig{ + VirtualPerPhysical: 128, + DesiredCount: 1, + DesiredVirtualCount: 128, + }, + }) + // END ShardingSettings + if err != nil { + t.Fatal(err) + } +} diff --git a/_includes/code/go-v6/compression_test.go b/_includes/code/go-v6/compression_test.go new file mode 100644 index 00000000..f69ffd88 --- /dev/null +++ b/_includes/code/go-v6/compression_test.go @@ -0,0 +1,92 @@ +package main + +import ( + "context" + "testing" + + "github.com/weaviate/weaviate-go-client/v6/collections" + "github.com/weaviate/weaviate-go-client/v6/collections/compression" + "github.com/weaviate/weaviate-go-client/v6/collections/vectorindex" + "github.com/weaviate/weaviate-go-client/v6/modules/model2vec" +) + +// The compression snippets below run against a live server. They are kept out +// of the CI run set (compile-only) and skip when executed directly. Rotational +// Quantization (RQ) is enabled per named vector through its VectorConfig. + +// TestEnableRQ enables 8-bit RQ for a new collection at creation time. +func TestEnableRQ(t *testing.T) { + t.Skip("requires a running Weaviate instance") + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + _ = client.Collections.Delete(ctx, "Article") + defer client.Collections.Delete(ctx, "Article") + + // START EnableRQ + _, err := client.Collections.Create(ctx, collections.Collection{ + Name: "Article", + Vectors: map[string]collections.VectorConfig{ + "default": {Vectorizer: model2vec.Text2Vec{}, Compression: compression.RQ{Bits: 8}}, + }, + }) + // END EnableRQ + if err != nil { + t.Fatal(err) + } +} + +// TestEnableRQ1Bit enables 1-bit RQ for a new collection at creation time. +func TestEnableRQ1Bit(t *testing.T) { + t.Skip("requires a running Weaviate instance") + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + _ = client.Collections.Delete(ctx, "Article") + defer client.Collections.Delete(ctx, "Article") + + // START 1BitEnableRQ + _, err := client.Collections.Create(ctx, collections.Collection{ + Name: "Article", + Vectors: map[string]collections.VectorConfig{ + "default": {Vectorizer: model2vec.Text2Vec{}, Compression: compression.RQ{Bits: 1}}, + }, + }) + // END 1BitEnableRQ + if err != nil { + t.Fatal(err) + } +} + +// TestRQWithOptions enables RQ and tunes its parameters alongside the vector +// index configuration. +func TestRQWithOptions(t *testing.T) { + t.Skip("requires a running Weaviate instance") + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + _ = client.Collections.Delete(ctx, "Article") + defer client.Collections.Delete(ctx, "Article") + + // START RQWithOptions + vectorConfig := collections.VectorConfig{ + Index: vectorindex.HFresh{Distance: vectorindex.DistanceCosine}, + Compression: compression.RQ{ + Bits: 8, + RescoreLimit: 20, + Cache: true, + }, + Vectorizer: model2vec.Text2Vec{}, + } + _, err := client.Collections.Create(ctx, collections.Collection{ + Name: "Article", + Vectors: map[string]collections.VectorConfig{"default": vectorConfig}, + }) + // END RQWithOptions + if err != nil { + t.Fatal(err) + } +} diff --git a/_includes/code/go-v6/connect_test.go b/_includes/code/go-v6/connect_test.go new file mode 100644 index 00000000..de8da281 --- /dev/null +++ b/_includes/code/go-v6/connect_test.go @@ -0,0 +1,195 @@ +package main + +import ( + "context" + "net/http" + "os" + "testing" + + weaviate "github.com/weaviate/weaviate-go-client/v6" + "golang.org/x/oauth2" +) + +// TestConnectLocalNoAuth connects to a local instance with no authentication. +// This doubles as the connection smoke test for the docs test stack. +func TestConnectLocalNoAuth(t *testing.T) { + ctx := context.Background() + + // START LocalNoAuth + client, err := weaviate.NewLocal(ctx) + if err != nil { + t.Fatal(err) + } + defer client.Close() + // END LocalNoAuth + + ready, err := client.IsReady(ctx) + if err != nil { + t.Fatal(err) + } + if !ready { + t.Fatal("weaviate is not ready") + } +} + +// TestConnectCustomURL connects to an instance on a custom host and port. +func TestConnectCustomURL(t *testing.T) { + ctx := context.Background() + + // START CustomURL + client, err := weaviate.NewClient(ctx, + weaviate.WithScheme("http"), + weaviate.WithHTTPHost("localhost"), + weaviate.WithHTTPPort(8080), + weaviate.WithGRPCHost("localhost"), + weaviate.WithGRPCPort(50051), + ) + if err != nil { + t.Fatal(err) + } + defer client.Close() + // END CustomURL + + if _, err := client.IsReady(ctx); err != nil { + t.Fatal(err) + } +} + +// TestConnectLocalAuth connects to a local instance with a Weaviate API key. +// +// It is skipped in docs CI: the default-port local Weaviate there is anonymous, +// while the API-key instance runs on a non-default port that NewLocal's defaults +// cannot target. Unlike the Python and TypeScript snippets, Go snippets are not +// port-substituted before the tests run, so this would dial the anonymous +// instance and fail with HTTP 401. The snippet is still compiled and rendered. +func TestConnectLocalAuth(t *testing.T) { + t.Skip("docs CI local Weaviate is anonymous on the default port; the API-key instance is on a non-default port that NewLocal cannot target") + ctx := context.Background() + + // START LocalAuth + client, err := weaviate.NewLocal(ctx, + weaviate.WithAPIKey(os.Getenv("WEAVIATE_API_KEY")), + ) + if err != nil { + t.Fatal(err) + } + defer client.Close() + // END LocalAuth + + if _, err := client.IsReady(ctx); err != nil { + t.Fatal(err) + } +} + +// TestConnectLocalThirdPartyAPIKeys passes a third-party inference key as a +// request header alongside a local connection. +func TestConnectLocalThirdPartyAPIKeys(t *testing.T) { + if os.Getenv("COHERE_API_KEY") == "" { + t.Skip("COHERE_API_KEY must be set for the third-party key connection test") + } + ctx := context.Background() + + // START LocalThirdPartyAPIKeys + client, err := weaviate.NewLocal(ctx, + weaviate.WithHeader(http.Header{ + "X-Cohere-Api-Key": []string{os.Getenv("COHERE_API_KEY")}, + }), + ) + if err != nil { + t.Fatal(err) + } + defer client.Close() + // END LocalThirdPartyAPIKeys + + if _, err := client.IsReady(ctx); err != nil { + t.Fatal(err) + } +} + +// TestConnectCloud connects to a Weaviate Cloud instance with an API key. +func TestConnectCloud(t *testing.T) { + if os.Getenv("WEAVIATE_URL") == "" || os.Getenv("WEAVIATE_API_KEY") == "" { + t.Skip("WEAVIATE_URL and WEAVIATE_API_KEY must be set for the cloud connection test") + } + ctx := context.Background() + + // START APIKeyWCD + client, err := weaviate.NewWeaviateCloud( + ctx, + os.Getenv("WEAVIATE_URL"), + os.Getenv("WEAVIATE_API_KEY"), + ) + if err != nil { + t.Fatal(err) + } + defer client.Close() + // END APIKeyWCD + + if _, err := client.IsReady(ctx); err != nil { + t.Fatal(err) + } +} + +// TestConnectCloudThirdPartyAPIKeys connects to Weaviate Cloud and forwards a +// third-party inference key as a request header. +func TestConnectCloudThirdPartyAPIKeys(t *testing.T) { + if os.Getenv("WEAVIATE_URL") == "" || os.Getenv("WEAVIATE_API_KEY") == "" || os.Getenv("COHERE_API_KEY") == "" { + t.Skip("WEAVIATE_URL, WEAVIATE_API_KEY and COHERE_API_KEY must be set for the cloud third-party key test") + } + ctx := context.Background() + + // START ThirdPartyAPIKeys + client, err := weaviate.NewWeaviateCloud( + ctx, + os.Getenv("WEAVIATE_URL"), + os.Getenv("WEAVIATE_API_KEY"), + weaviate.WithHeader(http.Header{ + "X-Cohere-Api-Key": []string{os.Getenv("COHERE_API_KEY")}, + }), + ) + if err != nil { + t.Fatal(err) + } + defer client.Close() + // END ThirdPartyAPIKeys + + if _, err := client.IsReady(ctx); err != nil { + t.Fatal(err) + } +} + +// TestConnectOIDC connects to a self-hosted instance using an OIDC bearer token +// obtained from an identity provider. +func TestConnectOIDC(t *testing.T) { + // The default-port CI instance is anonymous with no OIDC provider, so dialing + // it with a bearer token fails at discovery with HTTP 404 "get openid + // configuration". OIDC needs an OIDC-configured instance, which is a later CI + // tier. The snippet is still compiled and rendered. + t.Skip("anon :8080 CI instance has no OIDC provider (HTTP 404 'get openid configuration'); OIDC connection needs an OIDC-configured instance โ€” a later CI tier") + if os.Getenv("WEAVIATE_OIDC_ACCESS_TOKEN") == "" { + t.Skip("WEAVIATE_OIDC_ACCESS_TOKEN must be set for the OIDC connection test") + } + ctx := context.Background() + + // START OIDCConnect + client, err := weaviate.NewClient(ctx, + weaviate.WithScheme("http"), + weaviate.WithHTTPHost("localhost"), + weaviate.WithHTTPPort(8080), + weaviate.WithGRPCHost("localhost"), + weaviate.WithGRPCPort(50051), + weaviate.WithBearerToken(oauth2.Token{ + AccessToken: os.Getenv("WEAVIATE_OIDC_ACCESS_TOKEN"), + RefreshToken: os.Getenv("WEAVIATE_OIDC_REFRESH_TOKEN"), + }), + ) + if err != nil { + t.Fatal(err) + } + defer client.Close() + // END OIDCConnect + + if _, err := client.IsReady(ctx); err != nil { + t.Fatal(err) + } +} diff --git a/_includes/code/go-v6/cross_references_test.go b/_includes/code/go-v6/cross_references_test.go new file mode 100644 index 00000000..09241d01 --- /dev/null +++ b/_includes/code/go-v6/cross_references_test.go @@ -0,0 +1,104 @@ +package main + +import ( + "context" + "testing" + + "github.com/google/uuid" + "github.com/weaviate/weaviate-go-client/v6/collections" + "github.com/weaviate/weaviate-go-client/v6/data" +) + +func TestAddOneWayCrossReference(t *testing.T) { + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + _ = client.Collections.Delete(ctx, "JeopardyQuestion") + _ = client.Collections.Delete(ctx, "JeopardyCategory") + defer client.Collections.Delete(ctx, "JeopardyQuestion") + defer client.Collections.Delete(ctx, "JeopardyCategory") + + // The reference target must exist before the collection that points to it. + if _, err := client.Collections.Create(ctx, collections.Collection{ + Name: "JeopardyCategory", + Properties: []collections.Property{ + {Name: "title", DataType: collections.DataTypeText}, + }, + }); err != nil { + t.Fatal(err) + } + if _, err := client.Collections.Create(ctx, collections.Collection{ + Name: "JeopardyQuestion", + Properties: []collections.Property{ + {Name: "question", DataType: collections.DataTypeText}, + }, + References: []collections.Reference{ + {Name: "hasCategory", Collections: []string{"JeopardyCategory"}}, + }, + }); err != nil { + t.Fatal(err) + } + + categories := client.Collections.Use("JeopardyCategory") + questions := client.Collections.Use("JeopardyQuestion") + + categoryID := uuid.MustParse("c2d3e4f5-a6b7-4c8d-8e9f-8a9b0c1d2e3f") + if _, err := categories.Data.Insert(ctx, &data.Object{ + UUID: &categoryID, + Properties: map[string]any{"title": "SCIENCE"}, + }); err != nil { + t.Fatal(err) + } + questionID := uuid.MustParse("d2e3f4a5-b6c7-4d8e-8f9a-9b0c1d2e3f4a") + if _, err := questions.Data.Insert(ctx, &data.Object{ + UUID: &questionID, + Properties: map[string]any{"question": "This vector database is written in Go"}, + }); err != nil { + t.Fatal(err) + } + + // START OneWay + // Add a reference from the source object (a JeopardyQuestion) to the target + // object (a JeopardyCategory) through the "hasCategory" reference property. + res, err := questions.Data.AddReferences(ctx, data.Reference{ + Origin: data.ObjectPath{ + Collection: "JeopardyQuestion", + Property: "hasCategory", + UUID: questionID, + }, + UUID: categoryID, + }) + if err != nil { + t.Fatal(err) + } + // END OneWay + + for ref, msg := range res.Errors { + if msg != "" { + t.Fatalf("add reference %v: %s", ref, msg) + } + } +} + +// TestDeleteCrossReference is a placeholder: the v6 Go client can add +// references but cannot yet delete them. +func TestDeleteCrossReference(t *testing.T) { + t.Skip("deleting a cross-reference is not yet available in the v6 Go client") + + // TODO[g-despot]: cross-reference delete snippet pending v6 client support + // Delete Go + // Coming soon + // END Delete Go +} + +// TestUpdateCrossReference is a placeholder: the v6 Go client can add +// references but cannot yet update (replace) them. +func TestUpdateCrossReference(t *testing.T) { + t.Skip("updating a cross-reference is not yet available in the v6 Go client") + + // TODO[g-despot]: cross-reference update snippet pending v6 client support + // Update Go + // Coming soon + // END Update Go +} diff --git a/_includes/code/go-v6/filters_test.go b/_includes/code/go-v6/filters_test.go new file mode 100644 index 00000000..27e3bf6a --- /dev/null +++ b/_includes/code/go-v6/filters_test.go @@ -0,0 +1,429 @@ +package main + +import ( + "context" + "testing" + "time" + + "github.com/weaviate/weaviate-go-client/v6/query" + "github.com/weaviate/weaviate-go-client/v6/query/filter" +) + +// The filter snippets read the JeopardyQuestion demo collection on a local +// instance. Each shows how to express one kind of filter with the query/filter +// package; the search itself is a plain fetch (Query.OverAll) so the filter is +// the only variable. + +func TestSingleFilter(t *testing.T) { + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + setupJeopardyDemo(t, client) + defer cleanupJeopardyDemo(ctx, client) + + // START SingleFilter + jeopardy := client.Collections.Use("JeopardyQuestion") + response, err := jeopardy.Query.OverAll(ctx, query.OverAll{ + Limit: 3, + Filter: &filter.Cond{ + Target: "round", + Operator: filter.Equal, + Value: "Double Jeopardy!", + }, + }) + if err != nil { + t.Fatal(err) + } + for _, obj := range response.Objects { + t.Logf("%v", obj.Properties) + } + // END SingleFilter +} + +func TestMultipleFiltersAnd(t *testing.T) { + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + setupJeopardyDemo(t, client) + defer cleanupJeopardyDemo(ctx, client) + + // START MultipleFiltersAnd + jeopardy := client.Collections.Use("JeopardyQuestion") + response, err := jeopardy.Query.OverAll(ctx, query.OverAll{ + Limit: 3, + Filter: filter.And{ + &filter.Cond{ + Target: "round", + Operator: filter.Equal, + Value: "Double Jeopardy!", + }, + &filter.Cond{ + Target: "points", + Operator: filter.LessThan, + Value: int64(600), + }, + }, + }) + if err != nil { + t.Fatal(err) + } + for _, obj := range response.Objects { + t.Logf("%v", obj.Properties) + } + // END MultipleFiltersAnd +} + +func TestMultipleFiltersNested(t *testing.T) { + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + setupJeopardyDemo(t, client) + defer cleanupJeopardyDemo(ctx, client) + + // START MultipleFiltersNested + jeopardy := client.Collections.Use("JeopardyQuestion") + response, err := jeopardy.Query.OverAll(ctx, query.OverAll{ + Limit: 3, + Filter: filter.And{ + &filter.Cond{ + Target: "answer", + Operator: filter.Like, + Value: "*nest*", + }, + filter.Or{ + &filter.Cond{ + Target: "points", + Operator: filter.GreaterThan, + Value: int64(700), + }, + &filter.Cond{ + Target: "points", + Operator: filter.LessThan, + Value: int64(300), + }, + }, + }, + }) + if err != nil { + t.Fatal(err) + } + for _, obj := range response.Objects { + t.Logf("%v", obj.Properties) + } + // END MultipleFiltersNested +} + +func TestContainsAnyFilter(t *testing.T) { + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + setupJeopardyDemo(t, client) + defer cleanupJeopardyDemo(ctx, client) + + // START ContainsAnyFilter + // The tokens to match against the tokenized "question" property. + tokens := []string{"animal", "elephant"} + + jeopardy := client.Collections.Use("JeopardyQuestion") + response, err := jeopardy.Query.OverAll(ctx, query.OverAll{ + Limit: 3, + Filter: &filter.Cond{ + Target: "question", + Operator: filter.ContainsAny, + Value: tokens, + }, + }) + if err != nil { + t.Fatal(err) + } + for _, obj := range response.Objects { + t.Logf("%v", obj.Properties) + } + // END ContainsAnyFilter +} + +func TestContainsAllFilter(t *testing.T) { + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + setupJeopardyDemo(t, client) + defer cleanupJeopardyDemo(ctx, client) + + // START ContainsAllFilter + tokens := []string{"blood", "glucose"} + + jeopardy := client.Collections.Use("JeopardyQuestion") + response, err := jeopardy.Query.OverAll(ctx, query.OverAll{ + Limit: 3, + Filter: &filter.Cond{ + Target: "question", + Operator: filter.ContainsAll, + Value: tokens, + }, + }) + if err != nil { + t.Fatal(err) + } + for _, obj := range response.Objects { + t.Logf("%v", obj.Properties) + } + // END ContainsAllFilter +} + +func TestContainsNoneFilter(t *testing.T) { + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + setupJeopardyDemo(t, client) + defer cleanupJeopardyDemo(ctx, client) + + // START ContainsNoneFilter + tokens := []string{"animal", "elephant"} + + jeopardy := client.Collections.Use("JeopardyQuestion") + response, err := jeopardy.Query.OverAll(ctx, query.OverAll{ + Limit: 3, + Filter: &filter.Cond{ + Target: "question", + Operator: filter.ContainsNone, + Value: tokens, + }, + }) + if err != nil { + t.Fatal(err) + } + for _, obj := range response.Objects { + t.Logf("%v", obj.Properties) + } + // END ContainsNoneFilter +} + +func TestLikeFilter(t *testing.T) { + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + setupJeopardyDemo(t, client) + defer cleanupJeopardyDemo(ctx, client) + + // START LikeFilter + jeopardy := client.Collections.Use("JeopardyQuestion") + response, err := jeopardy.Query.OverAll(ctx, query.OverAll{ + Limit: 3, + Filter: &filter.Cond{ + Target: "answer", + Operator: filter.Like, + // "*" matches zero or more characters, "?" matches exactly one. + Value: "*giraffe*", + }, + }) + if err != nil { + t.Fatal(err) + } + for _, obj := range response.Objects { + t.Logf("%v", obj.Properties) + } + // END LikeFilter +} + +func TestCrossReferenceFilter(t *testing.T) { + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + setupJeopardyDemo(t, client) + defer cleanupJeopardyDemo(ctx, client) + + // START CrossReferenceFilter + jeopardy := client.Collections.Use("JeopardyQuestion") + response, err := jeopardy.Query.OverAll(ctx, query.OverAll{ + Limit: 3, + Filter: &filter.Cond{ + // Build a path into the referenced collection with Reference(). + Target: filter.Reference("hasCategory").Property("title"), + Operator: filter.Like, + Value: "*Sport*", + }, + }) + if err != nil { + t.Fatal(err) + } + for _, obj := range response.Objects { + t.Logf("%v", obj.Properties) + } + // END CrossReferenceFilter +} + +func TestFilterByDate(t *testing.T) { + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + setupJeopardyDemo(t, client) + defer cleanupJeopardyDemo(ctx, client) + + // START FilterByDate + jeopardy := client.Collections.Use("JeopardyQuestion") + response, err := jeopardy.Query.OverAll(ctx, query.OverAll{ + Limit: 3, + Filter: &filter.Cond{ + Target: "dateRecorded", + Operator: filter.GreaterThan, + // Compare date properties against a time.Time value. + Value: time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC), + }, + }) + if err != nil { + t.Fatal(err) + } + for _, obj := range response.Objects { + t.Logf("%v", obj.Properties) + } + // END FilterByDate +} + +func TestFilterById(t *testing.T) { + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + setupJeopardyDemo(t, client) + defer cleanupJeopardyDemo(ctx, client) + + // START FilterById + jeopardy := client.Collections.Use("JeopardyQuestion") + response, err := jeopardy.Query.OverAll(ctx, query.OverAll{ + Filter: &filter.Cond{ + // filter.UUID targets the object's own id. + Target: filter.UUID, + Operator: filter.Equal, + Value: "a1b2c3d4-e5f6-4a5b-8c9d-1a2b3c4d5e6f", + }, + }) + if err != nil { + t.Fatal(err) + } + for _, obj := range response.Objects { + t.Logf("%v", obj.Properties) + } + // END FilterById +} + +func TestFilterByTimestamp(t *testing.T) { + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + setupJeopardyDemo(t, client) + defer cleanupJeopardyDemo(ctx, client) + + // START FilterByTimestamp + jeopardy := client.Collections.Use("JeopardyQuestion") + response, err := jeopardy.Query.OverAll(ctx, query.OverAll{ + Limit: 3, + Filter: &filter.Cond{ + // filter.CreatedAt and filter.LastUpdatedAt target internal timestamps. + Target: filter.CreatedAt, + Operator: filter.GreaterThanEqual, + Value: time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC), + }, + }) + if err != nil { + t.Fatal(err) + } + for _, obj := range response.Objects { + t.Logf("%v", obj.Properties) + } + // END FilterByTimestamp +} + +func TestFilterByPropertyLength(t *testing.T) { + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + setupJeopardyDemo(t, client) + defer cleanupJeopardyDemo(ctx, client) + + // START FilterByPropertyLength + jeopardy := client.Collections.Use("JeopardyQuestion") + response, err := jeopardy.Query.OverAll(ctx, query.OverAll{ + Limit: 3, + Filter: &filter.Cond{ + // filter.Len wraps a property in a len(property) target. + Target: filter.Len("answer"), + Operator: filter.GreaterThan, + Value: int64(20), + }, + }) + if err != nil { + t.Fatal(err) + } + for _, obj := range response.Objects { + t.Logf("%v", obj.Properties) + } + // END FilterByPropertyLength +} + +func TestFilterByPropertyNullState(t *testing.T) { + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + setupJeopardyDemo(t, client) + defer cleanupJeopardyDemo(ctx, client) + + // START FilterByPropertyNullState + jeopardy := client.Collections.Use("JeopardyQuestion") + response, err := jeopardy.Query.OverAll(ctx, query.OverAll{ + Limit: 3, + Filter: &filter.Cond{ + Target: "answer", + Operator: filter.IsNull, + Value: true, + }, + }) + if err != nil { + t.Fatal(err) + } + for _, obj := range response.Objects { + t.Logf("%v", obj.Properties) + } + // END FilterByPropertyNullState +} + +// TestNearTextWithFilter combines a semantic search with a filter. It queries a +// collection whose vectorizer turns the query text into a vector server-side. +func TestNearTextWithFilter(t *testing.T) { + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + setupJeopardyVectorized(t, client) + defer client.Collections.Delete(ctx, "JeopardyQuestion") + + // START NearTextWithFilter + jeopardy := client.Collections.Use("JeopardyQuestion") + response, err := jeopardy.Query.NearText(ctx, query.NearText{ + Concepts: []string{"large animals"}, + Limit: 2, + Filter: &filter.Cond{ + Target: "round", + Operator: filter.Equal, + Value: "Double Jeopardy!", + }, + }) + if err != nil { + t.Fatal(err) + } + for _, obj := range response.Objects { + t.Logf("%v", obj.Properties) + } + // END NearTextWithFilter +} diff --git a/_includes/code/go-v6/go.mod b/_includes/code/go-v6/go.mod new file mode 100644 index 00000000..2f3b9789 --- /dev/null +++ b/_includes/code/go-v6/go.mod @@ -0,0 +1,49 @@ +module weaviate.io/docs-go-v6 + +go 1.25.8 + +// The v6 Go client is not yet published to the Go module proxy, so this module +// intentionally declares NO `replace` directive. The docs CI `test-go` job injects +// `go mod edit -replace github.com/weaviate/weaviate-go-client/v6=/tmp/go-client` +// from a fresh clone of the client's `v6` branch before building (mirrors the Java +// lane's unpublished-client approach). Do not add a replace directive here. +require github.com/weaviate/weaviate-go-client/v6 v6.0.0 + +require ( + github.com/google/uuid v1.6.0 + golang.org/x/oauth2 v0.36.0 +) + +require ( + cloud.google.com/go/compute/metadata v0.9.0 // indirect + github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 // indirect + github.com/getkin/kin-openapi v0.139.0 // indirect + github.com/go-openapi/jsonpointer v0.23.1 // indirect + github.com/go-openapi/swag/jsonname v0.26.0 // indirect + github.com/go-viper/mapstructure/v2 v2.5.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/mailru/easyjson v0.9.2 // indirect + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect + github.com/oapi-codegen/oapi-codegen/v2 v2.7.0 // indirect + github.com/oapi-codegen/runtime v1.4.1 // indirect + github.com/oasdiff/yaml v0.1.0 // indirect + github.com/oasdiff/yaml3 v0.0.13 // indirect + github.com/perimeterx/marshmallow v1.1.5 // indirect + github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect + github.com/speakeasy-api/jsonpath v0.6.3 // indirect + github.com/speakeasy-api/openapi v1.23.0 // indirect + github.com/vmware-labs/yaml-jsonpath v0.3.2 // indirect + github.com/woodsbury/decimal128 v1.4.0 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/mod v0.36.0 // indirect + golang.org/x/net v0.55.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.45.0 // indirect + golang.org/x/text v0.37.0 // indirect + golang.org/x/tools v0.45.0 // indirect + google.golang.org/api v0.282.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect + google.golang.org/grpc v1.81.1 // indirect + google.golang.org/protobuf v1.36.11 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/_includes/code/go-v6/go.sum b/_includes/code/go-v6/go.sum new file mode 100644 index 00000000..330d5d60 --- /dev/null +++ b/_includes/code/go-v6/go.sum @@ -0,0 +1,225 @@ +cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= +cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= +github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dprotaso/go-yit v0.0.0-20191028211022-135eb7262960/go.mod h1:9HQzr9D/0PGwMEbC3d5AB7oi67+h4TsQqItC1GVYG58= +github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 h1:PRxIJD8XjimM5aTknUK9w6DHLDox2r2M3DI4i2pnd3w= +github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936/go.mod h1:ttYvX5qlB+mlV1okblJqcSMtR4c52UKxDiX9GRBS8+Q= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/getkin/kin-openapi v0.139.0 h1:pBFXcZJFwz9J1X64jzxlOoNgFm+TF7kNrs9+HJVN6Ic= +github.com/getkin/kin-openapi v0.139.0/go.mod h1:NGxPfE4PwS/TRXEbyx2RrxDFPZvxcWw31Tw8XXjPZLs= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-openapi/jsonpointer v0.23.1 h1:1HBACs7XIwR2RcmItfdSFlALhGbe6S92p0ry4d1GWg4= +github.com/go-openapi/jsonpointer v0.23.1/go.mod h1:iWRmZTrGn7XwYhtPt/fvdSFj1OfNBngqRT2UG3BxSqY= +github.com/go-openapi/swag/jsonname v0.26.0 h1:gV1NFX9M8avo0YSpmWogqfQISigCmpaiNci8cGECU5w= +github.com/go-openapi/swag/jsonname v0.26.0/go.mod h1:urBBR8bZNoDYGr653ynhIx+gTeIz0ARZxHkAPktJK2M= +github.com/go-openapi/testify/v2 v2.4.2 h1:tiByHpvE9uHrrKjOszax7ZvKB7QOgizBWGBLuq0ePx4= +github.com/go-openapi/testify/v2 v2.4.2/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= +github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= +github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= +github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mailru/easyjson v0.9.2 h1:dX8U45hQsZpxd80nLvDGihsQ/OxlvTkVUXH2r/8cb2M= +github.com/mailru/easyjson v0.9.2/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/oapi-codegen/oapi-codegen/v2 v2.7.0 h1:/8daqIYZfwnsHEAZdHUu9m0D5LA+5DoJCP7zLlT5Cs0= +github.com/oapi-codegen/oapi-codegen/v2 v2.7.0/go.mod h1:qzFy6iuobJw/hD1aRILee4G87/ShmhR0xYCwcUtZMCw= +github.com/oapi-codegen/runtime v1.4.1 h1:9nwLoI+KrWxzbBcp0jO/R8uXqbik/HUyCvPeU68Y/qo= +github.com/oapi-codegen/runtime v1.4.1/go.mod h1:GwV7hC2hviaMzj+ITfHVRESK5J2W/GefVwIND/bMGvU= +github.com/oasdiff/yaml v0.1.0 h1:0bqZjfKc/8S9urj4JuwepX41WX9EoA6ifhU3SV06cXg= +github.com/oasdiff/yaml v0.1.0/go.mod h1:kOlRmMdL2X3vucLCEQO5u61SU22RysnfXvcttrZA1O0= +github.com/oasdiff/yaml3 v0.0.13 h1:06svmvOHOVBqF81+sY2EUScvUI/iS/vl2VIeUUxZQwg= +github.com/oasdiff/yaml3 v0.0.13/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.2/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= +github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= +github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw= +github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= +github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= +github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= +github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= +github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/speakeasy-api/jsonpath v0.6.3 h1:c+QPwzAOdrWvzycuc9HFsIZcxKIaWcNpC+xhOW9rJxU= +github.com/speakeasy-api/jsonpath v0.6.3/go.mod h1:2cXloNuQ+RSXi5HTRaeBh7JEmjRXTiaKpFTdZiL7URI= +github.com/speakeasy-api/openapi v1.23.0 h1:BlnHqUR/8uIs4tp3d2B4QDN03gw1/QY2R2MHg6fLhRU= +github.com/speakeasy-api/openapi v1.23.0/go.mod h1:Ih+ZzaTCCPyB2ykiDXaxhqk6jsY84ke3n5p6fobMDXk= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/vmware-labs/yaml-jsonpath v0.3.2 h1:/5QKeCBGdsInyDCyVNLbXyilb61MXGi9NP674f9Hobk= +github.com/vmware-labs/yaml-jsonpath v0.3.2/go.mod h1:U6whw1z03QyqgWdgXxvVnQ90zN1BWz5V+51Ewf8k+rQ= +github.com/woodsbury/decimal128 v1.4.0 h1:xJATj7lLu4f2oObouMt2tgGiElE5gO6mSWUjQsBgUlc= +github.com/woodsbury/decimal128 v1.4.0/go.mod h1:BP46FUrVjVhdTbKT+XuQh2xfQaGki9LMIRJSFuh6THU= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= +go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= +go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= +go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= +golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= +golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= +golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= +golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/api v0.282.0 h1:WmJiSVqUnKqJCpJOx7YADbXaC+9DDsnGSfllFSj7R2I= +google.golang.org/api v0.282.0/go.mod h1:6Wssta4c5n9qHq5CBhmlai5h/PUa1djdDAIhYEHyvcM= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ= +google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20191026110619-0b21df46bc1d/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/_includes/code/go-v6/hybrid_test.go b/_includes/code/go-v6/hybrid_test.go new file mode 100644 index 00000000..4002bb49 --- /dev/null +++ b/_includes/code/go-v6/hybrid_test.go @@ -0,0 +1,271 @@ +package main + +import ( + "context" + "testing" + + "github.com/weaviate/weaviate-go-client/v6/query" + "github.com/weaviate/weaviate-go-client/v6/query/filter" + "github.com/weaviate/weaviate-go-client/v6/types" +) + +// The hybrid snippets query a collection whose vectorizer turns the query +// string into a vector server-side, mirroring the demo datasets used across the +// docs. They connect to a local instance and read the JeopardyQuestion demo +// collection. + +func TestHybridBasic(t *testing.T) { + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + setupJeopardyVectorized(t, client) + defer client.Collections.Delete(ctx, "JeopardyQuestion") + + // START HybridBasic + jeopardy := client.Collections.Use("JeopardyQuestion") + response, err := jeopardy.Query.Hybrid(ctx, query.Hybrid{ + Query: "food", + Limit: 3, + }) + if err != nil { + t.Fatal(err) + } + for _, obj := range response.Objects { + t.Logf("%v", obj.Properties) + } + // END HybridBasic +} + +func TestHybridWithScore(t *testing.T) { + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + setupJeopardyVectorized(t, client) + defer client.Collections.Delete(ctx, "JeopardyQuestion") + + // START HybridWithScore + jeopardy := client.Collections.Use("JeopardyQuestion") + response, err := jeopardy.Query.Hybrid(ctx, query.Hybrid{ + Query: "food", + Limit: 3, + ReturnMetadata: query.ReturnMetadata{ + Score: true, + ExplainScore: true, + }, + }) + if err != nil { + t.Fatal(err) + } + for _, obj := range response.Objects { + if obj.Metadata.Score != nil { + t.Logf("score: %v", *obj.Metadata.Score) + } + if obj.Metadata.ExplainScore != nil { + t.Logf("explain: %v", *obj.Metadata.ExplainScore) + } + } + // END HybridWithScore +} + +func TestHybridWithAlpha(t *testing.T) { + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + setupJeopardyVectorized(t, client) + defer client.Collections.Delete(ctx, "JeopardyQuestion") + + // START HybridWithAlpha + jeopardy := client.Collections.Use("JeopardyQuestion") + response, err := jeopardy.Query.Hybrid(ctx, query.Hybrid{ + Query: "food", + // Alpha of 0 is pure keyword search, 1 is pure vector search. + Alpha: query.Alpha(0.25), + Limit: 3, + }) + if err != nil { + t.Fatal(err) + } + for _, obj := range response.Objects { + t.Logf("%v", obj.Properties) + } + // END HybridWithAlpha +} + +func TestHybridWithFusionType(t *testing.T) { + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + setupJeopardyVectorized(t, client) + defer client.Collections.Delete(ctx, "JeopardyQuestion") + + // START HybridWithFusionType + jeopardy := client.Collections.Use("JeopardyQuestion") + response, err := jeopardy.Query.Hybrid(ctx, query.Hybrid{ + Query: "food", + Fusion: query.HybridFusionRelativeScore, + Limit: 3, + }) + if err != nil { + t.Fatal(err) + } + for _, obj := range response.Objects { + t.Logf("%v", obj.Properties) + } + // END HybridWithFusionType +} + +func TestHybridWithProperties(t *testing.T) { + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + setupJeopardyVectorized(t, client) + defer client.Collections.Delete(ctx, "JeopardyQuestion") + + // START HybridWithProperties + jeopardy := client.Collections.Use("JeopardyQuestion") + response, err := jeopardy.Query.Hybrid(ctx, query.Hybrid{ + Query: "food", + // Restrict the keyword search to these properties. + QueryProperties: []string{"question", "answer"}, + Limit: 3, + }) + if err != nil { + t.Fatal(err) + } + for _, obj := range response.Objects { + t.Logf("%v", obj.Properties) + } + // END HybridWithProperties +} + +func TestHybridWithPropertyWeighting(t *testing.T) { + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + setupJeopardyVectorized(t, client) + defer client.Collections.Delete(ctx, "JeopardyQuestion") + + // START HybridWithPropertyWeighting + jeopardy := client.Collections.Use("JeopardyQuestion") + response, err := jeopardy.Query.Hybrid(ctx, query.Hybrid{ + Query: "food", + // Boost the "question" property with the ^ operator. + QueryProperties: []string{"question^2", "answer"}, + Limit: 3, + }) + if err != nil { + t.Fatal(err) + } + for _, obj := range response.Objects { + t.Logf("%v", obj.Properties) + } + // END HybridWithPropertyWeighting +} + +func TestHybridWithVector(t *testing.T) { + t.Skip("hybrid search with a provided NearVector panics the server (Weaviate 1.38.0): rpc code=Unknown \"panic occurred: nil pointer dereference\". Reproduced live against a MATCHING-dimension bring-your-own-vector collection, so it is not a dimension mismatch; plain NearVector and plain Hybrid over the same collection both succeed, so the defect is specific to Hybrid.NearVector. The v6 client marshals proto.Hybrid.NearVector via marshalNearVector (internal/api/search.go:675) into VectorForTargets/Targets, a shape the 1.38 hybrid path mishandles. Snippet is idiomatic; deferred pending a client/server fix") + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + // START HybridWithVector + // A query vector, for example an embedding produced by your model. + vector := []float32{0.12, 0.20, 0.33} + + jeopardy := client.Collections.Use("JeopardyQuestion") + response, err := jeopardy.Query.Hybrid(ctx, query.Hybrid{ + Query: "food", + // Supply the vector for the vector-search half of the query. + NearVector: &query.NearVector{ + Target: &types.Vector{Name: "default", Single: vector}, + }, + Limit: 3, + }) + if err != nil { + t.Fatal(err) + } + for _, obj := range response.Objects { + t.Logf("%v", obj.Properties) + } + // END HybridWithVector +} + +func TestHybridLimit(t *testing.T) { + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + setupJeopardyVectorized(t, client) + defer client.Collections.Delete(ctx, "JeopardyQuestion") + + // START HybridLimit + jeopardy := client.Collections.Use("JeopardyQuestion") + response, err := jeopardy.Query.Hybrid(ctx, query.Hybrid{ + Query: "food", + Limit: 3, + }) + if err != nil { + t.Fatal(err) + } + for _, obj := range response.Objects { + t.Logf("%v", obj.Properties) + } + // END HybridLimit +} + +func TestHybridAutocut(t *testing.T) { + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + setupJeopardyVectorized(t, client) + defer client.Collections.Delete(ctx, "JeopardyQuestion") + + // START HybridAutocut + jeopardy := client.Collections.Use("JeopardyQuestion") + response, err := jeopardy.Query.Hybrid(ctx, query.Hybrid{ + Query: "food", + AutoLimit: 1, + }) + if err != nil { + t.Fatal(err) + } + for _, obj := range response.Objects { + t.Logf("%v", obj.Properties) + } + // END HybridAutocut +} + +func TestHybridWithFilter(t *testing.T) { + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + setupJeopardyVectorized(t, client) + defer client.Collections.Delete(ctx, "JeopardyQuestion") + + // START HybridWithFilter + jeopardy := client.Collections.Use("JeopardyQuestion") + response, err := jeopardy.Query.Hybrid(ctx, query.Hybrid{ + Query: "food", + Limit: 3, + Filter: &filter.Cond{ + Target: "round", + Operator: filter.Equal, + Value: "Double Jeopardy!", + }, + }) + if err != nil { + t.Fatal(err) + } + for _, obj := range response.Objects { + t.Logf("%v", obj.Properties) + } + // END HybridWithFilter +} diff --git a/_includes/code/go-v6/main.go b/_includes/code/go-v6/main.go new file mode 100644 index 00000000..68565d5f --- /dev/null +++ b/_includes/code/go-v6/main.go @@ -0,0 +1,9 @@ +// Package main holds tested code snippets for the Weaviate Go client (v6). +// +// The snippets live in the *_test.go files and are embedded into the docs via +// // START / // END comment pairs, mirroring the other +// _includes/code language modules. This file only exists so the package builds +// with `go build ./...`; the real work happens under `go test`. +package main + +func main() {} diff --git a/_includes/code/go-v6/main_test.go b/_includes/code/go-v6/main_test.go new file mode 100644 index 00000000..24ea8664 --- /dev/null +++ b/_includes/code/go-v6/main_test.go @@ -0,0 +1,565 @@ +package main + +import ( + "context" + "net/http" + "os" + "strconv" + "testing" + "time" + + "github.com/google/uuid" + weaviate "github.com/weaviate/weaviate-go-client/v6" + "github.com/weaviate/weaviate-go-client/v6/aggregate" + "github.com/weaviate/weaviate-go-client/v6/collections" + "github.com/weaviate/weaviate-go-client/v6/data" + "github.com/weaviate/weaviate-go-client/v6/tenant" +) + +// connectLocal returns a client connected to a local Weaviate instance +// (localhost:8080 REST, localhost:50051 gRPC). This is the connection the +// docs test stack exposes; the insert and query smoke tests dial through it. +func connectLocal(t *testing.T) *weaviate.Client { + t.Helper() + ctx := context.Background() + client, err := weaviate.NewLocal(ctx) + if err != nil { + t.Fatalf("connect to local Weaviate: %v", err) + } + return client +} + +// getenvOr returns the value of environment variable key, or fallback when the +// variable is unset or empty. +func getenvOr(key, fallback string) string { + if v := os.Getenv(key); v != "" { + return v + } + return fallback +} + +// getenvPortOr returns the integer value of environment variable key, or fallback +// when the variable is unset, empty, or not a valid integer. +func getenvPortOr(key string, fallback int) int { + if v := os.Getenv(key); v != "" { + if p, err := strconv.Atoi(v); err == nil { + return p + } + } + return fallback +} + +// connectRBACAdmin returns a client connected as the RBAC root user to the +// RBAC-enabled Weaviate instance the docs CI stack runs (service weaviate_rbac in +// tests/docker-compose-rbac.yml: HTTP :8580 / gRPC :50551, root user "root-user" +// with API key "root-user-key", RBAC + OIDC + DB-users enabled). Host, ports and +// key are overridable by environment variable so the same helper works on a +// laptop and in CI. The RBAC snippets dial through this instead of connectLocal +// (which reaches the anonymous default-port instance that does not enforce RBAC), +// so the role, database-user and OIDC snippets run against an instance that +// actually authorizes them. This helper sits outside every snippet marker, so it +// never appears in a rendered snippet. +// +// The root key is sent as a Bearer Authorization header rather than via +// weaviate.WithAPIKey(...). WithAPIKey attaches the key as a gRPC per-RPC +// credential (oauth.TokenSource) whose RequireTransportSecurity() reports true, so +// against this plaintext-http instance grpc.NewClient() refuses to build the +// channel ("credentials require transport level security") and NewClient fails to +// connect at all โ€” a v6-client bug (internal/transports/grpc.go attaches the +// per-RPC credential even when it selected insecure transport creds; still present +// on origin/v6 @8a70091). Every RBAC endpoint is REST, so an Authorization header +// authenticates all of them as root-user while sidestepping the broken gRPC path. +// The rendered AdminClient snippet still shows weaviate.WithAPIKey, which is the +// correct pattern for a TLS deployment where the bug does not bite. See the tier-4 +// escalation notes. +func connectRBACAdmin(t *testing.T) *weaviate.Client { + t.Helper() + ctx := context.Background() + apiKey := getenvOr("WEAVIATE_RBAC_API_KEY", "root-user-key") + client, err := weaviate.NewClient(ctx, + weaviate.WithScheme("http"), + weaviate.WithHTTPHost(getenvOr("WEAVIATE_RBAC_HTTP_HOST", "localhost")), + weaviate.WithHTTPPort(getenvPortOr("WEAVIATE_RBAC_HTTP_PORT", 8580)), + weaviate.WithGRPCHost(getenvOr("WEAVIATE_RBAC_GRPC_HOST", "localhost")), + weaviate.WithGRPCPort(getenvPortOr("WEAVIATE_RBAC_GRPC_PORT", 50551)), + weaviate.WithHeader(http.Header{"Authorization": []string{"Bearer " + apiKey}}), + ) + if err != nil { + t.Fatalf("connect to RBAC Weaviate: %v", err) + } + return client +} + +// setupArticle (re)creates a minimal Article collection used by the object and +// query smoke tests. It has no vectorizer, so objects are inserted with +// explicit properties only and read back with a plain fetch. +func setupArticle(t *testing.T, client *weaviate.Client) { + t.Helper() + ctx := context.Background() + // Start from a clean slate; ignore the error when the collection is absent. + _ = client.Collections.Delete(ctx, "Article") + if _, err := client.Collections.Create(ctx, collections.Collection{ + Name: "Article", + Properties: []collections.Property{ + {Name: "title", DataType: collections.DataTypeText}, + {Name: "body", DataType: collections.DataTypeText}, + }, + }); err != nil { + t.Fatalf("create Article collection: %v", err) + } +} + +// waitForCount polls a collection handle until it reports at least n objects, +// then returns. The docs test instance runs with ASYNC_INDEXING enabled, so +// objects are not guaranteed to be queryable the instant Insert returns; polling +// the aggregate object count settles that race and keeps the seeded tests +// deterministic. For a multi-tenant collection, pass a tenant-bound handle so +// the aggregate is scoped to that tenant. +func waitForCount(t *testing.T, handle *collections.Handle, n int) { + t.Helper() + ctx := context.Background() + deadline := time.Now().Add(30 * time.Second) + var last int64 + for time.Now().Before(deadline) { + res, err := handle.Aggregate.OverAll(ctx, aggregate.OverAll{TotalCount: true}) + if err == nil && res.TotalCount != nil { + last = *res.TotalCount + if last >= int64(n) { + return + } + } + time.Sleep(200 * time.Millisecond) + } + t.Fatalf("waitForCount: %q reached %d/%d objects before timeout", handle.CollectionName(), last, n) +} + +// filterByIdSeedUUID is the id filtered on by TestFilterById; setupJeopardyDemo +// seeds one question with this id so that snippet returns a deterministic match. +// +// The first byte must be non-zero. Weaviate's gRPC search reply carries the +// object id in the `id_as_bytes` field, which drops leading zero bytes: an id +// beginning 0x00 comes back as 15 bytes and the client's uuid.FromBytes rejects +// the whole reply with "invalid UUID (got 15 bytes)", failing every query over +// the collection. The previous value ("00037775-โ€ฆ") began 0x00 and did exactly +// that. Keep every seeded id's first byte non-zero. +const filterByIdSeedUUID = "a1b2c3d4-e5f6-4a5b-8c9d-1a2b3c4d5e6f" + +// Fixed ids for the demo JeopardyCategory rows so questions can be linked to +// them with AddReferences during seeding. +var ( + demoCatAnimals = uuid.MustParse("11111111-1111-4111-8111-111111111111") + demoCatSports = uuid.MustParse("22222222-2222-4222-8222-222222222222") + demoCatScience = uuid.MustParse("33333333-3333-4333-8333-333333333333") +) + +// setupJeopardyDemo (re)creates the full JeopardyQuestion demo collection used by +// the filter and cross-reference snippets: a question/answer/category/round/ +// points/dateRecorded schema plus a hasCategory reference to a seeded +// JeopardyCategory. It has no vectorizer, so the filter examples run on a plain +// fetch (Query.OverAll) with the filter as the only variable. +// +// The inverted index turns on the null-state, property-length and timestamp +// indexes because the FilterByPropertyNullState, FilterByPropertyLength and +// FilterByTimestamp snippets query those indexes and error without them. +func setupJeopardyDemo(t *testing.T, client *weaviate.Client) { + t.Helper() + ctx := context.Background() + _ = client.Collections.Delete(ctx, "JeopardyQuestion") + _ = client.Collections.Delete(ctx, "JeopardyCategory") + + // The reference target must exist before the collection that points to it. + if _, err := client.Collections.Create(ctx, collections.Collection{ + Name: "JeopardyCategory", + Properties: []collections.Property{ + {Name: "title", DataType: collections.DataTypeText}, + }, + }); err != nil { + t.Fatalf("create JeopardyCategory collection: %v", err) + } + if _, err := client.Collections.Create(ctx, collections.Collection{ + Name: "JeopardyQuestion", + Properties: []collections.Property{ + {Name: "question", DataType: collections.DataTypeText}, + {Name: "answer", DataType: collections.DataTypeText}, + {Name: "category", DataType: collections.DataTypeText}, + {Name: "round", DataType: collections.DataTypeText}, + {Name: "points", DataType: collections.DataTypeInt}, + {Name: "dateRecorded", DataType: collections.DataTypeDate}, + }, + References: []collections.Reference{ + {Name: "hasCategory", Collections: []string{"JeopardyCategory"}}, + }, + InvertedIndex: &collections.InvertedIndexConfig{ + IndexNullState: true, + IndexPropertyLength: true, + IndexTimestamps: true, + }, + }); err != nil { + t.Fatalf("create JeopardyQuestion collection: %v", err) + } + + categories := client.Collections.Use("JeopardyCategory") + if _, err := categories.Data.Insert(ctx, + &data.Object{UUID: &demoCatAnimals, Properties: map[string]any{"title": "Animals"}}, + &data.Object{UUID: &demoCatSports, Properties: map[string]any{"title": "Sports"}}, + &data.Object{UUID: &demoCatScience, Properties: map[string]any{"title": "Science"}}, + ); err != nil { + t.Fatalf("seed JeopardyCategory: %v", err) + } + + // Every question gets a fixed, non-leading-zero id so the run is deterministic + // and no seeded object trips the id_as_bytes leading-zero bug (see + // filterByIdSeedUUID). Random or server-assigned ids carry a ~1/256 chance of a + // leading 0x00 byte, which would flake the whole collection's queries. + q1 := uuid.MustParse(filterByIdSeedUUID) + q2 := uuid.MustParse("b2c3d4e5-f6a7-4b5c-8d9e-2a3b4c5d6e7f") + q3 := uuid.MustParse("c3d4e5f6-a7b8-4c5d-8e9f-3a4b5c6d7e80") + q4 := uuid.MustParse("d4e5f6a7-b8c9-4d5e-8f90-4b5c6d7e8f90") + q5 := uuid.MustParse("e5f6a7b8-c9d0-4e5f-8a01-5c6d7e8f9012") + q6 := uuid.MustParse("f6a7b8c9-d0e1-4f5a-8b02-6d7e8f901234") + jeopardy := client.Collections.Use("JeopardyQuestion") + if _, err := jeopardy.Data.Insert(ctx, + &data.Object{UUID: &q1, Properties: map[string]any{ + "question": "This organ removes excess glucose from the blood & stores it as glycogen", + "answer": "Liver", "category": "SCIENCE", "round": "Jeopardy!", "points": 100, + "dateRecorded": "2021-05-05T00:00:00Z", + }}, + &data.Object{UUID: &q2, Properties: map[string]any{ + "question": "This large animal, the elephant, is the only living member of Proboscidea", + "answer": "Elephant", "category": "ANIMALS", "round": "Double Jeopardy!", "points": 200, + "dateRecorded": "2022-01-01T00:00:00Z", + }}, + &data.Object{UUID: &q3, Properties: map[string]any{ + "question": "This tall animal has a long neck and roams the African savanna", + "answer": "Giraffe", "category": "ANIMALS", "round": "Double Jeopardy!", "points": 500, + "dateRecorded": "2023-03-03T00:00:00Z", + }}, + &data.Object{UUID: &q4, Properties: map[string]any{ + "question": "Bees build these six-sided structures to store honey in a hive", + "answer": "A honeycomb nest built by bees", "category": "NATURE", "round": "Jeopardy!", "points": 200, + "dateRecorded": "2020-06-06T00:00:00Z", + }}, + &data.Object{UUID: &q5, Properties: map[string]any{ + "question": "This racket sport holds its most famous tournament at Wimbledon", + "answer": "Tennis", "category": "SPORTS", "round": "Final Jeopardy!", "points": 800, + "dateRecorded": "2021-09-09T00:00:00Z", + }}, + // A question with no answer so FilterByPropertyNullState has a match. + &data.Object{UUID: &q6, Properties: map[string]any{ + "question": "This open-source vector database is written in Go", + "category": "TECH", "round": "Double Jeopardy!", "points": 400, + "dateRecorded": "2024-04-04T00:00:00Z", + }}, + ); err != nil { + t.Fatalf("seed JeopardyQuestion: %v", err) + } + + // Link a few questions to their categories so the cross-reference filter has + // data to traverse. Best-effort: only a transport error is fatal. + if _, err := jeopardy.Data.AddReferences(ctx, + data.Reference{Origin: data.ObjectPath{Collection: "JeopardyQuestion", Property: "hasCategory", UUID: q1}, UUID: demoCatScience}, + data.Reference{Origin: data.ObjectPath{Collection: "JeopardyQuestion", Property: "hasCategory", UUID: q2}, UUID: demoCatAnimals}, + data.Reference{Origin: data.ObjectPath{Collection: "JeopardyQuestion", Property: "hasCategory", UUID: q3}, UUID: demoCatAnimals}, + data.Reference{Origin: data.ObjectPath{Collection: "JeopardyQuestion", Property: "hasCategory", UUID: q5}, UUID: demoCatSports}, + ); err != nil { + t.Fatalf("seed JeopardyQuestion references: %v", err) + } + + waitForCount(t, jeopardy, 6) +} + +// cleanupJeopardyDemo removes the collections created by setupJeopardyDemo. Call +// it with defer so the delete runs while the client is still open. +func cleanupJeopardyDemo(ctx context.Context, client *weaviate.Client) { + _ = client.Collections.Delete(ctx, "JeopardyQuestion") + _ = client.Collections.Delete(ctx, "JeopardyCategory") +} + +// Fixed ids for the multi-tenancy seed so TestMtAddCrossRef can reference a real +// source object (a MultiTenancyCollection question in tenantA) and a real target +// object (a JeopardyCategory row) instead of dangling random ids. +var ( + mtSourceID = uuid.MustParse("44444444-4444-4444-8444-444444444444") + mtSourceID2 = uuid.MustParse("66666666-6666-4666-8666-666666666666") + mtCategoryID = uuid.MustParse("55555555-5555-4555-8555-555555555555") +) + +// createMultiTenancyCollection (re)creates the MultiTenancyCollection schema used +// by the multi-tenancy snippets: a multi-tenant question collection with a +// hasCategory reference to a (non-tenant) JeopardyCategory, mirroring the docs +// demo. It creates no tenants and seeds no data, so the AddTenantsToClass snippet +// can create tenantA/tenantB itself. +func createMultiTenancyCollection(t *testing.T, client *weaviate.Client) { + t.Helper() + ctx := context.Background() + _ = client.Collections.Delete(ctx, "MultiTenancyCollection") + _ = client.Collections.Delete(ctx, "JeopardyCategory") + + if _, err := client.Collections.Create(ctx, collections.Collection{ + Name: "JeopardyCategory", + Properties: []collections.Property{ + {Name: "title", DataType: collections.DataTypeText}, + }, + }); err != nil { + t.Fatalf("create JeopardyCategory collection: %v", err) + } + if _, err := client.Collections.Create(ctx, collections.Collection{ + Name: "MultiTenancyCollection", + Properties: []collections.Property{ + {Name: "question", DataType: collections.DataTypeText}, + }, + References: []collections.Reference{ + {Name: "hasCategory", Collections: []string{"JeopardyCategory"}}, + }, + MultiTenancy: &collections.MultiTenancyConfig{Enabled: true}, + }); err != nil { + t.Fatalf("create MultiTenancyCollection: %v", err) + } +} + +// setupMultiTenancy (re)creates MultiTenancyCollection, adds tenantA and seeds it +// with a question (plus a JeopardyCategory row) so the tenant read/search and +// cross-reference snippets have data to work with. +func setupMultiTenancy(t *testing.T, client *weaviate.Client) { + t.Helper() + ctx := context.Background() + createMultiTenancyCollection(t, client) + + if _, err := client.Collections.Use("JeopardyCategory").Data.Insert(ctx, + &data.Object{UUID: &mtCategoryID, Properties: map[string]any{"title": "Software"}}, + ); err != nil { + t.Fatalf("seed JeopardyCategory: %v", err) + } + + collection := client.Collections.Use("MultiTenancyCollection") + if err := collection.Tenants.Create(ctx, tenant.Tenant{Name: "tenantA"}); err != nil { + t.Fatalf("create tenantA: %v", err) + } + + tenantA := client.Collections.Use("MultiTenancyCollection", collections.WithTenant("tenantA")) + if _, err := tenantA.Data.Insert(ctx, + &data.Object{UUID: &mtSourceID, Properties: map[string]any{ + "question": "This vector DB is OSS and supports automatic property type inference on import", + }}, + // Fixed, non-leading-zero id: a server-assigned 0x00-leading id truncates in + // the gRPC id_as_bytes reply and flakes tenant queries (see filterByIdSeedUUID). + &data.Object{UUID: &mtSourceID2, Properties: map[string]any{ + "question": "This organ removes excess glucose from the blood & stores it as glycogen", + }}, + ); err != nil { + t.Fatalf("seed tenantA: %v", err) + } + + waitForCount(t, tenantA, 2) +} + +// cleanupMultiTenancy removes the collections created for the multi-tenancy +// snippets. Call it with defer so the delete runs while the client is still open. +func cleanupMultiTenancy(ctx context.Context, client *weaviate.Client) { + _ = client.Collections.Delete(ctx, "MultiTenancyCollection") + _ = client.Collections.Delete(ctx, "JeopardyCategory") +} + +// setupMultiTenancyJeopardy (re)creates a multi-tenant JeopardyQuestion +// collection and seeds tenantA. The "read all objects" multi-tenancy search +// snippet binds tenantA on a JeopardyQuestion handle, so this seeds that exact +// collection name (distinct from the non-tenant JeopardyQuestion demo). +func setupMultiTenancyJeopardy(t *testing.T, client *weaviate.Client) { + t.Helper() + ctx := context.Background() + _ = client.Collections.Delete(ctx, "JeopardyQuestion") + if _, err := client.Collections.Create(ctx, collections.Collection{ + Name: "JeopardyQuestion", + Properties: []collections.Property{ + {Name: "question", DataType: collections.DataTypeText}, + {Name: "answer", DataType: collections.DataTypeText}, + {Name: "category", DataType: collections.DataTypeText}, + {Name: "points", DataType: collections.DataTypeInt}, + }, + MultiTenancy: &collections.MultiTenancyConfig{Enabled: true}, + }); err != nil { + t.Fatalf("create multi-tenant JeopardyQuestion: %v", err) + } + + collection := client.Collections.Use("JeopardyQuestion") + if err := collection.Tenants.Create(ctx, tenant.Tenant{Name: "tenantA"}); err != nil { + t.Fatalf("create tenantA: %v", err) + } + + tenantA := client.Collections.Use("JeopardyQuestion", collections.WithTenant("tenantA")) + // Fixed, non-leading-zero ids keep the tenant read/search snippets deterministic + // (a server-assigned 0x00-leading id flakes gRPC queries; see filterByIdSeedUUID). + mtjLiver := uuid.MustParse("77777777-7777-4777-8777-777777777777") + mtjElephant := uuid.MustParse("88888888-8888-4888-8888-888888888888") + if _, err := tenantA.Data.Insert(ctx, + &data.Object{UUID: &mtjLiver, Properties: map[string]any{"question": "This organ removes excess glucose from the blood", "answer": "Liver", "category": "SCIENCE", "points": 100}}, + &data.Object{UUID: &mtjElephant, Properties: map[string]any{"question": "The only living mammal in the order Proboscidea", "answer": "Elephant", "category": "ANIMALS", "points": 200}}, + ); err != nil { + t.Fatalf("seed tenantA: %v", err) + } + + waitForCount(t, tenantA, 2) +} + +// contextionaryVectorizer is a test-only Vectorizer that selects the +// text2vec-contextionary module, which is enabled on the docs test instance +// (deterministic, no model download, no API key). The v6 client encodes any +// modules.Module by reflection from its Name, so a struct that only reports the +// module name is enough to request server-side vectorization; the module does +// not need to be registered with the client because these seeds only create +// collections (the write path) and never decode the module config back. +// +// It carries no configuration, so it serializes to +// {"text2vec-contextionary": {}} and the module vectorizes every text property +// of the collection by default. The near-text and hybrid snippets rely on this +// to turn their query text into a vector server-side. +type contextionaryVectorizer struct{} + +func (contextionaryVectorizer) Name() string { return "text2vec-contextionary" } + +// setupJeopardyVectorized (re)creates a JeopardyQuestion collection whose +// "default" vector is produced server-side by text2vec-contextionary, then +// seeds it. Because the server vectorizes the objects on import (and the query +// text at search time), this unblocks the near-text and hybrid snippets, which +// do not supply their own vectors. Every object gets a fixed, non-leading-zero +// id so the run stays deterministic and no seeded id trips the id_as_bytes +// leading-zero bug (see filterByIdSeedUUID). +func setupJeopardyVectorized(t *testing.T, client *weaviate.Client) { + t.Helper() + ctx := context.Background() + _ = client.Collections.Delete(ctx, "JeopardyQuestion") + if _, err := client.Collections.Create(ctx, collections.Collection{ + Name: "JeopardyQuestion", + Properties: []collections.Property{ + {Name: "question", DataType: collections.DataTypeText}, + {Name: "answer", DataType: collections.DataTypeText}, + {Name: "category", DataType: collections.DataTypeText}, + {Name: "round", DataType: collections.DataTypeText}, + {Name: "points", DataType: collections.DataTypeInt}, + }, + // The "default" vector is filled in by the server via + // text2vec-contextionary, so objects are inserted without vectors and + // near-text/hybrid queries can vectorize their query text server-side. + Vectors: map[string]collections.VectorConfig{ + "default": {Vectorizer: contextionaryVectorizer{}}, + }, + }); err != nil { + t.Fatalf("create vectorized JeopardyQuestion collection: %v", err) + } + + q1 := uuid.MustParse("a1b2c3d4-e5f6-4a5b-8c9d-1a2b3c4d5e6f") + q2 := uuid.MustParse("b2c3d4e5-f6a7-4b5c-8d9e-2a3b4c5d6e7f") + q3 := uuid.MustParse("c3d4e5f6-a7b8-4c5d-8e9f-3a4b5c6d7e80") + q4 := uuid.MustParse("d4e5f6a7-b8c9-4d5e-8f90-4b5c6d7e8f90") + q5 := uuid.MustParse("e5f6a7b8-c9d0-4e5f-8a01-5c6d7e8f9012") + q6 := uuid.MustParse("f6a7b8c9-d0e1-4f5a-8b02-6d7e8f901234") + jeopardy := client.Collections.Use("JeopardyQuestion") + if _, err := jeopardy.Data.Insert(ctx, + &data.Object{UUID: &q1, Properties: map[string]any{ + "question": "This organ removes excess glucose from the blood and stores it as glycogen", + "answer": "Liver", "category": "SCIENCE", "round": "Jeopardy!", "points": 100, + }}, + &data.Object{UUID: &q2, Properties: map[string]any{ + "question": "This large animal is the only living mammal in the order Proboscidea", + "answer": "Elephant", "category": "ANIMALS", "round": "Double Jeopardy!", "points": 200, + }}, + &data.Object{UUID: &q3, Properties: map[string]any{ + "question": "This tall animal has a long neck and roams the African savanna", + "answer": "Giraffe", "category": "ANIMALS", "round": "Double Jeopardy!", "points": 500, + }}, + &data.Object{UUID: &q4, Properties: map[string]any{ + "question": "Bees make this sweet food and store it in a hive", + "answer": "Honey", "category": "NATURE", "round": "Jeopardy!", "points": 200, + }}, + &data.Object{UUID: &q5, Properties: map[string]any{ + "question": "This racket sport holds its most famous tournament at Wimbledon", + "answer": "Tennis", "category": "SPORTS", "round": "Final Jeopardy!", "points": 800, + }}, + &data.Object{UUID: &q6, Properties: map[string]any{ + "question": "This yellow fruit is a favorite food of monkeys", + "answer": "Banana", "category": "FOOD", "round": "Double Jeopardy!", "points": 300, + }}, + ); err != nil { + t.Fatalf("seed vectorized JeopardyQuestion: %v", err) + } + + waitForCount(t, jeopardy, 6) +} + +// setupWineReviewNV (re)creates a WineReviewNV collection with a single named +// vector, "title_country", produced server-side by text2vec-contextionary from +// the collection's text properties (title and country). It seeds a few reviews +// so the named-vector near-text snippet has a target vector to search. +func setupWineReviewNV(t *testing.T, client *weaviate.Client) { + t.Helper() + ctx := context.Background() + _ = client.Collections.Delete(ctx, "WineReviewNV") + if _, err := client.Collections.Create(ctx, collections.Collection{ + Name: "WineReviewNV", + Properties: []collections.Property{ + {Name: "title", DataType: collections.DataTypeText}, + {Name: "country", DataType: collections.DataTypeText}, + }, + Vectors: map[string]collections.VectorConfig{ + "title_country": {Vectorizer: contextionaryVectorizer{}}, + }, + }); err != nil { + t.Fatalf("create WineReviewNV collection: %v", err) + } + + w1 := uuid.MustParse("1a2b3c4d-5e6f-4a5b-8c9d-1a2b3c4d5e6f") + w2 := uuid.MustParse("2b3c4d5e-6f7a-4b5c-8d9e-2b3c4d5e6f7a") + w3 := uuid.MustParse("3c4d5e6f-7a8b-4c5d-8e9f-3c4d5e6f7a8b") + w4 := uuid.MustParse("4d5e6f7a-8b9c-4d5e-8f90-4d5e6f7a8b9c") + reviews := client.Collections.Use("WineReviewNV") + if _, err := reviews.Data.Insert(ctx, + &data.Object{UUID: &w1, Properties: map[string]any{"title": "A sweet white Riesling wine", "country": "Germany"}}, + &data.Object{UUID: &w2, Properties: map[string]any{"title": "A dry white Chardonnay wine", "country": "France"}}, + &data.Object{UUID: &w3, Properties: map[string]any{"title": "A bold red Cabernet Sauvignon wine", "country": "United States"}}, + &data.Object{UUID: &w4, Properties: map[string]any{"title": "A crisp white Sauvignon Blanc wine", "country": "New Zealand"}}, + ); err != nil { + t.Fatalf("seed WineReviewNV: %v", err) + } + + waitForCount(t, reviews, 4) +} + +// setupJeopardyTinyVectorized (re)creates JeopardyTiny with two named vectors, +// "jeopardy_questions_vector" and "jeopardy_answers_vector", both produced +// server-side by text2vec-contextionary. It is the vectorized companion to +// setupJeopardyTiny (which brings its own vectors): the multi-target near-text +// snippets need server-side vectors so the query text can be vectorized against +// each named target, whereas the multi-target near-vector snippets supply their +// own vectors and use setupJeopardyTiny. +func setupJeopardyTinyVectorized(t *testing.T, client *weaviate.Client) { + t.Helper() + ctx := context.Background() + _ = client.Collections.Delete(ctx, "JeopardyTiny") + if _, err := client.Collections.Create(ctx, collections.Collection{ + Name: "JeopardyTiny", + Properties: []collections.Property{ + {Name: "question", DataType: collections.DataTypeText}, + {Name: "answer", DataType: collections.DataTypeText}, + }, + Vectors: map[string]collections.VectorConfig{ + "jeopardy_questions_vector": {Vectorizer: contextionaryVectorizer{}}, + "jeopardy_answers_vector": {Vectorizer: contextionaryVectorizer{}}, + }, + }); err != nil { + t.Fatalf("create vectorized JeopardyTiny collection: %v", err) + } + + t1 := uuid.MustParse("5e6f7a8b-9c0d-4e5f-8a01-5e6f7a8b9c0d") + t2 := uuid.MustParse("6f7a8b9c-0d1e-4f5a-8b02-6f7a8b9c0d1e") + t3 := uuid.MustParse("7a8b9c0d-1e2f-4a5b-8c03-7a8b9c0d1e2f") + jeopardy := client.Collections.Use("JeopardyTiny") + if _, err := jeopardy.Data.Insert(ctx, + &data.Object{UUID: &t1, Properties: map[string]any{"question": "This organ removes excess glucose from the blood", "answer": "Liver"}}, + &data.Object{UUID: &t2, Properties: map[string]any{"question": "This large animal is the only living mammal in the order Proboscidea", "answer": "Elephant"}}, + &data.Object{UUID: &t3, Properties: map[string]any{"question": "This tall animal has a long neck and roams the savanna", "answer": "Giraffe"}}, + ); err != nil { + t.Fatalf("seed vectorized JeopardyTiny: %v", err) + } + + waitForCount(t, jeopardy, 3) +} diff --git a/_includes/code/go-v6/manage_collections_test.go b/_includes/code/go-v6/manage_collections_test.go new file mode 100644 index 00000000..e0c6e84c --- /dev/null +++ b/_includes/code/go-v6/manage_collections_test.go @@ -0,0 +1,125 @@ +package main + +import ( + "context" + "testing" + + "github.com/weaviate/weaviate-go-client/v6/collections" +) + +func TestCreateCollection(t *testing.T) { + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + _ = client.Collections.Delete(ctx, "Article") + defer client.Collections.Delete(ctx, "Article") + + // START CreateCollection + _, err := client.Collections.Create(ctx, collections.Collection{ + Name: "Article", + Properties: []collections.Property{ + {Name: "title", DataType: collections.DataTypeText}, + {Name: "body", DataType: collections.DataTypeText}, + }, + }) + // END CreateCollection + if err != nil { + t.Fatal(err) + } +} + +// The collection-management snippets below run against a live server. They are +// kept out of the CI run set (compile-only) and skip when executed directly. + +// TestBasicCreateCollection creates a collection with only a name. Missing +// properties are added by auto-schema when data is first inserted. +func TestBasicCreateCollection(t *testing.T) { + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + _ = client.Collections.Delete(ctx, "Article") + defer client.Collections.Delete(ctx, "Article") + + // START BasicCreateCollection + _, err := client.Collections.Create(ctx, collections.Collection{ + Name: "Article", + }) + // END BasicCreateCollection + if err != nil { + t.Fatal(err) + } +} + +// TestCreateCollectionWithProperties defines the collection properties and +// their data types up front instead of relying on auto-schema. +func TestCreateCollectionWithProperties(t *testing.T) { + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + _ = client.Collections.Delete(ctx, "Article") + defer client.Collections.Delete(ctx, "Article") + + // START CreateCollectionWithProperties + _, err := client.Collections.Create(ctx, collections.Collection{ + Name: "Article", + Properties: []collections.Property{ + {Name: "title", DataType: collections.DataTypeText}, + {Name: "body", DataType: collections.DataTypeText}, + }, + }) + // END CreateCollectionWithProperties + if err != nil { + t.Fatal(err) + } +} + +// TestCheckIfExists reports whether a collection is defined in the schema. +func TestCheckIfExists(t *testing.T) { + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + // START CheckIfExists + exists, err := client.Collections.Exists(ctx, "Article") + // END CheckIfExists + if err != nil { + t.Fatal(err) + } + t.Logf("Article exists: %v", exists) +} + +// TestReadOneCollection reads a single collection definition from the schema. +func TestReadOneCollection(t *testing.T) { + t.Skip("requires a running Weaviate instance") + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + // START ReadOneCollection + config, err := client.Collections.GetConfig(ctx, "Article") + // END ReadOneCollection + if err != nil { + t.Fatal(err) + } + t.Logf("%s", config.Name) +} + +// TestReadAllCollections reads every collection definition in the schema. +func TestReadAllCollections(t *testing.T) { + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + // START ReadAllCollections + configs, err := client.Collections.List(ctx) + // END ReadAllCollections + if err != nil { + t.Fatal(err) + } + for _, config := range configs { + t.Logf("%s", config.Name) + } +} diff --git a/_includes/code/go-v6/manage_objects_test.go b/_includes/code/go-v6/manage_objects_test.go new file mode 100644 index 00000000..f14c5e67 --- /dev/null +++ b/_includes/code/go-v6/manage_objects_test.go @@ -0,0 +1,200 @@ +package main + +import ( + "context" + "testing" + + "github.com/google/uuid" + weaviate "github.com/weaviate/weaviate-go-client/v6" + "github.com/weaviate/weaviate-go-client/v6/collections" + "github.com/weaviate/weaviate-go-client/v6/data" + "github.com/weaviate/weaviate-go-client/v6/query/filter" +) + +// setupJeopardy (re)creates a minimal JeopardyQuestion collection used by the +// object how-to snippets. It has no vectorizer, so objects carry explicit +// properties only. +func setupJeopardy(t *testing.T, client *weaviate.Client) { + t.Helper() + ctx := context.Background() + // Start from a clean slate; ignore the error when the collection is absent. + _ = client.Collections.Delete(ctx, "JeopardyQuestion") + if _, err := client.Collections.Create(ctx, collections.Collection{ + Name: "JeopardyQuestion", + Properties: []collections.Property{ + {Name: "question", DataType: collections.DataTypeText}, + {Name: "answer", DataType: collections.DataTypeText}, + {Name: "category", DataType: collections.DataTypeText}, + }, + }); err != nil { + t.Fatalf("create JeopardyQuestion collection: %v", err) + } +} + +func TestCreateObject(t *testing.T) { + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + setupJeopardy(t, client) + defer client.Collections.Delete(ctx, "JeopardyQuestion") + + questions := client.Collections.Use("JeopardyQuestion") + + // START CreateObject + res, err := questions.Data.Insert(ctx, &data.Object{ + Properties: map[string]any{ + "question": "This vector database is open source and written in Go", + "answer": "Weaviate", + "category": "SCIENCE", + }, + }) + if err != nil { + t.Fatal(err) + } + // END CreateObject + + for id, msg := range res.Errors { + if msg != "" { + t.Fatalf("insert %s: %s", id, msg) + } + } +} + +func TestReplaceObject(t *testing.T) { + // Deferred: this is a go-client v6 (alpha) bug, not a snippet bug. The client's + // Replace serializes the PUT body WITHOUT the object id (it sends the id only in + // the URL path), but Weaviate's class-scoped PUT handler requires the body id to + // equal the path id, so the server rejects it with HTTP 422 "field 'id' is + // immutable". The published UpdateReplace snippet below is already idiomatic and + // carries the object's UUID; re-enable this test once the client sends the id. + t.Skip("go-client v6 Data.Replace omits the object id from the PUT body; Weaviate requires body id == path id (HTTP 422 \"field 'id' is immutable\") โ€” deferred pending a client fix") + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + setupJeopardy(t, client) + defer client.Collections.Delete(ctx, "JeopardyQuestion") + + questions := client.Collections.Use("JeopardyQuestion") + + id := uuid.MustParse("e1f2a3b4-c5d6-4e7f-8a9b-4c5d6e7f8a9b") + if _, err := questions.Data.Insert(ctx, &data.Object{ + UUID: &id, + Properties: map[string]any{ + "question": "Placeholder question", + "answer": "Placeholder answer", + "category": "SCIENCE", + }, + }); err != nil { + t.Fatal(err) + } + + // START UpdateReplace + // Replace overwrites the whole object. Properties that are omitted here are + // removed from the stored object, so include every value you want to keep. + err := questions.Data.Replace(ctx, data.Object{ + UUID: &id, + Properties: map[string]any{ + "question": "This vector database is open source and written in Go", + "answer": "Weaviate", + "category": "SCIENCE", + }, + }) + if err != nil { + t.Fatal(err) + } + // END UpdateReplace +} + +// TestPartialUpdate is a placeholder: the v6 Go client can replace a whole +// object but cannot yet merge a partial update into an existing object. +func TestPartialUpdate(t *testing.T) { + t.Skip("partial update (merge) is not yet available in the v6 Go client; use Data.Replace for a full update") + + // TODO[g-despot]: partial update (merge) snippet pending v6 client support + // START UpdateMerge + // Coming soon + // END UpdateMerge +} + +func TestDeleteObject(t *testing.T) { + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + setupJeopardy(t, client) + defer client.Collections.Delete(ctx, "JeopardyQuestion") + + questions := client.Collections.Use("JeopardyQuestion") + + id := uuid.MustParse("f1a2b3c4-d5e6-4f7a-8b9c-5d6e7f8a9b0c") + if _, err := questions.Data.Insert(ctx, &data.Object{ + UUID: &id, + Properties: map[string]any{"question": "This object will be deleted"}, + }); err != nil { + t.Fatal(err) + } + + // START DeleteObject + err := questions.Data.Delete(ctx, id) + if err != nil { + t.Fatal(err) + } + // END DeleteObject +} + +func TestDeleteMany(t *testing.T) { + // Deferred: this is a go-client v6 (alpha) bug, not a snippet bug. Data.DeleteSelected + // panics because *api.DeleteObjectsRequest is not wired into the gRPC transport + // dispatch (internal/api/transport/transport.go has no BatchDeleteRequest case), so it + // falls through to dev.Assert(false, "...does not implement MessageMarshaler..."). The + // published DeleteMany snippet below is idiomatic; re-enable once the client wires it up. + t.Skip("go-client v6 Data.DeleteSelected panics โ€” *api.DeleteObjectsRequest not wired to gRPC MessageMarshaler; deferred pending a client fix") + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + setupJeopardy(t, client) + defer client.Collections.Delete(ctx, "JeopardyQuestion") + + questions := client.Collections.Use("JeopardyQuestion") + dm1 := uuid.MustParse("a2b3c4d5-e6f7-4a8b-8c9d-6e7f8a9b0c1d") + dm2 := uuid.MustParse("b2c3d4e5-f6a7-4b8c-8d9e-7f8a9b0c1d2e") + if _, err := questions.Data.Insert(ctx, + &data.Object{UUID: &dm1, Properties: map[string]any{"answer": "Hawaii", "category": "GEOGRAPHY"}}, + &data.Object{UUID: &dm2, Properties: map[string]any{"answer": "Kilauea", "category": "GEOGRAPHY"}}, + ); err != nil { + t.Fatal(err) + } + + // START DeleteMany + res, err := questions.Data.DeleteSelected(ctx, data.DeleteSelected{ + Filter: &filter.Cond{ + Target: "category", + Operator: filter.Equal, + Value: "GEOGRAPHY", + }, + }) + if err != nil { + t.Fatal(err) + } + // END DeleteMany + + for id, delErr := range res.Errors { + if delErr != nil { + t.Fatalf("delete %s: %v", id, delErr) + } + } +} + +// TestReadObjectByID is a placeholder: the v6 Go client does not yet expose a +// fetch-object-by-id call. Retrieve objects through a search query for now. +func TestReadObjectByID(t *testing.T) { + t.Skip("fetch-object-by-id is not yet available in the v6 Go client") + + // TODO[g-despot]: fetch-object-by-id snippet pending v6 client support + // START ReadObject + // Coming soon + // END ReadObject +} diff --git a/_includes/code/go-v6/multi_target_test.go b/_includes/code/go-v6/multi_target_test.go new file mode 100644 index 00000000..8f7a27d7 --- /dev/null +++ b/_includes/code/go-v6/multi_target_test.go @@ -0,0 +1,306 @@ +package main + +import ( + "context" + "testing" + + "github.com/google/uuid" + weaviate "github.com/weaviate/weaviate-go-client/v6" + "github.com/weaviate/weaviate-go-client/v6/collections" + "github.com/weaviate/weaviate-go-client/v6/data" + "github.com/weaviate/weaviate-go-client/v6/modules/selfprovided" + "github.com/weaviate/weaviate-go-client/v6/query" + "github.com/weaviate/weaviate-go-client/v6/types" +) + +// The multi-target vector snippets query a "JeopardyTiny" collection with two +// named vectors. The bring-your-own-vector (NearVector) variants run live via +// setupJeopardyTiny; the near-text variants stay skipped until a vectorizer is +// available in this CI lane. + +// setupJeopardyTiny (re)creates JeopardyTiny with two bring-your-own named +// vectors and seeds objects that carry both. The vector-config keys match the +// target names used by the multi-target near-vector snippets. +func setupJeopardyTiny(t *testing.T, client *weaviate.Client) { + t.Helper() + ctx := context.Background() + _ = client.Collections.Delete(ctx, "JeopardyTiny") + if _, err := client.Collections.Create(ctx, collections.Collection{ + Name: "JeopardyTiny", + Properties: []collections.Property{ + {Name: "question", DataType: collections.DataTypeText}, + {Name: "answer", DataType: collections.DataTypeText}, + }, + Vectors: map[string]collections.VectorConfig{ + "jeopardy_questions_vector": {Vectorizer: selfprovided.Vectorizer}, + "jeopardy_answers_vector": {Vectorizer: selfprovided.Vectorizer}, + }, + }); err != nil { + t.Fatalf("create JeopardyTiny collection: %v", err) + } + + jeopardy := client.Collections.Use("JeopardyTiny") + // Fixed, non-leading-zero ids keep the near-vector run deterministic: a + // server-assigned id beginning 0x00 truncates to 15 bytes in the gRPC + // id_as_bytes reply field and the client rejects the whole SearchReply. See + // the filterByIdSeedUUID note in main_test.go. + t1 := uuid.MustParse("8d9e0f1a-2b3c-4d5e-8f60-4d5e6f7a8b9c") + t2 := uuid.MustParse("9e0f1a2b-3c4d-4e5f-8061-5e6f7a8b9c0d") + t3 := uuid.MustParse("af1a2b3c-4d5e-4f6a-8b62-6f7a8b9c0d1e") + if _, err := jeopardy.Data.Insert(ctx, + &data.Object{ + UUID: &t1, + Properties: map[string]any{"question": "This organ removes excess glucose from the blood", "answer": "Liver"}, + Vectors: []types.Vector{ + {Name: "jeopardy_questions_vector", Single: []float32{0.10, 0.21, 0.32}}, + {Name: "jeopardy_answers_vector", Single: []float32{0.20, 0.11, 0.30}}, + }, + }, + &data.Object{ + UUID: &t2, + Properties: map[string]any{"question": "The only living mammal in the order Proboscidea", "answer": "Elephant"}, + Vectors: []types.Vector{ + {Name: "jeopardy_questions_vector", Single: []float32{0.11, 0.20, 0.34}}, + {Name: "jeopardy_answers_vector", Single: []float32{0.14, 0.19, 0.30}}, + }, + }, + &data.Object{ + UUID: &t3, + Properties: map[string]any{"question": "This tall animal has a long neck and roams the savanna", "answer": "Giraffe"}, + Vectors: []types.Vector{ + {Name: "jeopardy_questions_vector", Single: []float32{0.14, 0.19, 0.30}}, + {Name: "jeopardy_answers_vector", Single: []float32{0.11, 0.20, 0.34}}, + }, + }, + ); err != nil { + t.Fatalf("seed JeopardyTiny: %v", err) + } + + waitForCount(t, jeopardy, 3) +} + +// TestMultiBasic searches several target vectors by name. With no join strategy +// specified, Weaviate combines the results using the default (minimum) strategy. +func TestMultiBasic(t *testing.T) { + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + setupJeopardyTinyVectorized(t, client) + defer client.Collections.Delete(ctx, "JeopardyTiny") + + // START MultiBasic + jeopardy := client.Collections.Use("JeopardyTiny") + response, err := jeopardy.Query.NearText(ctx, query.NearText{ + Concepts: []string{"a wild animal"}, + // Minimum is the default join strategy when combining target vectors. + Target: query.Min([]query.VectorName{ + "jeopardy_questions_vector", + "jeopardy_answers_vector", + }), + Limit: 2, + ReturnMetadata: query.ReturnMetadata{Distance: true}, + }) + if err != nil { + t.Fatal(err) + } + for _, obj := range response.Objects { + t.Logf("%v", obj.Properties) + } + // END MultiBasic +} + +// TestMultiTargetNearVector supplies a separate query vector for each target +// vector. +func TestMultiTargetNearVector(t *testing.T) { + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + setupJeopardyTiny(t, client) + defer client.Collections.Delete(ctx, "JeopardyTiny") + + v1 := []float32{0.12, 0.20, 0.33} + v2 := []float32{0.14, 0.19, 0.30} + + // START MultiTargetNearVector + jeopardy := client.Collections.Use("JeopardyTiny") + response, err := jeopardy.Query.NearVector(ctx, query.NearVector{ + // Pair each named target vector with its own query vector. + Target: query.Min([]types.Vector{ + {Name: "jeopardy_questions_vector", Single: v1}, + {Name: "jeopardy_answers_vector", Single: v2}, + }), + Limit: 2, + ReturnMetadata: query.ReturnMetadata{Distance: true}, + }) + if err != nil { + t.Fatal(err) + } + for _, obj := range response.Objects { + t.Logf("%v", obj.Properties) + } + // END MultiTargetNearVector +} + +// TestMultiTargetMultipleNearVectorsV1 targets the same vector more than once by +// listing it twice with different query vectors. +func TestMultiTargetMultipleNearVectorsV1(t *testing.T) { + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + setupJeopardyTiny(t, client) + defer client.Collections.Delete(ctx, "JeopardyTiny") + + v1 := []float32{0.12, 0.20, 0.33} + v2 := []float32{0.14, 0.19, 0.30} + v3 := []float32{0.11, 0.20, 0.34} + + // START MultiTargetMultipleNearVectorsV1 + jeopardy := client.Collections.Use("JeopardyTiny") + response, err := jeopardy.Query.NearVector(ctx, query.NearVector{ + // A target vector name may appear more than once, each with its own vector. + Target: query.Min([]types.Vector{ + {Name: "jeopardy_questions_vector", Single: v1}, + {Name: "jeopardy_answers_vector", Single: v2}, + {Name: "jeopardy_answers_vector", Single: v3}, + }), + Limit: 2, + ReturnMetadata: query.ReturnMetadata{Distance: true}, + }) + if err != nil { + t.Fatal(err) + } + for _, obj := range response.Objects { + t.Logf("%v", obj.Properties) + } + // END MultiTargetMultipleNearVectorsV1 +} + +// TestMultiTargetMultipleNearVectorsV2 assigns a weight to each query vector. +func TestMultiTargetMultipleNearVectorsV2(t *testing.T) { + t.Skip("go-client v6 mis-marshals a repeated target with per-vector weights: marshalNearVector (internal/api/search.go) de-duplicates TargetVectors but appends one WeightsForTarget per vector, so a target named twice yields len(weights)=3 vs len(targets)=2. Weaviate 1.38 rejects it (parse_search_request.go extractWeights requires equal, positional counts): \"number of weights (3) does not match number of targets (2)\". The Python/Java clients duplicate the target name to match the weights; the Go client should too. Snippet is idiomatic (matches the other clients); deferred pending a client fix") + + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + setupJeopardyTiny(t, client) + defer client.Collections.Delete(ctx, "JeopardyTiny") + + v1 := []float32{0.12, 0.20, 0.33} + v2 := []float32{0.14, 0.19, 0.30} + v3 := []float32{0.11, 0.20, 0.34} + + // START MultiTargetMultipleNearVectorsV2 + jeopardy := client.Collections.Use("JeopardyTiny") + response, err := jeopardy.Query.NearVector(ctx, query.NearVector{ + // Weight each query vector; repeated targets are weighted independently. + Target: query.ManualWeights([]query.WeightedVector[types.Vector]{ + query.Weighted(types.Vector{Name: "jeopardy_questions_vector", Single: v1}, 10), + query.Weighted(types.Vector{Name: "jeopardy_answers_vector", Single: v2}, 30), + query.Weighted(types.Vector{Name: "jeopardy_answers_vector", Single: v3}, 30), + }), + Limit: 2, + ReturnMetadata: query.ReturnMetadata{Distance: true}, + }) + if err != nil { + t.Fatal(err) + } + for _, obj := range response.Objects { + t.Logf("%v", obj.Properties) + } + // END MultiTargetMultipleNearVectorsV2 +} + +// TestMultiTargetWithSimpleJoin selects a named join strategy for the target +// vectors. +func TestMultiTargetWithSimpleJoin(t *testing.T) { + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + setupJeopardyTinyVectorized(t, client) + defer client.Collections.Delete(ctx, "JeopardyTiny") + + // START MultiTargetWithSimpleJoin + jeopardy := client.Collections.Use("JeopardyTiny") + response, err := jeopardy.Query.NearText(ctx, query.NearText{ + Concepts: []string{"a wild animal"}, + // query.Sum, query.Min, query.ManualWeights, and query.RelativeScore + // are also available. + Target: query.Average([]query.VectorName{ + "jeopardy_questions_vector", + "jeopardy_answers_vector", + }), + Limit: 2, + ReturnMetadata: query.ReturnMetadata{Distance: true}, + }) + if err != nil { + t.Fatal(err) + } + for _, obj := range response.Objects { + t.Logf("%v", obj.Properties) + } + // END MultiTargetWithSimpleJoin +} + +// TestMultiTargetManualWeights weights the raw distance to each target vector. +func TestMultiTargetManualWeights(t *testing.T) { + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + setupJeopardyTinyVectorized(t, client) + defer client.Collections.Delete(ctx, "JeopardyTiny") + + // START MultiTargetManualWeights + jeopardy := client.Collections.Use("JeopardyTiny") + response, err := jeopardy.Query.NearText(ctx, query.NearText{ + Concepts: []string{"a wild animal"}, + Target: query.ManualWeights([]query.WeightedVector[query.VectorName]{ + query.Weighted(query.VectorName("jeopardy_questions_vector"), 10), + query.Weighted(query.VectorName("jeopardy_answers_vector"), 50), + }), + Limit: 2, + ReturnMetadata: query.ReturnMetadata{Distance: true}, + }) + if err != nil { + t.Fatal(err) + } + for _, obj := range response.Objects { + t.Logf("%v", obj.Properties) + } + // END MultiTargetManualWeights +} + +// TestMultiTargetRelativeScore weights the normalized distance to each target +// vector. +func TestMultiTargetRelativeScore(t *testing.T) { + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + setupJeopardyTinyVectorized(t, client) + defer client.Collections.Delete(ctx, "JeopardyTiny") + + // START MultiTargetRelativeScore + jeopardy := client.Collections.Use("JeopardyTiny") + response, err := jeopardy.Query.NearText(ctx, query.NearText{ + Concepts: []string{"a wild animal"}, + Target: query.RelativeScore([]query.WeightedVector[query.VectorName]{ + query.Weighted(query.VectorName("jeopardy_questions_vector"), 10), + query.Weighted(query.VectorName("jeopardy_answers_vector"), 10), + }), + Limit: 2, + ReturnMetadata: query.ReturnMetadata{Distance: true}, + }) + if err != nil { + t.Fatal(err) + } + for _, obj := range response.Objects { + t.Logf("%v", obj.Properties) + } + // END MultiTargetRelativeScore +} diff --git a/_includes/code/go-v6/rbac_test.go b/_includes/code/go-v6/rbac_test.go new file mode 100644 index 00000000..fbf2a51b --- /dev/null +++ b/_includes/code/go-v6/rbac_test.go @@ -0,0 +1,891 @@ +package main + +import ( + "context" + "testing" + + weaviate "github.com/weaviate/weaviate-go-client/v6" + "github.com/weaviate/weaviate-go-client/v6/rbac" +) + +// The RBAC snippets run against the RBAC-enabled instance (connectRBACAdmin, +// service weaviate_rbac on :8580). TestRBACAdminClient stays skipped: its connect +// is INSIDE the snippet marker (weaviate.NewLocal + WithAPIKey), so it cannot be +// redirected to the non-default RBAC port without leaking the port into the +// rendered snippet โ€” the same class as TestConnectLocalAuth. +const rbacSkip = "connect is inside the snippet marker (NewLocal+WithAPIKey); cannot redirect to the non-default RBAC port without leaking it into the rendered snippet" + +// ----------------------------------------------------------------------------- +// Test-only isolation helpers. They live outside every snippet marker, so the +// rendered snippets are unaffected. +// +// Roles, database users and OIDC role assignments are cluster-global, not scoped +// to a collection, and these tests run sequentially against one shared RBAC +// instance. Each test therefore deletes the role/user it touches BEFORE it runs +// (clearing anything a previous failed run leaked) and AFTER (via defer, while the +// client is still open), and seeds any prerequisite a snippet assumes already +// exists. Every RBAC operation is a REST call, so none of them hit the gRPC +// transport-switch panic path (unlike Tenants.Get / batch-delete). +// ----------------------------------------------------------------------------- + +// deleteRoleIfExists best-effort removes a role, ignoring a missing role. Safe to +// call for delete-before-create isolation and as deferred cleanup. +func deleteRoleIfExists(client *weaviate.Client, roleID string) { + ctx := context.Background() + if exists, err := client.Roles.Exists(ctx, roleID); err == nil && exists { + _ = client.Roles.Delete(ctx, roleID) + } +} + +// seedRole (re)creates roleID with a known permission set so snippets that expect +// the role to already exist (add/remove permissions, inspect, assign, delete) have +// something to act on. The set is a superset of the permissions +// TestRBACRemovePermissions removes, plus a cluster-read permission it does not +// remove, so the role never ends up empty. +func seedRole(t *testing.T, client *weaviate.Client, roleID string) { + t.Helper() + ctx := context.Background() + deleteRoleIfExists(client, roleID) + if err := client.Roles.Create(ctx, rbac.Role{ + ID: roleID, + Permissions: rbac.Permissions{ + Collections: []rbac.CollectionPermission{ + {Collection: "TargetCollection*", Create: true, Read: true, Update: true, Delete: true}, + }, + Data: []rbac.DataPermission{ + {Collection: "TargetCollection*", Read: true}, + }, + Cluster: []rbac.ClusterPermission{{Read: true}}, + }, + }); err != nil { + t.Fatalf("seed role %q: %v", roleID, err) + } +} + +// deleteDBUserIfExists best-effort removes a database user, ignoring a missing +// user. Safe for delete-before-create isolation and deferred cleanup. +func deleteDBUserIfExists(client *weaviate.Client, userID string) { + _ = client.Users.DB.Delete(context.Background(), userID) +} + +// seedDBUser (re)creates a database user so snippets that expect the user to +// already exist (delete, rotate key, assign/revoke/list roles) have a target. +func seedDBUser(t *testing.T, client *weaviate.Client, userID string) { + t.Helper() + deleteDBUserIfExists(client, userID) + if _, err := client.Users.DB.Create(context.Background(), userID); err != nil { + t.Fatalf("seed database user %q: %v", userID, err) + } +} + +// ----------------------------------------------------------------------------- +// Requirements +// ----------------------------------------------------------------------------- + +// TestRBACAdminClient connects with a key belonging to a user that has the +// permissions needed to manage roles and users. +func TestRBACAdminClient(t *testing.T) { + t.Skip(rbacSkip) + ctx := context.Background() + + // START AdminClient + client, err := weaviate.NewLocal(ctx, + weaviate.WithAPIKey("admin-api-key"), + ) + if err != nil { + t.Fatal(err) + } + defer client.Close() + // END AdminClient +} + +// ----------------------------------------------------------------------------- +// Role management: create roles with permissions +// ----------------------------------------------------------------------------- + +// TestRBACAddManageRolesPermission creates a role that can manage other roles. +func TestRBACAddManageRolesPermission(t *testing.T) { + ctx := context.Background() + client := connectRBACAdmin(t) + defer client.Close() + deleteRoleIfExists(client, "testRole") + defer deleteRoleIfExists(client, "testRole") + + // START AddManageRolesPermission + err := client.Roles.Create(ctx, rbac.Role{ + ID: "testRole", + Permissions: rbac.Permissions{ + Roles: []rbac.RolePermission{ + { + RoleID: "testRole*", // Applies to all roles starting with "testRole". + // Match limits role management to the current user's permission + // level; use rbac.RoleScopeAll to allow managing all permissions. + Scope: rbac.RoleScopeMatch, + Create: true, // Allow creating roles. + Read: true, // Allow reading roles. + Update: true, // Allow updating roles. + Delete: true, // Allow deleting roles. + }, + }, + }, + }) + // END AddManageRolesPermission + if err != nil { + t.Fatal(err) + } +} + +// TestRBACAddManageUsersPermission creates a role that can manage users. +func TestRBACAddManageUsersPermission(t *testing.T) { + ctx := context.Background() + client := connectRBACAdmin(t) + defer client.Close() + deleteRoleIfExists(client, "testRole") + defer deleteRoleIfExists(client, "testRole") + + // START AddManageUsersPermission + err := client.Roles.Create(ctx, rbac.Role{ + ID: "testRole", + Permissions: rbac.Permissions{ + Users: []rbac.UserPermission{ + { + UserID: "testUser*", // Applies to all users starting with "testUser". + Create: true, // Allow creating users. + Read: true, // Allow reading user info. + Update: true, // Allow rotating a user's API key. + Delete: true, // Allow deleting users. + AssignAndRevoke: true, // Allow assigning and revoking roles. + }, + }, + }, + }) + // END AddManageUsersPermission + if err != nil { + t.Fatal(err) + } +} + +// TestRBACAddCollectionsPermission creates a role with collection permissions. +func TestRBACAddCollectionsPermission(t *testing.T) { + ctx := context.Background() + client := connectRBACAdmin(t) + defer client.Close() + deleteRoleIfExists(client, "testRole") + defer deleteRoleIfExists(client, "testRole") + + // START AddCollectionsPermission + err := client.Roles.Create(ctx, rbac.Role{ + ID: "testRole", + Permissions: rbac.Permissions{ + Collections: []rbac.CollectionPermission{ + { + Collection: "TargetCollection*", // Applies to all matching collections. + Create: true, // Allow creating collections. + Read: true, // Allow reading collection config. + Update: true, // Allow updating collection config. + Delete: true, // Allow deleting collections. + }, + }, + }, + }) + // END AddCollectionsPermission + if err != nil { + t.Fatal(err) + } +} + +// TestRBACAddTenantPermission creates a role with tenant permissions. +func TestRBACAddTenantPermission(t *testing.T) { + ctx := context.Background() + client := connectRBACAdmin(t) + defer client.Close() + deleteRoleIfExists(client, "testRole") + defer deleteRoleIfExists(client, "testRole") + + // START AddTenantPermission + err := client.Roles.Create(ctx, rbac.Role{ + ID: "testRole", + Permissions: rbac.Permissions{ + Tenants: []rbac.TenantPermission{ + { + Collection: "TargetCollection*", // Applies to all matching collections. + Tenant: "TargetTenant*", // Applies to all matching tenants. + Create: true, // Allow creating tenants. + Read: true, // Allow reading tenant info. + Update: true, // Allow updating tenant states. + Delete: true, // Allow deleting tenants. + }, + }, + }, + }) + // END AddTenantPermission + if err != nil { + t.Fatal(err) + } +} + +// TestRBACAddDataObjectPermission creates a role with data object permissions. +func TestRBACAddDataObjectPermission(t *testing.T) { + ctx := context.Background() + client := connectRBACAdmin(t) + defer client.Close() + deleteRoleIfExists(client, "testRole") + defer deleteRoleIfExists(client, "testRole") + + // START AddDataObjectPermission + err := client.Roles.Create(ctx, rbac.Role{ + ID: "testRole", + Permissions: rbac.Permissions{ + Data: []rbac.DataPermission{ + { + Collection: "TargetCollection*", // Applies to all matching collections. + Tenant: "TargetTenant*", // Applies to all matching tenants. + Create: true, // Allow data inserts. + Read: true, // Allow query and fetch operations. + Update: true, // Allow data updates. + // Delete is left false, disallowing data deletes. + }, + }, + }, + }) + // END AddDataObjectPermission + if err != nil { + t.Fatal(err) + } +} + +// TestRBACAddBackupPermission creates a role with backup permissions. +func TestRBACAddBackupPermission(t *testing.T) { + ctx := context.Background() + client := connectRBACAdmin(t) + defer client.Close() + deleteRoleIfExists(client, "testRole") + defer deleteRoleIfExists(client, "testRole") + + // START AddBackupPermission + err := client.Roles.Create(ctx, rbac.Role{ + ID: "testRole", + Permissions: rbac.Permissions{ + Backups: []rbac.BackupsPermission{ + { + Collection: "TargetCollection*", // Applies to all matching collections. + Manage: true, // Allow managing backups. + }, + }, + }, + }) + // END AddBackupPermission + if err != nil { + t.Fatal(err) + } +} + +// TestRBACAddClusterPermission creates a role with cluster read permission. +func TestRBACAddClusterPermission(t *testing.T) { + ctx := context.Background() + client := connectRBACAdmin(t) + defer client.Close() + deleteRoleIfExists(client, "testRole") + defer deleteRoleIfExists(client, "testRole") + + // START AddClusterPermission + err := client.Roles.Create(ctx, rbac.Role{ + ID: "testRole", + Permissions: rbac.Permissions{ + Cluster: []rbac.ClusterPermission{ + {Read: true}, // Allow reading cluster data. + }, + }, + }) + // END AddClusterPermission + if err != nil { + t.Fatal(err) + } +} + +// TestRBACAddNodesPermission creates a role with node read permission. +func TestRBACAddNodesPermission(t *testing.T) { + ctx := context.Background() + client := connectRBACAdmin(t) + defer client.Close() + deleteRoleIfExists(client, "testRole") + defer deleteRoleIfExists(client, "testRole") + + // START AddNodesPermission + err := client.Roles.Create(ctx, rbac.Role{ + ID: "testRole", + Permissions: rbac.Permissions{ + Nodes: []rbac.NodesPermission{ + { + Collection: "TargetCollection*", // Verbose reads are scoped to a collection. + Verbosity: rbac.NodeVerbosityVerbose, // Or rbac.NodeVerbosityMinimal for all collections. + Read: true, // Allow reading node metadata. + }, + }, + }, + }) + // END AddNodesPermission + if err != nil { + t.Fatal(err) + } +} + +// TestRBACAddAliasPermission creates a role with alias permissions. +func TestRBACAddAliasPermission(t *testing.T) { + ctx := context.Background() + client := connectRBACAdmin(t) + defer client.Close() + deleteRoleIfExists(client, "testRole") + defer deleteRoleIfExists(client, "testRole") + + // START AddAliasPermission + err := client.Roles.Create(ctx, rbac.Role{ + ID: "testRole", + Permissions: rbac.Permissions{ + Aliases: []rbac.AliasPermission{ + { + Alias: "TargetAlias*", // Applies to all matching aliases. + Collection: "TargetCollection*", // Applies to all matching collections. + Create: true, // Allow alias creation. + Read: true, // Allow listing aliases. + Update: true, // Allow updating aliases. + // Delete is left false, disallowing alias deletion. + }, + }, + }, + }) + // END AddAliasPermission + if err != nil { + t.Fatal(err) + } +} + +// TestRBACAddReplicationsPermission creates a role with replication permissions. +func TestRBACAddReplicationsPermission(t *testing.T) { + ctx := context.Background() + client := connectRBACAdmin(t) + defer client.Close() + deleteRoleIfExists(client, "testRole") + defer deleteRoleIfExists(client, "testRole") + + // START AddReplicationsPermission + err := client.Roles.Create(ctx, rbac.Role{ + ID: "testRole", + Permissions: rbac.Permissions{ + Replication: []rbac.ReplicationPermission{ + { + Collection: "TargetCollection*", // Applies to all matching collections. + Shard: "TargetShard*", // Applies to all matching shards. + Create: true, // Allow replica movement operations. + Read: true, // Allow retrieving replication status. + Update: true, // Allow cancelling replication operations. + // Delete is left false, disallowing deleting replication operations. + }, + }, + }, + }) + // END AddReplicationsPermission + if err != nil { + t.Fatal(err) + } +} + +// TestRBACAddGroupsPermission creates a role with group permissions. +func TestRBACAddGroupsPermission(t *testing.T) { + ctx := context.Background() + client := connectRBACAdmin(t) + defer client.Close() + deleteRoleIfExists(client, "testRole") + defer deleteRoleIfExists(client, "testRole") + + // START AddGroupsPermission + err := client.Roles.Create(ctx, rbac.Role{ + ID: "testRole", + Permissions: rbac.Permissions{ + Groups: []rbac.GroupPermission{ + { + GroupID: "TargetGroup*", // Applies to all groups starting with "TargetGroup". + Type: rbac.GroupTypeOIDC, + Read: true, // Allow reading group information. + AssignAndRevoke: true, // Allow assigning and revoking group memberships. + }, + }, + }, + }) + // END AddGroupsPermission + if err != nil { + t.Fatal(err) + } +} + +// ----------------------------------------------------------------------------- +// Role management: modify and inspect roles +// ----------------------------------------------------------------------------- + +// TestRBACAddRoles grants additional permissions to an existing role. +func TestRBACAddRoles(t *testing.T) { + ctx := context.Background() + client := connectRBACAdmin(t) + defer client.Close() + seedRole(t, client, "testRole") + defer deleteRoleIfExists(client, "testRole") + + // START AddRoles + err := client.Roles.AddPermissions(ctx, rbac.AddPermissions{ + RoleID: "testRole", + Permissions: rbac.Permissions{ + Data: []rbac.DataPermission{ + {Collection: "TargetCollection*", Create: true}, + }, + }, + }) + // END AddRoles + if err != nil { + t.Fatal(err) + } +} + +// TestRBACRemovePermissions removes permissions from a role. +func TestRBACRemovePermissions(t *testing.T) { + ctx := context.Background() + client := connectRBACAdmin(t) + defer client.Close() + seedRole(t, client, "testRole") + defer deleteRoleIfExists(client, "testRole") + + // START RemovePermissions + err := client.Roles.RemovePermissions(ctx, rbac.RemovePermissions{ + RoleID: "testRole", + Permissions: rbac.Permissions{ + Collections: []rbac.CollectionPermission{ + {Collection: "TargetCollection*", Read: true, Create: true, Delete: true}, + }, + Data: []rbac.DataPermission{ + {Collection: "TargetCollection*", Read: true}, + }, + }, + }) + // END RemovePermissions + if err != nil { + t.Fatal(err) + } +} + +// TestRBACCheckRoleExists checks whether a role exists. +func TestRBACCheckRoleExists(t *testing.T) { + ctx := context.Background() + client := connectRBACAdmin(t) + defer client.Close() + + // START CheckRoleExists + exists, err := client.Roles.Exists(ctx, "testRole") + if err != nil { + t.Fatal(err) + } + t.Logf("testRole exists: %t", exists) + // END CheckRoleExists +} + +// TestRBACInspectRole retrieves a role and its permissions. +func TestRBACInspectRole(t *testing.T) { + ctx := context.Background() + client := connectRBACAdmin(t) + defer client.Close() + seedRole(t, client, "testRole") + defer deleteRoleIfExists(client, "testRole") + + // START InspectRole + role, err := client.Roles.Get(ctx, "testRole") + if err != nil { + t.Fatal(err) + } + t.Logf("role: %s", role.ID) + t.Logf("collection permissions: %+v", role.Collections) + t.Logf("data permissions: %+v", role.Data) + // END InspectRole +} + +// TestRBACListAllRoles lists every role in the instance. +func TestRBACListAllRoles(t *testing.T) { + ctx := context.Background() + client := connectRBACAdmin(t) + defer client.Close() + + // START ListAllRoles + roles, err := client.Roles.List(ctx) + if err != nil { + t.Fatal(err) + } + for _, role := range roles { + t.Logf("%s", role.ID) + } + // END ListAllRoles +} + +// TestRBACAssignedUsers lists the users that have a given role. +func TestRBACAssignedUsers(t *testing.T) { + ctx := context.Background() + client := connectRBACAdmin(t) + defer client.Close() + seedRole(t, client, "testRole") + defer deleteRoleIfExists(client, "testRole") + + // START AssignedUsers + userIDs, err := client.Roles.AssignedUserIDs(ctx, "testRole") + if err != nil { + t.Fatal(err) + } + for _, id := range userIDs { + t.Logf("assigned user: %s", id) + } + // END AssignedUsers +} + +// TestRBACDeleteRole deletes a role. +func TestRBACDeleteRole(t *testing.T) { + ctx := context.Background() + client := connectRBACAdmin(t) + defer client.Close() + seedRole(t, client, "testRole") + defer deleteRoleIfExists(client, "testRole") + + // START DeleteRole + err := client.Roles.Delete(ctx, "testRole") + // END DeleteRole + if err != nil { + t.Fatal(err) + } +} + +// ----------------------------------------------------------------------------- +// User management (database users) +// ----------------------------------------------------------------------------- + +// TestRBACListAllUsers lists all database users. +func TestRBACListAllUsers(t *testing.T) { + ctx := context.Background() + client := connectRBACAdmin(t) + defer client.Close() + + // START ListAllUsers + users, err := client.Users.DB.List(ctx, rbac.ListUsersOptions{}) + if err != nil { + t.Fatal(err) + } + for _, u := range users { + t.Logf("%s (active: %t)", u.ID, u.Active) + } + // END ListAllUsers +} + +// TestRBACCreateUser creates a database user and prints its API key. +func TestRBACCreateUser(t *testing.T) { + ctx := context.Background() + client := connectRBACAdmin(t) + defer client.Close() + deleteDBUserIfExists(client, "custom-user") + defer deleteDBUserIfExists(client, "custom-user") + + // START CreateUser + // Create returns the new user's API key. Store it securely; it cannot be + // retrieved again later. + apiKey, err := client.Users.DB.Create(ctx, "custom-user") + if err != nil { + t.Fatal(err) + } + t.Logf("new API key: %s", apiKey) + // END CreateUser +} + +// TestRBACDeleteUser deletes a database user. +func TestRBACDeleteUser(t *testing.T) { + ctx := context.Background() + client := connectRBACAdmin(t) + defer client.Close() + seedDBUser(t, client, "custom-user") + defer deleteDBUserIfExists(client, "custom-user") + + // START DeleteUser + err := client.Users.DB.Delete(ctx, "custom-user") + // END DeleteUser + if err != nil { + t.Fatal(err) + } +} + +// TestRBACRotateApiKey rotates a database user's API key. +func TestRBACRotateApiKey(t *testing.T) { + ctx := context.Background() + client := connectRBACAdmin(t) + defer client.Close() + seedDBUser(t, client, "custom-user") + defer deleteDBUserIfExists(client, "custom-user") + + // START RotateApiKey + newAPIKey, err := client.Users.DB.RotateKey(ctx, "custom-user") + if err != nil { + t.Fatal(err) + } + t.Logf("rotated API key: %s", newAPIKey) + // END RotateApiKey +} + +// TestRBACAssignRole assigns roles to a database user. +func TestRBACAssignRole(t *testing.T) { + ctx := context.Background() + client := connectRBACAdmin(t) + defer client.Close() + seedDBUser(t, client, "custom-user") + seedRole(t, client, "testRole") // "viewer" is a built-in role. + defer deleteDBUserIfExists(client, "custom-user") + defer deleteRoleIfExists(client, "testRole") + + // START AssignRole + err := client.Users.DB.AssignRoles(ctx, rbac.AssignRolesOptions{ + ID: "custom-user", + Roles: []string{"testRole", "viewer"}, + }) + // END AssignRole + if err != nil { + t.Fatal(err) + } +} + +// TestRBACRevokeRoles revokes roles from a database user. +func TestRBACRevokeRoles(t *testing.T) { + ctx := context.Background() + client := connectRBACAdmin(t) + defer client.Close() + seedDBUser(t, client, "custom-user") + seedRole(t, client, "testRole") + // Assign the role first so the snippet has something to revoke. + if err := client.Users.DB.AssignRoles(ctx, rbac.AssignRolesOptions{ + ID: "custom-user", + Roles: []string{"testRole"}, + }); err != nil { + t.Fatalf("seed role assignment: %v", err) + } + defer deleteDBUserIfExists(client, "custom-user") + defer deleteRoleIfExists(client, "testRole") + + // START RevokeRoles + err := client.Users.DB.RevokeRoles(ctx, rbac.RevokeRolesOptions{ + ID: "custom-user", + Roles: []string{"testRole"}, + }) + // END RevokeRoles + if err != nil { + t.Fatal(err) + } +} + +// TestRBACListUserRoles lists the roles assigned to a database user. +func TestRBACListUserRoles(t *testing.T) { + ctx := context.Background() + client := connectRBACAdmin(t) + defer client.Close() + seedDBUser(t, client, "custom-user") + defer deleteDBUserIfExists(client, "custom-user") + + // START ListUserRoles + roles, err := client.Users.DB.AssignedRoles(ctx, rbac.AssignedRolesOptions{ + ID: "custom-user", + }) + if err != nil { + t.Fatal(err) + } + for _, role := range roles { + t.Logf("%s", role.ID) + } + // END ListUserRoles +} + +// ----------------------------------------------------------------------------- +// OIDC users +// ----------------------------------------------------------------------------- + +// TestRBACAssignOidcUserRole assigns roles to an OIDC user. +func TestRBACAssignOidcUserRole(t *testing.T) { + ctx := context.Background() + client := connectRBACAdmin(t) + defer client.Close() + seedRole(t, client, "testRole") // "viewer" is a built-in role. + defer deleteRoleIfExists(client, "testRole") + defer func() { + _ = client.Users.OIDC.RevokeRoles(ctx, rbac.RevokeRolesOptions{ + ID: "custom-user", Roles: []string{"testRole", "viewer"}, + }) + }() + + // START AssignOidcUserRole + err := client.Users.OIDC.AssignRoles(ctx, rbac.AssignRolesOptions{ + ID: "custom-user", + Roles: []string{"testRole", "viewer"}, + }) + // END AssignOidcUserRole + if err != nil { + t.Fatal(err) + } +} + +// TestRBACRevokeOidcUserRoles revokes roles from an OIDC user. +func TestRBACRevokeOidcUserRoles(t *testing.T) { + ctx := context.Background() + client := connectRBACAdmin(t) + defer client.Close() + seedRole(t, client, "testRole") + // Assign the role first so the snippet has something to revoke. + if err := client.Users.OIDC.AssignRoles(ctx, rbac.AssignRolesOptions{ + ID: "custom-user", + Roles: []string{"testRole"}, + }); err != nil { + t.Fatalf("seed OIDC role assignment: %v", err) + } + defer deleteRoleIfExists(client, "testRole") + + // START RevokeOidcUserRoles + err := client.Users.OIDC.RevokeRoles(ctx, rbac.RevokeRolesOptions{ + ID: "custom-user", + Roles: []string{"testRole"}, + }) + // END RevokeOidcUserRoles + if err != nil { + t.Fatal(err) + } +} + +// TestRBACListOidcUserRoles lists the roles assigned to an OIDC user. +func TestRBACListOidcUserRoles(t *testing.T) { + ctx := context.Background() + client := connectRBACAdmin(t) + defer client.Close() + + // START ListOidcUserRoles + roles, err := client.Users.OIDC.AssignedRoles(ctx, rbac.AssignedRolesOptions{ + ID: "custom-user", + }) + if err != nil { + t.Fatal(err) + } + for _, role := range roles { + t.Logf("%s", role.ID) + } + // END ListOidcUserRoles +} + +// ----------------------------------------------------------------------------- +// OIDC groups +// ----------------------------------------------------------------------------- + +// TestRBACAssignOidcGroupRoles assigns roles to an OIDC group. +func TestRBACAssignOidcGroupRoles(t *testing.T) { + ctx := context.Background() + client := connectRBACAdmin(t) + defer client.Close() + seedRole(t, client, "testRole") // "viewer" is a built-in role. + defer deleteRoleIfExists(client, "testRole") + defer func() { + _ = client.Groups.RevokeRoles(ctx, rbac.RevokeRolesOptions{ + ID: "/admin-group", Roles: []string{"testRole", "viewer"}, + }) + }() + + // START AssignOidcGroupRoles + err := client.Groups.AssignRoles(ctx, rbac.AssignRolesOptions{ + ID: "/admin-group", + Roles: []string{"testRole", "viewer"}, + }) + // END AssignOidcGroupRoles + if err != nil { + t.Fatal(err) + } +} + +// TestRBACRevokeOidcGroupRoles revokes roles from an OIDC group. +func TestRBACRevokeOidcGroupRoles(t *testing.T) { + ctx := context.Background() + client := connectRBACAdmin(t) + defer client.Close() + seedRole(t, client, "testRole") + // Assign the role first so the snippet has something to revoke. + if err := client.Groups.AssignRoles(ctx, rbac.AssignRolesOptions{ + ID: "/admin-group", + Roles: []string{"testRole"}, + }); err != nil { + t.Fatalf("seed group role assignment: %v", err) + } + defer deleteRoleIfExists(client, "testRole") + + // START RevokeOidcGroupRoles + err := client.Groups.RevokeRoles(ctx, rbac.RevokeRolesOptions{ + ID: "/admin-group", + Roles: []string{"testRole"}, + }) + // END RevokeOidcGroupRoles + if err != nil { + t.Fatal(err) + } +} + +// TestRBACGetOidcGroupRoles lists the roles assigned to an OIDC group. +func TestRBACGetOidcGroupRoles(t *testing.T) { + ctx := context.Background() + client := connectRBACAdmin(t) + defer client.Close() + seedRole(t, client, "testRole") + if err := client.Groups.AssignRoles(ctx, rbac.AssignRolesOptions{ + ID: "/admin-group", + Roles: []string{"testRole"}, + }); err != nil { + t.Fatalf("seed group role assignment: %v", err) + } + defer deleteRoleIfExists(client, "testRole") + defer func() { + _ = client.Groups.RevokeRoles(ctx, rbac.RevokeRolesOptions{ + ID: "/admin-group", Roles: []string{"testRole"}, + }) + }() + + // START GetOidcGroupRoles + roles, err := client.Groups.AssignedRoles(ctx, rbac.AssignedRolesOptions{ + ID: "/admin-group", + IncludePermissions: true, + }) + if err != nil { + t.Fatal(err) + } + for _, role := range roles { + t.Logf("%s", role.ID) + } + // END GetOidcGroupRoles +} + +// TestRBACGetKnownOidcGroups is a placeholder: the v6 Go client cannot yet list +// all known OIDC groups. +func TestRBACGetKnownOidcGroups(t *testing.T) { + t.Skip("listing all known OIDC groups is not yet available in the v6 Go client") + + // TODO[g-despot]: list-known-OIDC-groups snippet pending v6 client support + // START GetKnownOidcGroups + // Coming soon + // END GetKnownOidcGroups +} + +// TestRBACGetGroupAssignments lists the groups assigned to a role. +func TestRBACGetGroupAssignments(t *testing.T) { + ctx := context.Background() + client := connectRBACAdmin(t) + defer client.Close() + seedRole(t, client, "testRole") + defer deleteRoleIfExists(client, "testRole") + + // START GetGroupAssignments + groups, err := client.Roles.GroupAssignments(ctx, "testRole") + if err != nil { + t.Fatal(err) + } + for _, g := range groups { + t.Logf("group ID: %s, type: %s", g.ID, g.Type) + } + // END GetGroupAssignments +} diff --git a/_includes/code/go-v6/search_basic_test.go b/_includes/code/go-v6/search_basic_test.go new file mode 100644 index 00000000..e2466b36 --- /dev/null +++ b/_includes/code/go-v6/search_basic_test.go @@ -0,0 +1,268 @@ +package main + +import ( + "context" + "testing" + + "github.com/google/uuid" + "github.com/weaviate/weaviate-go-client/v6/collections" + "github.com/weaviate/weaviate-go-client/v6/data" + "github.com/weaviate/weaviate-go-client/v6/query" +) + +func TestBasicQuery(t *testing.T) { + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + setupArticle(t, client) + defer client.Collections.Delete(ctx, "Article") + + articles := client.Collections.Use("Article") + // Fixed, non-leading-zero id keeps the query deterministic (a server-assigned + // 0x00-leading id flakes the gRPC reply; see filterByIdSeedUUID in main_test.go). + id := uuid.MustParse("d1e2f3a4-b5c6-4d7e-8f9a-3b4c5d6e7f8a") + if _, err := articles.Data.Insert(ctx, &data.Object{ + UUID: &id, + Properties: map[string]any{"title": "Hello", "body": "World"}, + }); err != nil { + t.Fatal(err) + } + + // START BasicQuery + response, err := articles.Query.OverAll(ctx, query.OverAll{ + Limit: 2, + }) + // END BasicQuery + if err != nil { + t.Fatal(err) + } + for _, obj := range response.Objects { + t.Logf("%v", obj.Properties) + } +} + +// TestBasicGet lists objects without any search parameters. +func TestBasicGet(t *testing.T) { + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + setupJeopardySearch(t, client) + defer client.Collections.Delete(ctx, "JeopardyQuestion") + + // START BasicGet + jeopardy := client.Collections.Use("JeopardyQuestion") + response, err := jeopardy.Query.OverAll(ctx, query.OverAll{}) + if err != nil { + t.Fatal(err) + } + for _, obj := range response.Objects { + t.Logf("%v", obj.Properties) + } + // END BasicGet +} + +// TestGetWithLimit caps the number of returned objects. +func TestGetWithLimit(t *testing.T) { + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + setupJeopardySearch(t, client) + defer client.Collections.Delete(ctx, "JeopardyQuestion") + + // START GetWithLimit + jeopardy := client.Collections.Use("JeopardyQuestion") + response, err := jeopardy.Query.OverAll(ctx, query.OverAll{ + Limit: 1, + }) + if err != nil { + t.Fatal(err) + } + for _, obj := range response.Objects { + t.Logf("%v", obj.Properties) + } + // END GetWithLimit +} + +// TestGetWithOffset paginates with limit and offset. +func TestGetWithOffset(t *testing.T) { + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + setupJeopardySearch(t, client) + defer client.Collections.Delete(ctx, "JeopardyQuestion") + + // START GetWithOffset + jeopardy := client.Collections.Use("JeopardyQuestion") + response, err := jeopardy.Query.OverAll(ctx, query.OverAll{ + Limit: 1, + Offset: 1, + }) + if err != nil { + t.Fatal(err) + } + for _, obj := range response.Objects { + t.Logf("%v", obj.Properties) + } + // END GetWithOffset +} + +// TestGetProperties returns a subset of object properties. +func TestGetProperties(t *testing.T) { + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + setupJeopardySearch(t, client) + defer client.Collections.Delete(ctx, "JeopardyQuestion") + + // START GetProperties + jeopardy := client.Collections.Use("JeopardyQuestion") + response, err := jeopardy.Query.OverAll(ctx, query.OverAll{ + Limit: 1, + ReturnProperties: []string{"question", "answer", "points"}, + }) + if err != nil { + t.Fatal(err) + } + for _, obj := range response.Objects { + t.Logf("%v", obj.Properties) + } + // END GetProperties +} + +// TestGetObjectVector returns the object vector alongside the results. +func TestGetObjectVector(t *testing.T) { + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + setupJeopardySearch(t, client) + defer client.Collections.Delete(ctx, "JeopardyQuestion") + + // START GetObjectVector + jeopardy := client.Collections.Use("JeopardyQuestion") + response, err := jeopardy.Query.OverAll(ctx, query.OverAll{ + Limit: 1, + // Name the vectors to return; use "default" for a single unnamed vector. + ReturnVectors: []string{"default"}, + }) + if err != nil { + t.Fatal(err) + } + for _, obj := range response.Objects { + t.Logf("%v", obj.Vectors["default"].Single) + } + // END GetObjectVector +} + +// TestGetObjectId reads the object id (uuid) from the results. +func TestGetObjectId(t *testing.T) { + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + setupJeopardySearch(t, client) + defer client.Collections.Delete(ctx, "JeopardyQuestion") + + // START GetObjectId + jeopardy := client.Collections.Use("JeopardyQuestion") + response, err := jeopardy.Query.OverAll(ctx, query.OverAll{ + Limit: 1, + }) + if err != nil { + t.Fatal(err) + } + for _, obj := range response.Objects { + // The object id is always returned. + t.Logf("%v", obj.UUID) + } + // END GetObjectId +} + +// TestGetWithCrossRefs returns properties from cross-referenced objects. +func TestGetWithCrossRefs(t *testing.T) { + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + setupJeopardyDemo(t, client) + defer cleanupJeopardyDemo(ctx, client) + + // START GetWithCrossRefs + jeopardy := client.Collections.Use("JeopardyQuestion") + response, err := jeopardy.Query.OverAll(ctx, query.OverAll{ + Limit: 2, + ReturnReferences: []query.Reference{ + { + PropertyName: "hasCategory", + TargetCollection: "JeopardyCategory", + ReturnProperties: []string{"title"}, + }, + }, + }) + if err != nil { + t.Fatal(err) + } + for _, obj := range response.Objects { + t.Logf("%v", obj.Properties) + } + // END GetWithCrossRefs +} + +// TestGetWithMetadata returns object metadata such as the creation timestamp. +func TestGetWithMetadata(t *testing.T) { + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + setupJeopardySearch(t, client) + defer client.Collections.Delete(ctx, "JeopardyQuestion") + + // START GetWithMetadata + jeopardy := client.Collections.Use("JeopardyQuestion") + response, err := jeopardy.Query.OverAll(ctx, query.OverAll{ + Limit: 1, + ReturnMetadata: query.ReturnMetadata{ + CreatedAt: true, + }, + }) + if err != nil { + t.Fatal(err) + } + for _, obj := range response.Objects { + if obj.CreatedAt != nil { + t.Logf("created at: %v", *obj.CreatedAt) + } + } + // END GetWithMetadata +} + +// TestMultiTenancy queries a specific tenant of a multi-tenant collection. +func TestMultiTenancy(t *testing.T) { + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + setupMultiTenancyJeopardy(t, client) + defer client.Collections.Delete(ctx, "JeopardyQuestion") + + // START MultiTenancy + // Bind the tenant once when you take the collection handle. + jeopardy := client.Collections.Use("JeopardyQuestion", + collections.WithTenant("tenantA"), + ) + response, err := jeopardy.Query.OverAll(ctx, query.OverAll{ + Limit: 2, + }) + if err != nil { + t.Fatal(err) + } + for _, obj := range response.Objects { + t.Logf("%v", obj.Properties) + } + // END MultiTenancy +} diff --git a/_includes/code/go-v6/search_placeholders_test.go b/_includes/code/go-v6/search_placeholders_test.go new file mode 100644 index 00000000..297b5cce --- /dev/null +++ b/_includes/code/go-v6/search_placeholders_test.go @@ -0,0 +1,63 @@ +package main + +import "testing" + +// This file holds placeholders for search and aggregate operations that the v6 +// Go client does not implement yet. Each marked region contains only a +// "Coming soon" line so the docs render a clear placeholder in the Go v6 tab; +// the surrounding test compiles and skips. + +// TestGetNearObject is a placeholder: the v6 Go client cannot yet search by an +// existing object id (near-object). +func TestGetNearObject(t *testing.T) { + t.Skip("near-object search is not yet available in the v6 Go client") + + // TODO[g-despot]: near-object search snippet pending v6 client support + // START NearObject + // Coming soon + // END NearObject +} + +// TestFilterByGeolocation is a placeholder: the v6 Go client does not yet +// expose a geo-coordinate range filter operator. +func TestFilterByGeolocation(t *testing.T) { + t.Skip("geo-coordinate filtering is not yet available in the v6 Go client") + + // TODO[g-despot]: geo-coordinate filter snippet pending v6 client support + // START GeoFilter + // Coming soon + // END GeoFilter +} + +// TestAggregateNearText is a placeholder: the v6 Go client cannot yet aggregate +// over the results of a near-text search. +func TestAggregateNearText(t *testing.T) { + t.Skip("aggregation over a near-text search is not yet available in the v6 Go client") + + // TODO[g-despot]: near-text aggregation snippet pending v6 client support + // START AggregateNearText + // Coming soon + // END AggregateNearText +} + +// TestAggregateHybrid is a placeholder: the v6 Go client cannot yet aggregate +// over the results of a hybrid search. +func TestAggregateHybrid(t *testing.T) { + t.Skip("aggregation over a hybrid search is not yet available in the v6 Go client") + + // TODO[g-despot]: hybrid aggregation snippet pending v6 client support + // START AggregateHybrid + // Coming soon + // END AggregateHybrid +} + +// TestAggregateWhereFilter is a placeholder: the v6 Go client cannot yet +// aggregate objects selected by a standalone filter. +func TestAggregateWhereFilter(t *testing.T) { + t.Skip("filtered aggregation is not yet available in the v6 Go client") + + // TODO[g-despot]: filtered aggregation snippet pending v6 client support + // START AggregateWhereFilter + // Coming soon + // END AggregateWhereFilter +} diff --git a/_includes/code/go-v6/search_test.go b/_includes/code/go-v6/search_test.go new file mode 100644 index 00000000..6cd81cad --- /dev/null +++ b/_includes/code/go-v6/search_test.go @@ -0,0 +1,317 @@ +package main + +import ( + "context" + "testing" + + "github.com/google/uuid" + weaviate "github.com/weaviate/weaviate-go-client/v6" + "github.com/weaviate/weaviate-go-client/v6/collections" + "github.com/weaviate/weaviate-go-client/v6/data" + "github.com/weaviate/weaviate-go-client/v6/modules/selfprovided" + "github.com/weaviate/weaviate-go-client/v6/query" + "github.com/weaviate/weaviate-go-client/v6/query/filter" + "github.com/weaviate/weaviate-go-client/v6/types" +) + +// setupJeopardySearch (re)creates a JeopardyQuestion collection with a single +// user-supplied ("bring your own") vector named "default" and seeds a few +// objects with explicit vectors. Because the collection has no vectorizer, the +// vector-search snippets that build on it run without an inference module. +// +// The near-text and hybrid snippets on this page instead query a collection +// that has a text vectorizer configured, mirroring the demo datasets used +// throughout the docs; they are not backed by this helper. +func setupJeopardySearch(t *testing.T, client *weaviate.Client) { + t.Helper() + ctx := context.Background() + _ = client.Collections.Delete(ctx, "JeopardyQuestion") + if _, err := client.Collections.Create(ctx, collections.Collection{ + Name: "JeopardyQuestion", + Properties: []collections.Property{ + {Name: "question", DataType: collections.DataTypeText}, + {Name: "answer", DataType: collections.DataTypeText}, + {Name: "category", DataType: collections.DataTypeText}, + {Name: "points", DataType: collections.DataTypeInt}, + }, + // A named vector that brings its own vectors: the server requires every + // vector config to name a vectorizer, so use the "none" vectorizer + // (selfprovided) and supply the vectors explicitly at insert time. + Vectors: map[string]collections.VectorConfig{ + "default": {Vectorizer: selfprovided.Vectorizer}, + }, + }); err != nil { + t.Fatalf("create JeopardyQuestion collection: %v", err) + } + + jeopardy := client.Collections.Use("JeopardyQuestion") + // Fixed, non-leading-zero ids keep the run deterministic. A server-assigned id + // beginning 0x00 comes back from gRPC search as 15 bytes (the id_as_bytes reply + // field drops leading zero bytes) and the client's uuid.FromBytes then rejects + // the whole SearchReply, flaking every query over this collection. See the + // filterByIdSeedUUID note in main_test.go. + s1 := uuid.MustParse("5a6b7c8d-9e0f-4a1b-8c2d-1a2b3c4d5e6f") + s2 := uuid.MustParse("6b7c8d9e-0f1a-4b2c-8d3e-2b3c4d5e6f7a") + s3 := uuid.MustParse("7c8d9e0f-1a2b-4c3d-8e4f-3c4d5e6f7a8b") + if _, err := jeopardy.Data.Insert(ctx, + &data.Object{ + UUID: &s1, + Properties: map[string]any{"question": "This organ removes excess glucose from the blood & stores it as glycogen", "answer": "Liver", "category": "SCIENCE", "points": 100}, + Vectors: []types.Vector{{Name: "default", Single: []float32{0.10, 0.21, 0.32}}}, + }, + &data.Object{ + UUID: &s2, + Properties: map[string]any{"question": "It's the only living mammal in the order Proboscidea", "answer": "Elephant", "category": "ANIMALS", "points": 200}, + Vectors: []types.Vector{{Name: "default", Single: []float32{0.11, 0.20, 0.34}}}, + }, + &data.Object{ + UUID: &s3, + Properties: map[string]any{"question": "The gavial looks very much like a crocodile except for this bodily feature", "answer": "the nose or snout", "category": "ANIMALS", "points": 400}, + Vectors: []types.Vector{{Name: "default", Single: []float32{0.14, 0.19, 0.30}}}, + }, + ); err != nil { + t.Fatalf("seed JeopardyQuestion: %v", err) + } + waitForCount(t, jeopardy, 3) +} + +// TestGetNearText runs a semantic search over a collection whose vectorizer +// turns the query text into a vector server-side. +func TestGetNearText(t *testing.T) { + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + setupJeopardyVectorized(t, client) + defer client.Collections.Delete(ctx, "JeopardyQuestion") + + // START GetNearText + jeopardy := client.Collections.Use("JeopardyQuestion") + response, err := jeopardy.Query.NearText(ctx, query.NearText{ + Concepts: []string{"animals in movies"}, + Limit: 2, + ReturnMetadata: query.ReturnMetadata{ + Distance: true, + }, + }) + if err != nil { + t.Fatal(err) + } + for _, obj := range response.Objects { + t.Logf("%v", obj.Properties) + if obj.Metadata.Distance != nil { + t.Logf("distance: %v", *obj.Metadata.Distance) + } + } + // END GetNearText +} + +// TestGetNearVector runs a vector similarity search from an input vector. +func TestGetNearVector(t *testing.T) { + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + setupJeopardySearch(t, client) + defer client.Collections.Delete(ctx, "JeopardyQuestion") + + // START GetNearVector + // A query vector, for example an embedding produced by your model. + vector := []float32{0.12, 0.20, 0.33} + + jeopardy := client.Collections.Use("JeopardyQuestion") + response, err := jeopardy.Query.NearVector(ctx, query.NearVector{ + Target: &types.Vector{Name: "default", Single: vector}, + Limit: 2, + ReturnMetadata: query.ReturnMetadata{ + Distance: true, + }, + }) + if err != nil { + t.Fatal(err) + } + for _, obj := range response.Objects { + t.Logf("%v", obj.Properties) + if obj.Metadata.Distance != nil { + t.Logf("distance: %v", *obj.Metadata.Distance) + } + } + // END GetNearVector +} + +// TestNamedVectorNearText searches a named vector by passing the vector name as +// the search target. +func TestNamedVectorNearText(t *testing.T) { + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + setupWineReviewNV(t, client) + defer client.Collections.Delete(ctx, "WineReviewNV") + + // START NamedVectorNearText + reviews := client.Collections.Use("WineReviewNV") + response, err := reviews.Query.NearText(ctx, query.NearText{ + Concepts: []string{"a sweet German white wine"}, + // Select the named vector to search against. + Target: query.VectorName("title_country"), + Limit: 2, + ReturnMetadata: query.ReturnMetadata{ + Distance: true, + }, + }) + if err != nil { + t.Fatal(err) + } + for _, obj := range response.Objects { + t.Logf("%v", obj.Properties) + } + // END NamedVectorNearText +} + +// TestGetWithDistance sets a maximum distance threshold on a vector search. +func TestGetWithDistance(t *testing.T) { + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + setupJeopardySearch(t, client) + defer client.Collections.Delete(ctx, "JeopardyQuestion") + + // START GetWithDistance + vector := []float32{0.12, 0.20, 0.33} + + jeopardy := client.Collections.Use("JeopardyQuestion") + response, err := jeopardy.Query.NearVector(ctx, query.NearVector{ + Target: &types.Vector{Name: "default", Single: vector}, + Similarity: query.Distance(0.25), + ReturnMetadata: query.ReturnMetadata{ + Distance: true, + }, + }) + if err != nil { + t.Fatal(err) + } + for _, obj := range response.Objects { + t.Logf("%v", obj.Properties) + } + // END GetWithDistance +} + +// TestGetLimitOffset paginates a vector search with limit and offset. +func TestGetLimitOffset(t *testing.T) { + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + setupJeopardySearch(t, client) + defer client.Collections.Delete(ctx, "JeopardyQuestion") + + // START GetLimitOffset + vector := []float32{0.12, 0.20, 0.33} + + jeopardy := client.Collections.Use("JeopardyQuestion") + response, err := jeopardy.Query.NearVector(ctx, query.NearVector{ + Target: &types.Vector{Name: "default", Single: vector}, + Limit: 2, + Offset: 1, + }) + if err != nil { + t.Fatal(err) + } + for _, obj := range response.Objects { + t.Logf("%v", obj.Properties) + } + // END GetLimitOffset +} + +// TestAutocut limits results to the first N distance clusters (autocut). +func TestAutocut(t *testing.T) { + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + setupJeopardySearch(t, client) + defer client.Collections.Delete(ctx, "JeopardyQuestion") + + // START Autocut + vector := []float32{0.12, 0.20, 0.33} + + jeopardy := client.Collections.Use("JeopardyQuestion") + response, err := jeopardy.Query.NearVector(ctx, query.NearVector{ + Target: &types.Vector{Name: "default", Single: vector}, + // Return objects from the first similarity cluster only. + AutoLimit: 1, + }) + if err != nil { + t.Fatal(err) + } + for _, obj := range response.Objects { + t.Logf("%v", obj.Properties) + } + // END Autocut +} + +// TestGetWithGroupBy groups the results of a vector search by a property. +func TestGetWithGroupBy(t *testing.T) { + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + setupJeopardySearch(t, client) + defer client.Collections.Delete(ctx, "JeopardyQuestion") + + // START GetWithGroupBy + vector := []float32{0.12, 0.20, 0.33} + + jeopardy := client.Collections.Use("JeopardyQuestion") + response, err := jeopardy.Query.NearVector.GroupBy(ctx, + query.NearVector{ + Target: &types.Vector{Name: "default", Single: vector}, + Limit: 10, + }, + query.GroupBy{ + Property: "category", + NumberOfGroups: 2, + ObjectLimit: 2, + }, + ) + if err != nil { + t.Fatal(err) + } + for name, group := range response.Groups { + t.Logf("group %q holds %d objects", name, group.Size) + } + // END GetWithGroupBy +} + +// TestGetWithFilter narrows a vector search with a property filter. +func TestGetWithFilter(t *testing.T) { + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + setupJeopardySearch(t, client) + defer client.Collections.Delete(ctx, "JeopardyQuestion") + + // START GetWithFilter + vector := []float32{0.12, 0.20, 0.33} + + jeopardy := client.Collections.Use("JeopardyQuestion") + response, err := jeopardy.Query.NearVector(ctx, query.NearVector{ + Target: &types.Vector{Name: "default", Single: vector}, + Limit: 2, + Filter: &filter.Cond{ + Target: "category", + Operator: filter.Equal, + Value: "ANIMALS", + }, + }) + if err != nil { + t.Fatal(err) + } + for _, obj := range response.Objects { + t.Logf("%v", obj.Properties) + } + // END GetWithFilter +} diff --git a/_includes/code/go-v6/tenants_test.go b/_includes/code/go-v6/tenants_test.go new file mode 100644 index 00000000..17f45f87 --- /dev/null +++ b/_includes/code/go-v6/tenants_test.go @@ -0,0 +1,226 @@ +package main + +import ( + "context" + "testing" + + "github.com/weaviate/weaviate-go-client/v6/collections" + "github.com/weaviate/weaviate-go-client/v6/data" + "github.com/weaviate/weaviate-go-client/v6/query" + "github.com/weaviate/weaviate-go-client/v6/tenant" +) + +// The multi-tenancy snippets below run against a live server. They are kept out +// of the CI run set (compile-only) and skip when executed directly. + +// TestEnableMultiTenancy creates a collection with multi-tenancy turned on. +func TestEnableMultiTenancy(t *testing.T) { + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + _ = client.Collections.Delete(ctx, "MultiTenancyCollection") + + // START EnableMultiTenancy + _, err := client.Collections.Create(ctx, collections.Collection{ + Name: "MultiTenancyCollection", + MultiTenancy: &collections.MultiTenancyConfig{ + Enabled: true, + }, + }) + // END EnableMultiTenancy + if err != nil { + t.Fatal(err) + } +} + +// TestEnableAutoMT creates a multi-tenancy collection that also creates and +// activates tenants automatically when data is inserted for an unknown tenant. +func TestEnableAutoMT(t *testing.T) { + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + _ = client.Collections.Delete(ctx, "MultiTenancyCollection") + + // START EnableAutoMT + _, err := client.Collections.Create(ctx, collections.Collection{ + Name: "MultiTenancyCollection", + MultiTenancy: &collections.MultiTenancyConfig{ + Enabled: true, + AutoTenantCreation: true, + AutoTenantActivation: true, + }, + }) + // END EnableAutoMT + if err != nil { + t.Fatal(err) + } +} + +// TestUpdateAutoMT is a placeholder: the v6 Go client cannot yet update an +// existing collection's configuration, so auto-tenant settings can only be set +// at creation time. +func TestUpdateAutoMT(t *testing.T) { + t.Skip("updating a collection's configuration is not yet available in the v6 Go client") + + // TODO[g-despot]: update-collection (auto-tenant) snippet pending v6 client support + // START UpdateAutoMT + // Coming soon + // END UpdateAutoMT +} + +// TestAddTenantsToClass adds tenants to a multi-tenancy collection. +func TestAddTenantsToClass(t *testing.T) { + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + // A fresh collection with no tenants; the snippet creates them. + createMultiTenancyCollection(t, client) + defer cleanupMultiTenancy(ctx, client) + + // START AddTenantsToClass + collection := client.Collections.Use("MultiTenancyCollection") + err := collection.Tenants.Create(ctx, + tenant.Tenant{Name: "tenantA"}, // Active by default. + tenant.Tenant{Name: "tenantB", Status: tenant.Inactive}, // Created on disk, not loaded. + ) + // END AddTenantsToClass + if err != nil { + t.Fatal(err) + } +} + +// TestListTenants lists every tenant in a multi-tenancy collection. +func TestListTenants(t *testing.T) { + t.Skip("go-client v6 Tenants.Get panics โ€” *api.GetTenantsRequest is not wired into the gRPC transport MessageMarshaler switch (internal/api/transport/transport.go), so Tenants.Get(ctx) hits the default dev.Assert(false) and panics, aborting the whole test binary; deferred pending a client fix") + + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + setupMultiTenancy(t, client) + defer cleanupMultiTenancy(ctx, client) + + // START ListTenants + collection := client.Collections.Use("MultiTenancyCollection") + // Passing no tenant names returns every tenant in the collection. + tenants, err := collection.Tenants.Get(ctx) + if err != nil { + t.Fatal(err) + } + for _, tn := range tenants { + t.Logf("%s: %s", tn.Name, tn.Status) + } + // END ListTenants +} + +// TestRemoveTenants deletes tenants (and their data) from a collection. +func TestRemoveTenants(t *testing.T) { + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + setupMultiTenancy(t, client) + defer cleanupMultiTenancy(ctx, client) + + // START RemoveTenants + collection := client.Collections.Use("MultiTenancyCollection") + // Unknown tenant names are ignored. + err := collection.Tenants.Delete(ctx, "tenantB", "tenantX") + // END RemoveTenants + if err != nil { + t.Fatal(err) + } +} + +// TestCreateMtObject inserts an object into a specific tenant. The tenant is +// bound once on the collection handle and applies to every operation made with +// it. +func TestCreateMtObject(t *testing.T) { + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + setupMultiTenancy(t, client) + defer cleanupMultiTenancy(ctx, client) + + // START CreateMtObject + // Bind the tenant to the collection handle. + collection := client.Collections.Use("MultiTenancyCollection", + collections.WithTenant("tenantA"), + ) + _, err := collection.Data.Insert(ctx, &data.Object{ + Properties: map[string]any{ + "question": "This vector DB is OSS and supports automatic property type inference on import", + }, + }) + // END CreateMtObject + if err != nil { + t.Fatal(err) + } +} + +// TestMtSearch runs a query scoped to a single tenant. +func TestMtSearch(t *testing.T) { + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + setupMultiTenancy(t, client) + defer cleanupMultiTenancy(ctx, client) + + // START Search + collection := client.Collections.Use("MultiTenancyCollection", + collections.WithTenant("tenantA"), + ) + response, err := collection.Query.OverAll(ctx, query.OverAll{ + Limit: 2, + }) + if err != nil { + t.Fatal(err) + } + for _, obj := range response.Objects { + t.Logf("%v", obj.Properties) + } + // END Search +} + +// TestMtAddCrossRef adds a cross-reference from an object that belongs to a +// tenant. The tenant is bound on the handle used to make the request. +func TestMtAddCrossRef(t *testing.T) { + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + setupMultiTenancy(t, client) + defer cleanupMultiTenancy(ctx, client) + + // Reference a seeded MultiTenancyCollection question (in tenantA) as the + // source and a seeded JeopardyCategory row as the target. + sourceID := mtSourceID + targetID := mtCategoryID + + // START AddCrossRef + collection := client.Collections.Use("MultiTenancyCollection", + collections.WithTenant("tenantA"), + ) + res, err := collection.Data.AddReferences(ctx, data.Reference{ + Origin: data.ObjectPath{ + Collection: "MultiTenancyCollection", + Property: "hasCategory", + UUID: sourceID, + }, + UUID: targetID, + }) + if err != nil { + t.Fatal(err) + } + // END AddCrossRef + for ref, msg := range res.Errors { + if msg != "" { + t.Fatalf("add reference %v: %s", ref, msg) + } + } +} diff --git a/_includes/code/go-v6/vector_config_test.go b/_includes/code/go-v6/vector_config_test.go new file mode 100644 index 00000000..89541847 --- /dev/null +++ b/_includes/code/go-v6/vector_config_test.go @@ -0,0 +1,228 @@ +package main + +import ( + "context" + "testing" + + "github.com/weaviate/weaviate-go-client/v6/collections" + "github.com/weaviate/weaviate-go-client/v6/collections/compression" + "github.com/weaviate/weaviate-go-client/v6/collections/vectorindex" + "github.com/weaviate/weaviate-go-client/v6/modules/model2vec" + "github.com/weaviate/weaviate-go-client/v6/modules/selfprovided" +) + +// The vectorizer and vector-index snippets below run against a live server. +// They are kept out of the CI run set (compile-only) and skip when executed +// directly. Named vectors are the default model: each entry of the Vectors map +// carries its own index, compression, and vectorizer. + +// TestCreateCollectionWithVectorizer configures a vectorizer that generates an +// embedding for each object. +func TestCreateCollectionWithVectorizer(t *testing.T) { + t.Skip("requires a running Weaviate instance") + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + _ = client.Collections.Delete(ctx, "Article") + defer client.Collections.Delete(ctx, "Article") + + // START CreateCollectionWithVectorizer + _, err := client.Collections.Create(ctx, collections.Collection{ + Name: "Article", + Properties: []collections.Property{ + {Name: "title", DataType: collections.DataTypeText}, + {Name: "body", DataType: collections.DataTypeText}, + }, + Vectors: map[string]collections.VectorConfig{ + "default": {Vectorizer: model2vec.Text2Vec{}}, + }, + }) + // END CreateCollectionWithVectorizer + if err != nil { + t.Fatal(err) + } +} + +// TestVectorizerSettings configures the vectorizer, such as which inference +// service it calls and which properties it embeds. +func TestVectorizerSettings(t *testing.T) { + t.Skip("requires a running Weaviate instance") + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + _ = client.Collections.Delete(ctx, "Article") + defer client.Collections.Delete(ctx, "Article") + + // START VectorizerSettings + // Point the vectorizer at a remote inference service and embed only the + // listed properties. + vectorizer := model2vec.Text2Vec{ + URL: "http://text2vec-model2vec:8080", + Properties: []string{"title"}, + } + _, err := client.Collections.Create(ctx, collections.Collection{ + Name: "Article", + Properties: []collections.Property{ + {Name: "title", DataType: collections.DataTypeText}, + {Name: "body", DataType: collections.DataTypeText}, + }, + Vectors: map[string]collections.VectorConfig{ + "default": {Vectorizer: vectorizer}, + }, + }) + // END VectorizerSettings + if err != nil { + t.Fatal(err) + } +} + +// TestCreateCollectionWithNamedVectors defines multiple named vectors, each +// with its own configuration. +func TestCreateCollectionWithNamedVectors(t *testing.T) { + t.Skip("requires a running Weaviate instance") + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + _ = client.Collections.Delete(ctx, "Article") + defer client.Collections.Delete(ctx, "Article") + + // START CreateCollectionWithNamedVectors + _, err := client.Collections.Create(ctx, collections.Collection{ + Name: "Article", + Properties: []collections.Property{ + {Name: "title", DataType: collections.DataTypeText}, + {Name: "body", DataType: collections.DataTypeText}, + }, + Vectors: map[string]collections.VectorConfig{ + // A vector generated from the title only. + "title": {Vectorizer: model2vec.Text2Vec{Properties: []string{"title"}}}, + // A vector you supply yourself at import time. + "custom": {Vectorizer: selfprovided.Vectorizer}, + }, + }) + // END CreateCollectionWithNamedVectors + if err != nil { + t.Fatal(err) + } +} + +// TestSetVectorIndexType selects the vector index type for a named vector. +func TestSetVectorIndexType(t *testing.T) { + t.Skip("requires a running Weaviate instance") + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + _ = client.Collections.Delete(ctx, "Article") + defer client.Collections.Delete(ctx, "Article") + + // START SetVectorIndexType + _, err := client.Collections.Create(ctx, collections.Collection{ + Name: "Article", + Vectors: map[string]collections.VectorConfig{ + "default": {Index: vectorindex.HFresh{}, Vectorizer: model2vec.Text2Vec{}}, + }, + }) + // END SetVectorIndexType + if err != nil { + t.Fatal(err) + } +} + +// TestSetVectorIndexParams tunes the vector index and enables vector +// compression for a named vector. +func TestSetVectorIndexParams(t *testing.T) { + t.Skip("requires a running Weaviate instance") + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + _ = client.Collections.Delete(ctx, "Article") + defer client.Collections.Delete(ctx, "Article") + + // START SetVectorIndexParams + vectorConfig := collections.VectorConfig{ + Index: vectorindex.HFresh{ + Distance: vectorindex.DistanceCosine, + MaxPostingSizeKB: 1024, + ReplicaCount: 3, + SearchProbe: 64, + }, + Compression: compression.RQ{ + Bits: 8, + RescoreLimit: 20, + Cache: true, + }, + Vectorizer: model2vec.Text2Vec{}, + } + _, err := client.Collections.Create(ctx, collections.Collection{ + Name: "Article", + Vectors: map[string]collections.VectorConfig{"default": vectorConfig}, + }) + // END SetVectorIndexParams + if err != nil { + t.Fatal(err) + } +} + +// TestPropModuleSettings sets property-level options, such as tokenization, +// and controls which properties the vectorizer embeds. +func TestPropModuleSettings(t *testing.T) { + t.Skip("requires a running Weaviate instance") + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + _ = client.Collections.Delete(ctx, "Article") + defer client.Collections.Delete(ctx, "Article") + + // START PropModuleSettings + _, err := client.Collections.Create(ctx, collections.Collection{ + Name: "Article", + Properties: []collections.Property{ + {Name: "title", DataType: collections.DataTypeText, Tokenization: collections.TokenizationWord}, + {Name: "chunk", DataType: collections.DataTypeText, Tokenization: collections.TokenizationWhitespace}, + }, + Vectors: map[string]collections.VectorConfig{ + // Embed the title only; chunk is left out of the vector. + "default": {Vectorizer: model2vec.Text2Vec{Properties: []string{"title"}}}, + }, + }) + // END PropModuleSettings + if err != nil { + t.Fatal(err) + } +} + +// TestDistanceMetric sets the distance metric for a collection that stores +// user-supplied vectors. +func TestDistanceMetric(t *testing.T) { + // Runs against the docs-CI server (Weaviate 1.38; the WEAVIATE_VERSION "1.35.0" + // workflow var is stale and unused โ€” the compose files pin 1.38.0), which supports + // the HFresh vector index. HFresh requires maxPostingSizeKB >= 8, so the snippet + // sets it explicitly; otherwise the server rejects the create with HTTP 422 + // "invalid hfresh config: maxPostingSizeKB is '0' but must be at least 8". + ctx := context.Background() + client := connectLocal(t) + defer client.Close() + + _ = client.Collections.Delete(ctx, "Article") + defer client.Collections.Delete(ctx, "Article") + + // START DistanceMetric + // Bring your own vectors and set the distance metric on the index. + index := vectorindex.HFresh{Distance: vectorindex.DistanceCosine, MaxPostingSizeKB: 8} + _, err := client.Collections.Create(ctx, collections.Collection{ + Name: "Article", + Vectors: map[string]collections.VectorConfig{ + "default": {Index: index, Vectorizer: selfprovided.Vectorizer}, + }, + }) + // END DistanceMetric + if err != nil { + t.Fatal(err) + } +} diff --git a/docs/deploy/configuration/backups.md b/docs/deploy/configuration/backups.md index de8ac6b1..98ac5d66 100644 --- a/docs/deploy/configuration/backups.md +++ b/docs/deploy/configuration/backups.md @@ -14,6 +14,7 @@ import TSCodeBackup from '!!raw-loader!/_includes/code/howto/configure.backups.b import TSCodeRestore from '!!raw-loader!/_includes/code/howto/configure.backups.restore.ts'; import TSCodeStatus from '!!raw-loader!/_includes/code/howto/configure.backups.status.ts'; import GoCode from '!!raw-loader!/_includes/code/howto/go/docs/deploy/backups_test.go'; +import BackupGoV6Code from '!!raw-loader!/_includes/code/go-v6/backup_test.go'; import JavaCode from '!!raw-loader!/_includes/code/java-v6/src/test/java/BackupsTest.java'; import CurlCode from '!!raw-loader!/_includes/code/howto/configure.backups.sh'; @@ -80,6 +81,14 @@ Restart Weaviate to apply the new configuration. Then, you are ready to start a language="go" /> + + + @@ -349,6 +358,14 @@ The `*` character matches any sequence of characters. For example, `Article*` ma language="go" /> + + + + + + + + + This operation is particularly useful if you have started a backup by accident, or if you would like to stop a backup that is taking too long. @@ -608,6 +641,14 @@ Versions prior to `v1.23.13` had a bug that could lead to data not being stored language="go" /> + + + + + + + + + :::tip Further resources @@ -628,6 +637,14 @@ This code example shows how to configure multiple named vectors through a client language="gonew" /> + + + :::tip Further resources diff --git a/docs/weaviate/config-refs/indexing/inverted-index.mdx b/docs/weaviate/config-refs/indexing/inverted-index.mdx index a9da3d3a..e5b20ff7 100644 --- a/docs/weaviate/config-refs/indexing/inverted-index.mdx +++ b/docs/weaviate/config-refs/indexing/inverted-index.mdx @@ -10,6 +10,7 @@ import FilteredTextBlock from "@site/src/components/Documentation/FilteredTextBl import PyCode from "!!raw-loader!/_includes/code/config-refs/reference.collections.py"; import TSCode from "!!raw-loader!/_includes/code/howto/manage-data.collections.ts"; import GoCode from "!!raw-loader!/_includes/code/howto/go/docs/manage-data.classes_test.go"; +import GoV6CollectionConfigCode from "!!raw-loader!/_includes/code/go-v6/collection_config_test.go"; import PyTokenizeEndpoint from "!!raw-loader!/_includes/code/tutorials/tokenization/tokenize_endpoint.py"; The **[inverted index](../../concepts/indexing/inverted-index.md)** maps values (like words or numbers) to the objects that contain them. It is the backbone for all attribute-based filtering (`where` filters) and keyword searching (`bm25`, `hybrid`). @@ -74,6 +75,14 @@ This code example shows how to configure inverted index parameters through a cli language="gonew" /> + + + --- diff --git a/docs/weaviate/configuration/compression/rq-compression.md b/docs/weaviate/configuration/compression/rq-compression.md index b8841b25..7ff5204a 100644 --- a/docs/weaviate/configuration/compression/rq-compression.md +++ b/docs/weaviate/configuration/compression/rq-compression.md @@ -11,6 +11,7 @@ import Rq1bit from '/_includes/feature-notes/rq-1bit.mdx'; import FilteredTextBlock from '@site/src/components/Documentation/FilteredTextBlock'; import PyCode from '!!raw-loader!/\_includes/code/howto/configure-rq/rq-compression-v4.py'; import GoCode from '!!raw-loader!/\_includes/code/howto/go/docs/configure/compression.rq_test.go'; +import GoV6CompressionCode from '!!raw-loader!/\_includes/code/go-v6/compression_test.go'; import TSCode from '!!raw-loader!/\_includes/code/howto/configure-rq/rq-compression-v3.ts'; import JavaCode from '!!raw-loader!/\_includes/code/java-v6/src/test/java/ConfigureRQTest.java'; import CSharpCode from "!!raw-loader!/\_includes/code/csharp/ConfigureRQTest.cs"; @@ -59,6 +60,14 @@ RQ can be enabled at collection creation time through the collection definition: language="go" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -83,7 +84,16 @@ import HostnameWarning from "/_includes/wcs/hostname-warning.mdx"; text={GoCode} startMarker="// START APIKeyWCD" endMarker="// END APIKeyWCD" - language="py" + language="go" + /> + + + + @@ -139,7 +149,15 @@ If you use API-based models for vectorization or RAG, you must provide an API ke text={GoCode} startMarker="// START ThirdPartyAPIKeys" endMarker="// END ThirdPartyAPIKeys" - language="py" + language="go" + /> + + + diff --git a/docs/weaviate/connections/connect-local.mdx b/docs/weaviate/connections/connect-local.mdx index 9f67c071..26158184 100644 --- a/docs/weaviate/connections/connect-local.mdx +++ b/docs/weaviate/connections/connect-local.mdx @@ -20,6 +20,7 @@ import JavaV6Code from "!!raw-loader!/_includes/code/java-v6/src/test/java/Conne import CSharpCode from "!!raw-loader!/_includes/code/csharp/ConnectionTest.cs"; import ShellCode from "!!raw-loader!/_includes/code/connections/connect.sh"; import GoCode from "!!raw-loader!/_includes/code/connections/connect.go"; +import GoV6Code from "!!raw-loader!/_includes/code/go-v6/connect_test.go"; Follow these steps to connect to a locally hosted Weaviate instance. @@ -58,6 +59,14 @@ To connect to a local instance without authentication, follow these examples. language="go" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + `${baseUrl}#${ref}`, + icon: '/img/site/logo-go.svg', +}; DOC_SYSTEMS.javaraw = DOC_SYSTEMS.java; DOC_SYSTEMS.csharpraw = DOC_SYSTEMS.csharp; diff --git a/src/theme/Tabs/index.js b/src/theme/Tabs/index.js index c08057a8..d383ff88 100644 --- a/src/theme/Tabs/index.js +++ b/src/theme/Tabs/index.js @@ -27,6 +27,7 @@ const LANGUAGE_CONFIG = { icon: "/img/site/logo-ts.svg", }, go: { label: "Go", icon: "/img/site/logo-go.svg" }, + go6: { label: "Go v6", icon: "/img/site/logo-go.svg" }, java: { label: "Java", icon: "/img/site/logo-java.svg" }, csharp: { label: "C#", icon: "/img/site/logo-csharp.svg" }, curl: { label: "Curl", icon: "/img/site/logo-curl.svg" }, @@ -204,6 +205,13 @@ const CodeDropdownTabs = ({ return "ts"; } + if (targetLang === "go6" && availableLangs.includes("go")) { + return "go"; + } + if (targetLang === "go" && availableLangs.includes("go6")) { + return "go6"; + } + return targetLang; } @@ -275,6 +283,10 @@ const CodeDropdownTabs = ({ availableLangs.includes("ts") ) { valueToSet = "ts"; + } else if (newGlobalLang === "go6" && availableLangs.includes("go")) { + valueToSet = "go"; + } else if (newGlobalLang === "go" && availableLangs.includes("go6")) { + valueToSet = "go6"; } } diff --git a/tests/test_go.py b/tests/test_go.py new file mode 100644 index 00000000..2ba4b206 --- /dev/null +++ b/tests/test_go.py @@ -0,0 +1,182 @@ +import glob +import json +import os +import re +import shlex +import subprocess + +import pytest + + +# Absolute path to the Go v6 snippet module, derived from this file so both the +# import-time source scan and the runtime `go test` subprocess resolve it the +# same way regardless of pytest's working directory. +_HERE = os.path.dirname(os.path.abspath(__file__)) +GO_V6_CWD = os.path.normpath(os.path.join(_HERE, "..", "_includes", "code", "go-v6")) + + +# --------------------------------------------------------------------------- # +# Name discovery (collection time) +# --------------------------------------------------------------------------- # +# One pytest case is collected per Go test function so CI reports the true +# pass/skip/fail breakdown (regression visibility) instead of one opaque case. +# +# Names are discovered by statically scanning the *_test.go sources rather than +# invoking the Go toolchain (`go test -list`). test_go.py is imported by EVERY +# pytest job (java, python, ...) โ€” which then deselect by marker โ€” so the +# parametrize argument must stay cheap and side-effect-free: a `go test -list` +# here would spawn the compiler (and fail on a not-yet-wired client) in jobs +# that have nothing to do with Go. The suite uses no subtests (no `t.Run`), so +# each top-level `func TestXxx(t *testing.T)` maps 1:1 to a `go test -json` +# result, making the source scan exact. +_TEST_FUNC_RE = re.compile(r"^func (Test\w*)\s*\(\s*\w+\s+\*testing\.T\s*\)") + + +def discover_test_names(): + names = [] + for path in sorted(glob.glob(os.path.join(GO_V6_CWD, "*_test.go"))): + try: + with open(path, encoding="utf-8") as fh: + for line in fh: + match = _TEST_FUNC_RE.match(line) + if match: + names.append(match.group(1)) + except OSError: + continue + return sorted(set(names)) + + +# --------------------------------------------------------------------------- # +# `go test -json` parsing +# --------------------------------------------------------------------------- # +_SKIP_MSG_RE = re.compile(r"^\s+[\w./-]+\.go:\d+:\s?(.*)$") + + +def parse_go_json(stdout): + """Aggregate `go test -json` events into a per-top-level-test dict. + + Returns ``(results, build_output)`` where ``results`` maps each top-level + test name to ``{"outcome": "pass"|"fail"|"skip"|None, "output": str}`` and + ``build_output`` is the concatenated package-level output. Compiler errors + live in ``build_output`` because a failed build emits events with no ``Test`` + field (plus, on some toolchains, plain non-JSON lines) โ€” capturing them is + what lets a build failure surface loudly instead of silently yielding zero + results. + """ + results = {} + build_output = [] + for line in stdout.splitlines(): + line = line.strip() + if not line: + continue + try: + event = json.loads(line) + except json.JSONDecodeError: + # Not a JSON event: some Go toolchains print build errors as plain + # text. Keep it so `run_go_suite` can report it. + build_output.append(line + "\n") + continue + action = event.get("Action") + test = event.get("Test") + if not test: + # Package-level event (start/pass/fail/output for the whole build). + if action == "output": + build_output.append(event.get("Output", "")) + continue + # Roll subtests (TestFoo/case) up under their parent so reporting stays + # at the test-function granularity even if subtests are added later. + top = test.split("/", 1)[0] + entry = results.setdefault(top, {"outcome": None, "output": []}) + if action == "output": + entry["output"].append(event.get("Output", "")) + elif action in ("pass", "fail", "skip") and test == top: + # Only the parent's terminal event decides the reported outcome; + # go already folds a failing subtest into the parent's "fail". + entry["outcome"] = action + for entry in results.values(): + entry["output"] = "".join(entry["output"]) + return results, "".join(build_output) + + +def _skip_reason(output): + # A t.Skip("...") prints as a ` file.go:NN: ` line in the test's + # captured output; pull those out so the pytest SKIP shows the real reason. + reasons = [m.group(1) for m in (_SKIP_MSG_RE.match(l) for l in output.splitlines()) if m] + if reasons: + return "\n".join(reasons) + return "skipped (no reason captured)" + + +class GoBuildError(Exception): + """Raised when `go test` produced no per-test results (build/compile failure).""" + + +def _results_or_raise(returncode, stdout, stderr): + results, build_output = parse_go_json(stdout) + if not results: + # No per-test events at all => the suite never ran. Fail loudly with the + # compiler output so the break is diagnosable, not an opaque "0 tests". + detail = [ + f"Go suite produced no test results (go test exit code {returncode}); " + "the suite likely failed to build." + ] + build_tail = build_output.splitlines()[-200:] + if build_tail: + detail.append("\n--- BUILD OUTPUT ---\n" + "\n".join(build_tail)) + if stderr: + detail.append("\n--- STDERR ---\n" + "\n".join(stderr.splitlines()[-200:])) + raise GoBuildError("\n".join(detail)) + return results + + +def run_go_suite(): + # Single invocation for the whole suite. -count=1 disables the test cache so + # each run actually hits Weaviate; -json emits one event per test action so + # we can report each test individually. Selection is by t.Skip (not a -run + # pattern): every test without a t.Skip executes, every test with one is + # reported as skipped with its reason. + command = shlex.split("go test ./... -json -count=1") + proc = subprocess.run( + command, cwd=GO_V6_CWD, env=dict(os.environ), + capture_output=True, text=True, + ) + return _results_or_raise(proc.returncode, proc.stdout, proc.stderr) + + +# --------------------------------------------------------------------------- # +# Fixture + parametrized reporting +# --------------------------------------------------------------------------- # +@pytest.fixture(scope="session") +def go_suite_results(empty_weaviates): + """Run the Go v6 snippet suite ONCE and return its parsed per-test results. + + Session-scoped so the ~150 parametrized cases below share a single + `go test` invocation. Depends on empty_weaviates so the docker stack is up + (a no-op in CI, where the workflow manages the containers). + """ + return run_go_suite() + + +# Run the whole Go v6 snippet suite live against the local docker stack on +# :8080 / :50051; tests self-seed via helpers like setupArticle / setupJeopardy. +# Each Go test function is reported as its own pytest case (regression +# visibility): a pass passes, a t.Skip becomes a pytest SKIP carrying its +# reason, and a failure fails the case with that test's own captured output. +@pytest.mark.go +@pytest.mark.parametrize("test_name", discover_test_names()) +def test_go_v6(go_suite_results, test_name): + result = go_suite_results.get(test_name) + if result is None: + pytest.fail( + f"{test_name}: no result captured from `go test -json`. The test was " + "discovered in the source but did not run โ€” the suite may have failed " + "to build, or the function name drifted." + ) + outcome = result["outcome"] + output = result["output"] + if outcome == "pass": + return + if outcome == "skip": + pytest.skip(_skip_reason(output)) + # "fail" or a missing terminal event (e.g. a panic mid-test) => failure. + pytest.fail(output or f"{test_name} failed with no captured output.")