From 0fcb0c30665566b3a2dabc7e8773d615cf005ecd Mon Sep 17 00:00:00 2001 From: Austin DeNoble Date: Fri, 14 Nov 2025 15:26:19 -0500 Subject: [PATCH 01/18] implement basic upsert functionality under the pc index upsert command --- internal/pkg/cli/command/index/cmd.go | 1 + internal/pkg/cli/command/index/upsert.go | 164 +++++++++++++++++++++++ 2 files changed, 165 insertions(+) create mode 100644 internal/pkg/cli/command/index/upsert.go diff --git a/internal/pkg/cli/command/index/cmd.go b/internal/pkg/cli/command/index/cmd.go index 284eb3c..283b2c6 100644 --- a/internal/pkg/cli/command/index/cmd.go +++ b/internal/pkg/cli/command/index/cmd.go @@ -39,6 +39,7 @@ func NewIndexCmd() *cobra.Command { cmd.AddCommand(NewCreatePodCmd()) cmd.AddCommand(NewConfigureIndexCmd()) cmd.AddCommand(NewDeleteCmd()) + cmd.AddCommand(NewUpsertCmd()) return cmd } diff --git a/internal/pkg/cli/command/index/upsert.go b/internal/pkg/cli/command/index/upsert.go new file mode 100644 index 0000000..75502a4 --- /dev/null +++ b/internal/pkg/cli/command/index/upsert.go @@ -0,0 +1,164 @@ +package index + +import ( + "encoding/json" + "os" + + "github.com/pinecone-io/cli/internal/pkg/utils/exit" + "github.com/pinecone-io/cli/internal/pkg/utils/help" + "github.com/pinecone-io/cli/internal/pkg/utils/msg" + "github.com/pinecone-io/cli/internal/pkg/utils/pcio" + "github.com/pinecone-io/cli/internal/pkg/utils/sdk" + "github.com/pinecone-io/cli/internal/pkg/utils/style" + "github.com/pinecone-io/cli/internal/pkg/utils/text" + "github.com/spf13/cobra" + + "github.com/pinecone-io/go-pinecone/v5/pinecone" +) + +type upsertCmdOptions struct { + file string + name string + namespace string + json bool +} + +type upsertFile struct { + Vectors []upsertVector `json:"vectors"` + Namespace string `json:"namespace"` +} + +type upsertVector struct { + ID string `json:"id"` + Values []float32 `json:"values"` + SparseValues *sparseValues `json:"sparse_values,omitempty"` + Metadata map[string]any `json:"metadata,omitempty"` +} + +type sparseValues struct { + Indices []uint32 `json:"indices"` + Values []float32 `json:"values"` +} + +func NewUpsertCmd() *cobra.Command { + options := upsertCmdOptions{} + + cmd := &cobra.Command{ + Use: "upsert [file]", + Short: "Upsert vectors into an index from a JSON file", + Example: help.Examples(` + pc index upsert --name my-index --namespace my-namespace ./vectors.json + `), + Run: func(cmd *cobra.Command, args []string) { + runUpsertCmd(cmd, options) + }, + } + + cmd.Flags().StringVarP(&options.name, "name", "n", "", "name of index to upsert into") + cmd.Flags().StringVar(&options.namespace, "namespace", "", "namespace to upsert into") + cmd.Flags().StringVarP(&options.file, "file", "f", "", "file to upsert from") + cmd.Flags().BoolVar(&options.json, "json", false, "output as JSON") + _ = cmd.MarkFlagRequired("name") + _ = cmd.MarkFlagRequired("file") + + return cmd +} + +func runUpsertCmd(cmd *cobra.Command, options upsertCmdOptions) { + filePath := options.file + raw, err := os.ReadFile(filePath) + if err != nil { + msg.FailMsg("Failed to read file %s: %s", style.Emphasis(filePath), err) + exit.Error().Err(err).Msgf("Failed to read file %s", filePath) + } + + var payload upsertFile + if err := json.Unmarshal(raw, &payload); err != nil { + msg.FailMsg("Failed to parse JSON from %s: %s", style.Emphasis(filePath), err) + exit.Error().Err(err).Msg("Failed to parse JSON for upsert") + } + + // Default namespace + ns := payload.Namespace + if options.namespace != "" { + ns = options.namespace + } + // Default if no namespace provided + if ns == "" { + ns = "__default__" + } + + if len(payload.Vectors) == 0 { + msg.FailMsg("No vectors found in %s", style.Emphasis(filePath)) + exit.Error().Msg("No vectors provided for upsert") + } + + // Map to SDK types + mapped := make([]*pinecone.Vector, 0, len(payload.Vectors)) + for _, v := range payload.Vectors { + values := v.Values + metadata, err := pinecone.NewMetadata(v.Metadata) + if err != nil { + msg.FailMsg("Failed to parse metadata: %s", err) + exit.Error().Err(err).Msg("Failed to parse metadata") + } + + var vector pinecone.Vector + vector.Id = v.ID + if v.Values != nil { + vector.Values = &values + } + if v.SparseValues != nil { + vector.SparseValues = &pinecone.SparseValues{ + Indices: v.SparseValues.Indices, + Values: v.SparseValues.Values, + } + } + vector.Metadata = metadata + mapped = append(mapped, &vector) + } + + // Get Pinecone client + pc := sdk.NewPineconeClient() + // TODO - Refactor this into an all-in-one function in sdk package + // Get index and establish IndexConnection + index, err := pc.DescribeIndex(cmd.Context(), options.name) + if err != nil { + msg.FailMsg("Failed to describe index %s: %s", style.Emphasis(options.name), err) + exit.Error().Err(err).Msg("Failed to describe index") + } + + ic, err := pc.Index(pinecone.NewIndexConnParams{ + Host: index.Host, + }) + if err != nil { + msg.FailMsg("Failed to create index connection: %s", err) + exit.Error().Err(err).Msg("Failed to create index connection") + } + // TODO - Isolate all of this ^^^^^^^^^^^^^^^^ + + batchSize := 1000 + batches := make([][]*pinecone.Vector, 0, (len(mapped)+batchSize-1)/batchSize) + for i := 0; i < len(mapped); i += batchSize { + end := i + batchSize + if end > len(mapped) { + end = len(mapped) + } + batches = append(batches, mapped[i:end]) + } + + for i, batch := range batches { + resp, err := ic.UpsertVectors(cmd.Context(), batch) + if err != nil { + msg.FailMsg("Failed to upsert %d vectors in batch %d: %s", len(batch), i+1, err) + exit.Error().Err(err).Msgf("Failed to upsert %d vectors in batch %d", len(batch), i+1) + } else { + if options.json { + json := text.IndentJSON(resp) + pcio.Println(json) + } else { + msg.SuccessMsg("Upserted %d vectors into namespace %s in %d batches", len(batch), ns, i+1) + } + } + } +} From acb1b878deb427cc6a99f9cb60a7187c42c37368 Mon Sep 17 00:00:00 2001 From: Austin DeNoble Date: Sun, 16 Nov 2025 15:24:15 -0500 Subject: [PATCH 02/18] update off rebase, add basic index fetch command --- internal/pkg/cli/command/index/cmd.go | 1 + internal/pkg/cli/command/index/fetch.go | 88 +++++++++++++++++++ internal/pkg/cli/command/index/upsert.go | 14 +-- .../pkg/utils/presenters/fetch_vectors.go | 57 ++++++++++++ internal/pkg/utils/presenters/managed_keys.go | 1 - 5 files changed, 153 insertions(+), 8 deletions(-) create mode 100644 internal/pkg/cli/command/index/fetch.go create mode 100644 internal/pkg/utils/presenters/fetch_vectors.go delete mode 100644 internal/pkg/utils/presenters/managed_keys.go diff --git a/internal/pkg/cli/command/index/cmd.go b/internal/pkg/cli/command/index/cmd.go index 283b2c6..8b8d10c 100644 --- a/internal/pkg/cli/command/index/cmd.go +++ b/internal/pkg/cli/command/index/cmd.go @@ -40,6 +40,7 @@ func NewIndexCmd() *cobra.Command { cmd.AddCommand(NewConfigureIndexCmd()) cmd.AddCommand(NewDeleteCmd()) cmd.AddCommand(NewUpsertCmd()) + cmd.AddCommand(NewFetchCmd()) return cmd } diff --git a/internal/pkg/cli/command/index/fetch.go b/internal/pkg/cli/command/index/fetch.go new file mode 100644 index 0000000..34bcc74 --- /dev/null +++ b/internal/pkg/cli/command/index/fetch.go @@ -0,0 +1,88 @@ +package index + +import ( + "context" + + "github.com/pinecone-io/cli/internal/pkg/utils/exit" + "github.com/pinecone-io/cli/internal/pkg/utils/help" + "github.com/pinecone-io/cli/internal/pkg/utils/msg" + "github.com/pinecone-io/cli/internal/pkg/utils/pcio" + "github.com/pinecone-io/cli/internal/pkg/utils/presenters" + "github.com/pinecone-io/cli/internal/pkg/utils/sdk" + "github.com/pinecone-io/cli/internal/pkg/utils/style" + "github.com/pinecone-io/cli/internal/pkg/utils/text" + "github.com/pinecone-io/go-pinecone/v5/pinecone" + "github.com/spf13/cobra" +) + +type fetchCmdOptions struct { + name string + namespace string + ids []string + json bool +} + +func NewFetchCmd() *cobra.Command { + options := fetchCmdOptions{} + cmd := &cobra.Command{ + Use: "fetch", + Short: "Fetch vectors by ID from an index", + Example: help.Examples(` + pc index fetch --name my-index --ids 123, 456, 789 + `), + Run: func(cmd *cobra.Command, args []string) { + runFetchCmd(cmd.Context(), options) + }, + } + + cmd.Flags().StringSliceVarP(&options.ids, "ids", "i", []string{}, "IDs of vectors to fetch") + cmd.Flags().StringVarP(&options.name, "name", "n", "", "name of the index to fetch from") + cmd.Flags().StringVar(&options.namespace, "namespace", "", "namespace to fetch from") + cmd.Flags().BoolVarP(&options.json, "json", "j", false, "output as JSON") + _ = cmd.MarkFlagRequired("name") + _ = cmd.MarkFlagRequired("ids") + + return cmd +} + +func runFetchCmd(ctx context.Context, options fetchCmdOptions) { + pc := sdk.NewPineconeClient() + + index, err := pc.DescribeIndex(ctx, options.name) + if err != nil { + msg.FailMsg("Failed to describe index %s: %s", style.Emphasis(options.name), err) + exit.Error(err, "Failed to describe index") + } + + ic, err := pc.Index(pinecone.NewIndexConnParams{ + Host: index.Host, + }) + if err != nil { + msg.FailMsg("Failed to create index connection: %s", err) + exit.Error(err, "Failed to create index connection") + } + + // Default namespace + ns := options.namespace + if options.namespace != "" { + ns = options.namespace + } + if ns == "" { + ns = "__default__" + } + if ns != ic.Namespace() { + ic = ic.WithNamespace(ns) + } + + vectors, err := ic.FetchVectors(ctx, options.ids) + if err != nil { + exit.Error(err, "Failed to fetch vectors") + } + + if options.json { + json := text.IndentJSON(vectors) + pcio.Println(json) + } else { + presenters.PrintFetchVectorsTable(vectors) + } +} diff --git a/internal/pkg/cli/command/index/upsert.go b/internal/pkg/cli/command/index/upsert.go index 75502a4..78605c7 100644 --- a/internal/pkg/cli/command/index/upsert.go +++ b/internal/pkg/cli/command/index/upsert.go @@ -69,13 +69,13 @@ func runUpsertCmd(cmd *cobra.Command, options upsertCmdOptions) { raw, err := os.ReadFile(filePath) if err != nil { msg.FailMsg("Failed to read file %s: %s", style.Emphasis(filePath), err) - exit.Error().Err(err).Msgf("Failed to read file %s", filePath) + exit.Errorf(err, "Failed to read file %s", filePath) } var payload upsertFile if err := json.Unmarshal(raw, &payload); err != nil { msg.FailMsg("Failed to parse JSON from %s: %s", style.Emphasis(filePath), err) - exit.Error().Err(err).Msg("Failed to parse JSON for upsert") + exit.Error(err, "Failed to parse JSON for upsert") } // Default namespace @@ -90,7 +90,7 @@ func runUpsertCmd(cmd *cobra.Command, options upsertCmdOptions) { if len(payload.Vectors) == 0 { msg.FailMsg("No vectors found in %s", style.Emphasis(filePath)) - exit.Error().Msg("No vectors provided for upsert") + exit.ErrorMsg("No vectors provided for upsert") } // Map to SDK types @@ -100,7 +100,7 @@ func runUpsertCmd(cmd *cobra.Command, options upsertCmdOptions) { metadata, err := pinecone.NewMetadata(v.Metadata) if err != nil { msg.FailMsg("Failed to parse metadata: %s", err) - exit.Error().Err(err).Msg("Failed to parse metadata") + exit.Error(err, "Failed to parse metadata") } var vector pinecone.Vector @@ -125,7 +125,7 @@ func runUpsertCmd(cmd *cobra.Command, options upsertCmdOptions) { index, err := pc.DescribeIndex(cmd.Context(), options.name) if err != nil { msg.FailMsg("Failed to describe index %s: %s", style.Emphasis(options.name), err) - exit.Error().Err(err).Msg("Failed to describe index") + exit.Error(err, "Failed to describe index") } ic, err := pc.Index(pinecone.NewIndexConnParams{ @@ -133,7 +133,7 @@ func runUpsertCmd(cmd *cobra.Command, options upsertCmdOptions) { }) if err != nil { msg.FailMsg("Failed to create index connection: %s", err) - exit.Error().Err(err).Msg("Failed to create index connection") + exit.Error(err, "Failed to create index connection") } // TODO - Isolate all of this ^^^^^^^^^^^^^^^^ @@ -151,7 +151,7 @@ func runUpsertCmd(cmd *cobra.Command, options upsertCmdOptions) { resp, err := ic.UpsertVectors(cmd.Context(), batch) if err != nil { msg.FailMsg("Failed to upsert %d vectors in batch %d: %s", len(batch), i+1, err) - exit.Error().Err(err).Msgf("Failed to upsert %d vectors in batch %d", len(batch), i+1) + exit.Errorf(err, "Failed to upsert %d vectors in batch %d", len(batch), i+1) } else { if options.json { json := text.IndentJSON(resp) diff --git a/internal/pkg/utils/presenters/fetch_vectors.go b/internal/pkg/utils/presenters/fetch_vectors.go new file mode 100644 index 0000000..ce2bee9 --- /dev/null +++ b/internal/pkg/utils/presenters/fetch_vectors.go @@ -0,0 +1,57 @@ +package presenters + +import ( + "strings" + + "github.com/pinecone-io/cli/internal/pkg/utils/pcio" + "github.com/pinecone-io/cli/internal/pkg/utils/text" + "github.com/pinecone-io/go-pinecone/v5/pinecone" +) + +func PrintFetchVectorsTable(resp *pinecone.FetchVectorsResponse) { + writer := NewTabWriter() + + // Header Block + if resp.Namespace != "" { + pcio.Fprintf(writer, "Namespace: %s\n", resp.Namespace) + } + if resp.Usage != nil { + pcio.Fprintf(writer, "Usage: %d (read units)\n", resp.Usage.ReadUnits) + } + + // Table Header + columns := []string{"ID", "DIMENSION", "VALUES", "SPARSE VALUES", "METADATA"} + pcio.Fprintln(writer, strings.Join(columns, "\t")) + + // Rows + for id, vector := range resp.Vectors { + dim := 0 + if vector.Values != nil { + dim = len(*vector.Values) + } + sparseDim := 0 + if vector.SparseValues != nil { + sparseDim = len(vector.SparseValues.Values) + } + metadata := "" + if vector.Metadata != nil { + metadata = text.InlineJSON(vector.Metadata) + } + preview := previewSlice(vector.Values, 3) + row := []string{id, pcio.Sprintf("%d", dim), preview, pcio.Sprintf("%d", sparseDim), metadata} + pcio.Fprintln(writer, strings.Join(row, "\t")) + } + + writer.Flush() +} + +func previewSlice(values *[]float32, limit int) string { + if values == nil || len(*values) == 0 { + return "" + } + vals := *values + if len(vals) > limit { + vals = vals[:limit] + } + return text.InlineJSON(vals) + "..." +} diff --git a/internal/pkg/utils/presenters/managed_keys.go b/internal/pkg/utils/presenters/managed_keys.go deleted file mode 100644 index 0cc9e34..0000000 --- a/internal/pkg/utils/presenters/managed_keys.go +++ /dev/null @@ -1 +0,0 @@ -package presenters From 3cd9f760170ba7f22c025b393032690349771eb1 Mon Sep 17 00:00:00 2001 From: Austin DeNoble Date: Sun, 16 Nov 2025 17:44:14 -0500 Subject: [PATCH 03/18] encapsulate logic for establishing and IndexConnection with a name and namespace --- internal/pkg/cli/command/index/fetch.go | 23 +++++--------------- internal/pkg/cli/command/index/upsert.go | 27 ++++++------------------ internal/pkg/utils/sdk/client.go | 16 ++++++++++++++ 3 files changed, 28 insertions(+), 38 deletions(-) diff --git a/internal/pkg/cli/command/index/fetch.go b/internal/pkg/cli/command/index/fetch.go index 34bcc74..aaa2dbd 100644 --- a/internal/pkg/cli/command/index/fetch.go +++ b/internal/pkg/cli/command/index/fetch.go @@ -9,9 +9,7 @@ import ( "github.com/pinecone-io/cli/internal/pkg/utils/pcio" "github.com/pinecone-io/cli/internal/pkg/utils/presenters" "github.com/pinecone-io/cli/internal/pkg/utils/sdk" - "github.com/pinecone-io/cli/internal/pkg/utils/style" "github.com/pinecone-io/cli/internal/pkg/utils/text" - "github.com/pinecone-io/go-pinecone/v5/pinecone" "github.com/spf13/cobra" ) @@ -48,20 +46,6 @@ func NewFetchCmd() *cobra.Command { func runFetchCmd(ctx context.Context, options fetchCmdOptions) { pc := sdk.NewPineconeClient() - index, err := pc.DescribeIndex(ctx, options.name) - if err != nil { - msg.FailMsg("Failed to describe index %s: %s", style.Emphasis(options.name), err) - exit.Error(err, "Failed to describe index") - } - - ic, err := pc.Index(pinecone.NewIndexConnParams{ - Host: index.Host, - }) - if err != nil { - msg.FailMsg("Failed to create index connection: %s", err) - exit.Error(err, "Failed to create index connection") - } - // Default namespace ns := options.namespace if options.namespace != "" { @@ -70,8 +54,11 @@ func runFetchCmd(ctx context.Context, options fetchCmdOptions) { if ns == "" { ns = "__default__" } - if ns != ic.Namespace() { - ic = ic.WithNamespace(ns) + + ic, err := sdk.NewIndexConnection(ctx, pc, options.name, ns) + if err != nil { + msg.FailMsg("Failed to create index connection: %s", err) + exit.Error(err, "Failed to create index connection") } vectors, err := ic.FetchVectors(ctx, options.ids) diff --git a/internal/pkg/cli/command/index/upsert.go b/internal/pkg/cli/command/index/upsert.go index 78605c7..02f67bd 100644 --- a/internal/pkg/cli/command/index/upsert.go +++ b/internal/pkg/cli/command/index/upsert.go @@ -83,10 +83,16 @@ func runUpsertCmd(cmd *cobra.Command, options upsertCmdOptions) { if options.namespace != "" { ns = options.namespace } - // Default if no namespace provided if ns == "" { ns = "__default__" } + // Get IndexConnection + pc := sdk.NewPineconeClient() + ic, err := sdk.NewIndexConnection(cmd.Context(), pc, options.name, ns) + if err != nil { + msg.FailMsg("Failed to create index connection: %s", err) + exit.Error(err, "Failed to create index connection") + } if len(payload.Vectors) == 0 { msg.FailMsg("No vectors found in %s", style.Emphasis(filePath)) @@ -118,25 +124,6 @@ func runUpsertCmd(cmd *cobra.Command, options upsertCmdOptions) { mapped = append(mapped, &vector) } - // Get Pinecone client - pc := sdk.NewPineconeClient() - // TODO - Refactor this into an all-in-one function in sdk package - // Get index and establish IndexConnection - index, err := pc.DescribeIndex(cmd.Context(), options.name) - if err != nil { - msg.FailMsg("Failed to describe index %s: %s", style.Emphasis(options.name), err) - exit.Error(err, "Failed to describe index") - } - - ic, err := pc.Index(pinecone.NewIndexConnParams{ - Host: index.Host, - }) - if err != nil { - msg.FailMsg("Failed to create index connection: %s", err) - exit.Error(err, "Failed to create index connection") - } - // TODO - Isolate all of this ^^^^^^^^^^^^^^^^ - batchSize := 1000 batches := make([][]*pinecone.Vector, 0, (len(mapped)+batchSize-1)/batchSize) for i := 0; i < len(mapped); i += batchSize { diff --git a/internal/pkg/utils/sdk/client.go b/internal/pkg/utils/sdk/client.go index 1acf11e..3050be6 100644 --- a/internal/pkg/utils/sdk/client.go +++ b/internal/pkg/utils/sdk/client.go @@ -153,6 +153,22 @@ func NewPineconeAdminClient() *pinecone.AdminClient { return ac } +func NewIndexConnection(ctx context.Context, pc *pinecone.Client, indexName string, namespace string) (*pinecone.IndexConnection, error) { + index, err := pc.DescribeIndex(ctx, indexName) + if err != nil { + return nil, pcio.Errorf("failed to describe index: %w", err) + } + + ic, err := pc.Index(pinecone.NewIndexConnParams{ + Host: index.Host, + Namespace: namespace, + }) + if err != nil { + return nil, pcio.Errorf("failed to create index connection: %w", err) + } + return ic, nil +} + func getCLIAPIKeyForProject(ctx context.Context, ac *pinecone.AdminClient, project *pinecone.Project) (string, error) { projectAPIKeysMap := secrets.ManagedAPIKeys.Get() var managedKey secrets.ManagedKey From 723dd6c01f86496fef92042bb3e1af323a9ac3f7 Mon Sep 17 00:00:00 2001 From: Austin DeNoble Date: Mon, 17 Nov 2025 17:56:54 -0500 Subject: [PATCH 04/18] Implement `sdk.NewIndexConnection`, clean up `context.Context` passing (#55) ## Problem There are a number of data plane features that need to be implemented in the CLI: index upsert and ingestion, query, fetch, list vectors, delete vectors, etc. In order to work with these resources via CLI, we need a consistent way of establishing an `IndexConnection` using index name and namespace. We're also not threading `context.Context` through the cobra command tree properly, which is important for properly timing out actions and network requests. Currently, we're passing a lot of `context.Background()` directly rather than using the `cmd.Context()` option for shared context. ## Solution Add `NewIndexConnection` to the `sdk` package to allow establishing a connection to an index by `pinecone.Client`, index `name`, and `namespace`. This encapsulates the logic for describing the index to grab the host, and then initializing an `IndexConnection`. Update `root.go` to add an explicit root parent `context.Context` to `Execute`. Use `signal.NotifyContext` to allow interrupt and termination signals to properly cancel commands. Add a global `--timeout` flag to allow users to control the overall timeout per command. Set the default `timeout=60s` for now. ## Type of Change - [ ] Bug fix (non-breaking change which fixes an issue) - [X] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [ ] This change requires a documentation update - [ ] Infrastructure change (CI configs, etc) - [ ] Non-code change (docs, etc) - [ ] None of the above: (explain here) ## Test Plan CI - unit & integration tests Existing operations should continue working as expected. If you want to test passing `--timeout` it can be passed to any command using a duration format: `10s`, `1h`, `2m`, etc. --- internal/pkg/cli/command/apiKey/create.go | 3 +- internal/pkg/cli/command/apiKey/delete.go | 3 +- internal/pkg/cli/command/apiKey/describe.go | 3 +- internal/pkg/cli/command/apiKey/list.go | 3 +- internal/pkg/cli/command/apiKey/update.go | 3 +- internal/pkg/cli/command/auth/configure.go | 2 +- .../pkg/cli/command/auth/local_keys_prune.go | 2 +- internal/pkg/cli/command/collection/create.go | 6 +-- internal/pkg/cli/command/collection/delete.go | 6 +-- .../pkg/cli/command/collection/describe.go | 6 +-- internal/pkg/cli/command/collection/list.go | 5 +-- internal/pkg/cli/command/index/configure.go | 7 ++-- internal/pkg/cli/command/index/create.go | 7 ++-- internal/pkg/cli/command/index/create_pod.go | 7 ++-- .../cli/command/index/create_serverless.go | 7 ++-- internal/pkg/cli/command/index/delete.go | 5 +-- internal/pkg/cli/command/index/describe.go | 3 +- internal/pkg/cli/command/index/list.go | 5 +-- .../pkg/cli/command/organization/delete.go | 3 +- .../pkg/cli/command/organization/describe.go | 3 +- internal/pkg/cli/command/organization/list.go | 3 +- .../pkg/cli/command/organization/update.go | 3 +- internal/pkg/cli/command/project/create.go | 7 ++-- internal/pkg/cli/command/project/delete.go | 18 ++++---- internal/pkg/cli/command/project/describe.go | 7 ++-- internal/pkg/cli/command/project/list.go | 5 +-- internal/pkg/cli/command/project/update.go | 7 ++-- internal/pkg/cli/command/root/root.go | 41 +++++++++++++++++-- internal/pkg/cli/command/target/target.go | 11 ++--- internal/pkg/utils/login/login.go | 7 ++-- internal/pkg/utils/sdk/client.go | 29 +++++++++---- 31 files changed, 132 insertions(+), 95 deletions(-) diff --git a/internal/pkg/cli/command/apiKey/create.go b/internal/pkg/cli/command/apiKey/create.go index f7e84fe..d614870 100644 --- a/internal/pkg/cli/command/apiKey/create.go +++ b/internal/pkg/cli/command/apiKey/create.go @@ -57,7 +57,8 @@ func NewCreateApiKeyCmd() *cobra.Command { pc api-key create --id "project-id" --name "key-name" `), Run: func(cmd *cobra.Command, args []string) { - ac := sdk.NewPineconeAdminClient() + ctx := cmd.Context() + ac := sdk.NewPineconeAdminClient(ctx) projId := options.projectId var err error diff --git a/internal/pkg/cli/command/apiKey/delete.go b/internal/pkg/cli/command/apiKey/delete.go index 65543db..a5b0dda 100644 --- a/internal/pkg/cli/command/apiKey/delete.go +++ b/internal/pkg/cli/command/apiKey/delete.go @@ -46,7 +46,8 @@ func NewDeleteKeyCmd() *cobra.Command { pc api-key delete --id "api-key-id" `), Run: func(cmd *cobra.Command, args []string) { - ac := sdk.NewPineconeAdminClient() + ctx := cmd.Context() + ac := sdk.NewPineconeAdminClient(ctx) // Verify key exists before trying to delete it. // This lets us give a more helpful error message than just diff --git a/internal/pkg/cli/command/apiKey/describe.go b/internal/pkg/cli/command/apiKey/describe.go index 2be9e1c..55922c4 100644 --- a/internal/pkg/cli/command/apiKey/describe.go +++ b/internal/pkg/cli/command/apiKey/describe.go @@ -28,7 +28,8 @@ func NewDescribeAPIKeyCmd() *cobra.Command { `), GroupID: help.GROUP_API_KEYS.ID, Run: func(cmd *cobra.Command, args []string) { - ac := sdk.NewPineconeAdminClient() + ctx := cmd.Context() + ac := sdk.NewPineconeAdminClient(ctx) apiKey, err := ac.APIKey.Describe(cmd.Context(), options.apiKeyID) if err != nil { diff --git a/internal/pkg/cli/command/apiKey/list.go b/internal/pkg/cli/command/apiKey/list.go index 6d42e13..1d41c5b 100644 --- a/internal/pkg/cli/command/apiKey/list.go +++ b/internal/pkg/cli/command/apiKey/list.go @@ -38,7 +38,8 @@ func NewListKeysCmd() *cobra.Command { `), GroupID: help.GROUP_API_KEYS.ID, Run: func(cmd *cobra.Command, args []string) { - ac := sdk.NewPineconeAdminClient() + ctx := cmd.Context() + ac := sdk.NewPineconeAdminClient(ctx) var err error projId := options.projectID diff --git a/internal/pkg/cli/command/apiKey/update.go b/internal/pkg/cli/command/apiKey/update.go index cd469c8..5338737 100644 --- a/internal/pkg/cli/command/apiKey/update.go +++ b/internal/pkg/cli/command/apiKey/update.go @@ -30,7 +30,8 @@ func NewUpdateAPIKeyCmd() *cobra.Command { `), GroupID: help.GROUP_API_KEYS.ID, Run: func(cmd *cobra.Command, args []string) { - ac := sdk.NewPineconeAdminClient() + ctx := cmd.Context() + ac := sdk.NewPineconeAdminClient(ctx) // Only set non-empty values updateParams := &pinecone.UpdateAPIKeyParams{} diff --git a/internal/pkg/cli/command/auth/configure.go b/internal/pkg/cli/command/auth/configure.go index 6a9bdd0..7b9b72a 100644 --- a/internal/pkg/cli/command/auth/configure.go +++ b/internal/pkg/cli/command/auth/configure.go @@ -139,7 +139,7 @@ func Run(ctx context.Context, io IO, opts configureCmdOptions) { // Use Admin API to fetch organization and project information for the service account // so that we can set the target context, or allow the user to set it like they do through the login or target flow - ac := sdk.NewPineconeAdminClient() + ac := sdk.NewPineconeAdminClient(ctx) // There should only be one organization listed for a service account orgs, err := ac.Organization.List(ctx) diff --git a/internal/pkg/cli/command/auth/local_keys_prune.go b/internal/pkg/cli/command/auth/local_keys_prune.go index 6098878..26e489b 100644 --- a/internal/pkg/cli/command/auth/local_keys_prune.go +++ b/internal/pkg/cli/command/auth/local_keys_prune.go @@ -80,7 +80,7 @@ func NewPruneLocalKeysCmd() *cobra.Command { } func runPruneLocalKeys(ctx context.Context, options pruneLocalKeysCmdOptions) { - ac := sdk.NewPineconeAdminClient() + ac := sdk.NewPineconeAdminClient(ctx) managedKeys := secrets.GetManagedProjectKeys() // Filter to projectId if provided diff --git a/internal/pkg/cli/command/collection/create.go b/internal/pkg/cli/command/collection/create.go index f145ab3..16307f5 100644 --- a/internal/pkg/cli/command/collection/create.go +++ b/internal/pkg/cli/command/collection/create.go @@ -1,8 +1,6 @@ package collection import ( - "context" - "github.com/pinecone-io/cli/internal/pkg/utils/exit" "github.com/pinecone-io/cli/internal/pkg/utils/help" "github.com/pinecone-io/cli/internal/pkg/utils/msg" @@ -32,8 +30,8 @@ func NewCreateCollectionCmd() *cobra.Command { pc collection create --name "collection-name" --source "index-source-name" `), Run: func(cmd *cobra.Command, args []string) { - pc := sdk.NewPineconeClient() - ctx := context.Background() + ctx := cmd.Context() + pc := sdk.NewPineconeClient(ctx) req := &pinecone.CreateCollectionRequest{ Name: options.name, diff --git a/internal/pkg/cli/command/collection/delete.go b/internal/pkg/cli/command/collection/delete.go index 1f78eaf..9dea8da 100644 --- a/internal/pkg/cli/command/collection/delete.go +++ b/internal/pkg/cli/command/collection/delete.go @@ -1,8 +1,6 @@ package collection import ( - "context" - "github.com/pinecone-io/cli/internal/pkg/utils/exit" "github.com/pinecone-io/cli/internal/pkg/utils/help" "github.com/pinecone-io/cli/internal/pkg/utils/msg" @@ -25,8 +23,8 @@ func NewDeleteCollectionCmd() *cobra.Command { pc collection delete --name "collection-name" `), Run: func(cmd *cobra.Command, args []string) { - ctx := context.Background() - pc := sdk.NewPineconeClient() + ctx := cmd.Context() + pc := sdk.NewPineconeClient(ctx) err := pc.DeleteCollection(ctx, options.name) if err != nil { diff --git a/internal/pkg/cli/command/collection/describe.go b/internal/pkg/cli/command/collection/describe.go index dd6ae19..ac774bb 100644 --- a/internal/pkg/cli/command/collection/describe.go +++ b/internal/pkg/cli/command/collection/describe.go @@ -1,8 +1,6 @@ package collection import ( - "context" - "github.com/pinecone-io/cli/internal/pkg/utils/exit" "github.com/pinecone-io/cli/internal/pkg/utils/help" "github.com/pinecone-io/cli/internal/pkg/utils/msg" @@ -28,8 +26,8 @@ func NewDescribeCollectionCmd() *cobra.Command { pc collection describe --name "collection-name" `), Run: func(cmd *cobra.Command, args []string) { - ctx := context.Background() - pc := sdk.NewPineconeClient() + ctx := cmd.Context() + pc := sdk.NewPineconeClient(ctx) collection, err := pc.DescribeCollection(ctx, options.name) if err != nil { diff --git a/internal/pkg/cli/command/collection/list.go b/internal/pkg/cli/command/collection/list.go index a2d41bc..9e28e8f 100644 --- a/internal/pkg/cli/command/collection/list.go +++ b/internal/pkg/cli/command/collection/list.go @@ -1,7 +1,6 @@ package collection import ( - "context" "os" "sort" "strconv" @@ -33,8 +32,8 @@ func NewListCollectionsCmd() *cobra.Command { pc collection list `), Run: func(cmd *cobra.Command, args []string) { - pc := sdk.NewPineconeClient() - ctx := context.Background() + ctx := cmd.Context() + pc := sdk.NewPineconeClient(ctx) collections, err := pc.ListCollections(ctx) if err != nil { diff --git a/internal/pkg/cli/command/index/configure.go b/internal/pkg/cli/command/index/configure.go index 85358e6..6c10c7e 100644 --- a/internal/pkg/cli/command/index/configure.go +++ b/internal/pkg/cli/command/index/configure.go @@ -33,7 +33,7 @@ func NewConfigureIndexCmd() *cobra.Command { pc index configure --name "index-name" --deletion-protection "enabled" `), Run: func(cmd *cobra.Command, args []string) { - runConfigureIndexCmd(options) + runConfigureIndexCmd(cmd.Context(), options) }, } @@ -48,9 +48,8 @@ func NewConfigureIndexCmd() *cobra.Command { return cmd } -func runConfigureIndexCmd(options configureIndexOptions) { - ctx := context.Background() - pc := sdk.NewPineconeClient() +func runConfigureIndexCmd(ctx context.Context, options configureIndexOptions) { + pc := sdk.NewPineconeClient(ctx) idx, err := pc.ConfigureIndex(ctx, options.name, pinecone.ConfigureIndexParams{ PodType: options.podType, diff --git a/internal/pkg/cli/command/index/create.go b/internal/pkg/cli/command/index/create.go index c01e083..e2f0054 100644 --- a/internal/pkg/cli/command/index/create.go +++ b/internal/pkg/cli/command/index/create.go @@ -99,7 +99,7 @@ func NewCreateIndexCmd() *cobra.Command { Long: createIndexHelp, Example: createIndexExample, Run: func(cmd *cobra.Command, args []string) { - runCreateIndexCmd(options) + runCreateIndexCmd(cmd.Context(), options) }, } @@ -141,9 +141,8 @@ func NewCreateIndexCmd() *cobra.Command { return cmd } -func runCreateIndexCmd(options createIndexOptions) { - ctx := context.Background() - pc := sdk.NewPineconeClient() +func runCreateIndexCmd(ctx context.Context, options createIndexOptions) { + pc := sdk.NewPineconeClient(ctx) idx, err := runCreateIndexWithService(ctx, pc, options) if err != nil { diff --git a/internal/pkg/cli/command/index/create_pod.go b/internal/pkg/cli/command/index/create_pod.go index dfa19a3..da38cf6 100644 --- a/internal/pkg/cli/command/index/create_pod.go +++ b/internal/pkg/cli/command/index/create_pod.go @@ -39,7 +39,7 @@ func NewCreatePodCmd() *cobra.Command { pc index create-pod --name "my-index" --dimension 1536 --metric "cosine" --environment "us-east-1-aws" --pod-type "p1.x1" --shards 2 --replicas 2 `), Run: func(cmd *cobra.Command, args []string) { - runCreatePodCmd(options) + runCreatePodCmd(cmd.Context(), options) }, } @@ -65,9 +65,8 @@ func NewCreatePodCmd() *cobra.Command { return cmd } -func runCreatePodCmd(options createPodOptions) { - ctx := context.Background() - pc := sdk.NewPineconeClient() +func runCreatePodCmd(ctx context.Context, options createPodOptions) { + pc := sdk.NewPineconeClient(ctx) // Deprecation warning pcio.Fprintf(os.Stderr, "⚠️ Warning: The '%s' command is deprecated. Please use '%s' instead.", style.Code("index create-pod"), style.Code("index create")) diff --git a/internal/pkg/cli/command/index/create_serverless.go b/internal/pkg/cli/command/index/create_serverless.go index cfa970f..eb9c9bb 100644 --- a/internal/pkg/cli/command/index/create_serverless.go +++ b/internal/pkg/cli/command/index/create_serverless.go @@ -36,7 +36,7 @@ func NewCreateServerlessCmd() *cobra.Command { pc index create-serverless --name "my-index" --dimension 1536 --metric "cosine" --cloud "aws" --region "us-east-1" `), Run: func(cmd *cobra.Command, args []string) { - runCreateServerlessCmd(options) + runCreateServerlessCmd(cmd.Context(), options) }, } @@ -58,9 +58,8 @@ func NewCreateServerlessCmd() *cobra.Command { return cmd } -func runCreateServerlessCmd(options createServerlessOptions) { - ctx := context.Background() - pc := sdk.NewPineconeClient() +func runCreateServerlessCmd(ctx context.Context, options createServerlessOptions) { + pc := sdk.NewPineconeClient(ctx) // Deprecation warning pcio.Fprintf(os.Stderr, "⚠️ Warning: The '%s' command is deprecated. Please use '%s' instead.", style.Code("index create-serverless"), style.Code("index create")) diff --git a/internal/pkg/cli/command/index/delete.go b/internal/pkg/cli/command/index/delete.go index ae57c00..902da3b 100644 --- a/internal/pkg/cli/command/index/delete.go +++ b/internal/pkg/cli/command/index/delete.go @@ -1,7 +1,6 @@ package index import ( - "context" "strings" "github.com/pinecone-io/cli/internal/pkg/utils/exit" @@ -26,8 +25,8 @@ func NewDeleteCmd() *cobra.Command { pc index delete --name "index-name" `), Run: func(cmd *cobra.Command, args []string) { - ctx := context.Background() - pc := sdk.NewPineconeClient() + ctx := cmd.Context() + pc := sdk.NewPineconeClient(ctx) err := pc.DeleteIndex(ctx, options.name) if err != nil { diff --git a/internal/pkg/cli/command/index/describe.go b/internal/pkg/cli/command/index/describe.go index 93f8165..1588c07 100644 --- a/internal/pkg/cli/command/index/describe.go +++ b/internal/pkg/cli/command/index/describe.go @@ -29,7 +29,8 @@ func NewDescribeCmd() *cobra.Command { pc index describe --name "index-name" `), Run: func(cmd *cobra.Command, args []string) { - pc := sdk.NewPineconeClient() + ctx := cmd.Context() + pc := sdk.NewPineconeClient(ctx) idx, err := pc.DescribeIndex(cmd.Context(), options.name) if err != nil { diff --git a/internal/pkg/cli/command/index/list.go b/internal/pkg/cli/command/index/list.go index 5e632df..4a7a858 100644 --- a/internal/pkg/cli/command/index/list.go +++ b/internal/pkg/cli/command/index/list.go @@ -1,7 +1,6 @@ package index import ( - "context" "os" "sort" "strings" @@ -32,8 +31,8 @@ func NewListCmd() *cobra.Command { pc index list `), Run: func(cmd *cobra.Command, args []string) { - pc := sdk.NewPineconeClient() - ctx := context.Background() + ctx := cmd.Context() + pc := sdk.NewPineconeClient(ctx) idxs, err := pc.ListIndexes(ctx) if err != nil { diff --git a/internal/pkg/cli/command/organization/delete.go b/internal/pkg/cli/command/organization/delete.go index b26471b..e0f9f81 100644 --- a/internal/pkg/cli/command/organization/delete.go +++ b/internal/pkg/cli/command/organization/delete.go @@ -34,7 +34,8 @@ func NewDeleteOrganizationCmd() *cobra.Command { `), GroupID: help.GROUP_ORGANIZATIONS.ID, Run: func(cmd *cobra.Command, args []string) { - ac := sdk.NewPineconeAdminClient() + ctx := cmd.Context() + ac := sdk.NewPineconeAdminClient(ctx) // get the organization first org, err := ac.Organization.Describe(cmd.Context(), options.organizationID) diff --git a/internal/pkg/cli/command/organization/describe.go b/internal/pkg/cli/command/organization/describe.go index 833b0d0..d6ae4f6 100644 --- a/internal/pkg/cli/command/organization/describe.go +++ b/internal/pkg/cli/command/organization/describe.go @@ -29,7 +29,8 @@ func NewDescribeOrganizationCmd() *cobra.Command { `), GroupID: help.GROUP_ORGANIZATIONS.ID, Run: func(cmd *cobra.Command, args []string) { - ac := sdk.NewPineconeAdminClient() + ctx := cmd.Context() + ac := sdk.NewPineconeAdminClient(ctx) orgId := options.organizationID var err error diff --git a/internal/pkg/cli/command/organization/list.go b/internal/pkg/cli/command/organization/list.go index 9feea27..91580f6 100644 --- a/internal/pkg/cli/command/organization/list.go +++ b/internal/pkg/cli/command/organization/list.go @@ -31,7 +31,8 @@ func NewListOrganizationsCmd() *cobra.Command { `), GroupID: help.GROUP_ORGANIZATIONS.ID, Run: func(cmd *cobra.Command, args []string) { - ac := sdk.NewPineconeAdminClient() + ctx := cmd.Context() + ac := sdk.NewPineconeAdminClient(ctx) orgs, err := ac.Organization.List(cmd.Context()) if err != nil { diff --git a/internal/pkg/cli/command/organization/update.go b/internal/pkg/cli/command/organization/update.go index fedae0e..5549791 100644 --- a/internal/pkg/cli/command/organization/update.go +++ b/internal/pkg/cli/command/organization/update.go @@ -31,7 +31,8 @@ func NewUpdateOrganizationCmd() *cobra.Command { `), GroupID: help.GROUP_ORGANIZATIONS.ID, Run: func(cmd *cobra.Command, args []string) { - ac := sdk.NewPineconeAdminClient() + ctx := cmd.Context() + ac := sdk.NewPineconeAdminClient(ctx) orgId := options.organizationID var err error diff --git a/internal/pkg/cli/command/project/create.go b/internal/pkg/cli/command/project/create.go index e8c422f..1359750 100644 --- a/internal/pkg/cli/command/project/create.go +++ b/internal/pkg/cli/command/project/create.go @@ -1,8 +1,6 @@ package project import ( - "context" - "github.com/pinecone-io/cli/internal/pkg/utils/configuration/state" "github.com/pinecone-io/cli/internal/pkg/utils/exit" "github.com/pinecone-io/cli/internal/pkg/utils/help" @@ -49,7 +47,8 @@ func NewCreateProjectCmd() *cobra.Command { pc project create --name "demo-project" --max-pods 10 --force-encryption `), Run: func(cmd *cobra.Command, args []string) { - ac := sdk.NewPineconeAdminClient() + ctx := cmd.Context() + ac := sdk.NewPineconeAdminClient(ctx) createParams := &pinecone.CreateProjectParams{} if options.name != "" { @@ -62,7 +61,7 @@ func NewCreateProjectCmd() *cobra.Command { createParams.ForceEncryptionWithCmek = &options.forceEncryptionWithCMEK } - proj, err := ac.Project.Create(context.Background(), createParams) + proj, err := ac.Project.Create(ctx, createParams) if err != nil { msg.FailMsg("Failed to create project %s: %s\n", style.Emphasis(options.name), err) exit.Errorf(err, "Failed to create project %s", style.Emphasis(options.name)) diff --git a/internal/pkg/cli/command/project/delete.go b/internal/pkg/cli/command/project/delete.go index 69dbd42..38e537e 100644 --- a/internal/pkg/cli/command/project/delete.go +++ b/internal/pkg/cli/command/project/delete.go @@ -38,8 +38,8 @@ func NewDeleteProjectCmd() *cobra.Command { `), GroupID: help.GROUP_PROJECTS.ID, Run: func(cmd *cobra.Command, args []string) { - ac := sdk.NewPineconeAdminClient() - ctx := context.Background() + ctx := cmd.Context() + ac := sdk.NewPineconeAdminClient(ctx) projId := options.projectId var err error @@ -58,8 +58,8 @@ func NewDeleteProjectCmd() *cobra.Command { exit.Error(err, "Failed to retrieve project information") } - verifyNoIndexes(projToDelete.Id, projToDelete.Name) - verifyNoCollections(projToDelete.Id, projToDelete.Name) + verifyNoIndexes(ctx, projToDelete.Id, projToDelete.Name) + verifyNoCollections(ctx, projToDelete.Id, projToDelete.Name) if !options.skipConfirmation { confirmDelete(projToDelete.Name) @@ -117,10 +117,9 @@ func confirmDelete(projectName string) { } } -func verifyNoIndexes(projectId string, projectName string) { +func verifyNoIndexes(ctx context.Context, projectId string, projectName string) { // Check if project contains indexes - pc := sdk.NewPineconeClientForProjectById(projectId) - ctx := context.Background() + pc := sdk.NewPineconeClientForProjectById(ctx, projectId) idxs, err := pc.ListIndexes(ctx) if err != nil { @@ -134,10 +133,9 @@ func verifyNoIndexes(projectId string, projectName string) { } } -func verifyNoCollections(projectId string, projectName string) { +func verifyNoCollections(ctx context.Context, projectId string, projectName string) { // Check if project contains collections - pc := sdk.NewPineconeClientForProjectById(projectId) - ctx := context.Background() + pc := sdk.NewPineconeClientForProjectById(ctx, projectId) collections, err := pc.ListCollections(ctx) if err != nil { diff --git a/internal/pkg/cli/command/project/describe.go b/internal/pkg/cli/command/project/describe.go index edf85b7..9558b7e 100644 --- a/internal/pkg/cli/command/project/describe.go +++ b/internal/pkg/cli/command/project/describe.go @@ -1,8 +1,6 @@ package project import ( - "context" - "github.com/pinecone-io/cli/internal/pkg/utils/configuration/state" "github.com/pinecone-io/cli/internal/pkg/utils/exit" "github.com/pinecone-io/cli/internal/pkg/utils/help" @@ -31,7 +29,8 @@ func NewDescribeProjectCmd() *cobra.Command { pc project describe --id "project-id" `), Run: func(cmd *cobra.Command, args []string) { - ac := sdk.NewPineconeAdminClient() + ctx := cmd.Context() + ac := sdk.NewPineconeAdminClient(ctx) projId := options.projectID var err error @@ -43,7 +42,7 @@ func NewDescribeProjectCmd() *cobra.Command { } } - project, err := ac.Project.Describe(context.Background(), projId) + project, err := ac.Project.Describe(ctx, projId) if err != nil { msg.FailMsg("Failed to describe project %s: %s\n", projId, err) exit.Errorf(err, "Failed to describe project %s", style.Emphasis(projId)) diff --git a/internal/pkg/cli/command/project/list.go b/internal/pkg/cli/command/project/list.go index 3bae081..8014924 100644 --- a/internal/pkg/cli/command/project/list.go +++ b/internal/pkg/cli/command/project/list.go @@ -1,7 +1,6 @@ package project import ( - "context" "os" "strconv" "strings" @@ -33,8 +32,8 @@ func NewListProjectsCmd() *cobra.Command { pc project list `), Run: func(cmd *cobra.Command, args []string) { - ac := sdk.NewPineconeAdminClient() - ctx := context.Background() + ctx := cmd.Context() + ac := sdk.NewPineconeAdminClient(ctx) projects, err := ac.Project.List(ctx) if err != nil { diff --git a/internal/pkg/cli/command/project/update.go b/internal/pkg/cli/command/project/update.go index 10ef925..1d40d36 100644 --- a/internal/pkg/cli/command/project/update.go +++ b/internal/pkg/cli/command/project/update.go @@ -1,8 +1,6 @@ package project import ( - "context" - "github.com/pinecone-io/cli/internal/pkg/utils/configuration/state" "github.com/pinecone-io/cli/internal/pkg/utils/exit" "github.com/pinecone-io/cli/internal/pkg/utils/help" @@ -35,7 +33,8 @@ func NewUpdateProjectCmd() *cobra.Command { `), GroupID: help.GROUP_PROJECTS.ID, Run: func(cmd *cobra.Command, args []string) { - ac := sdk.NewPineconeAdminClient() + ctx := cmd.Context() + ac := sdk.NewPineconeAdminClient(ctx) projId := options.projectId var err error @@ -60,7 +59,7 @@ func NewUpdateProjectCmd() *cobra.Command { updateParams.MaxPods = &options.maxPods } - project, err := ac.Project.Update(context.Background(), projId, updateParams) + project, err := ac.Project.Update(ctx, projId, updateParams) if err != nil { msg.FailMsg("Failed to update project %s: %s\n", projId, err) exit.Errorf(err, "Failed to update project %s", style.Emphasis(projId)) diff --git a/internal/pkg/cli/command/root/root.go b/internal/pkg/cli/command/root/root.go index 8092db3..290d0cf 100644 --- a/internal/pkg/cli/command/root/root.go +++ b/internal/pkg/cli/command/root/root.go @@ -1,7 +1,11 @@ package root import ( + "context" "os" + "os/signal" + "syscall" + "time" "github.com/pinecone-io/cli/internal/pkg/cli/command/apiKey" "github.com/pinecone-io/cli/internal/pkg/cli/command/auth" @@ -20,14 +24,23 @@ import ( "github.com/spf13/cobra" ) -var rootCmd *cobra.Command +var ( + rootCmd *cobra.Command + globalOptions GlobalOptions + cancelRootFunc context.CancelFunc +) type GlobalOptions struct { - quiet bool + quiet bool + timeout time.Duration } func Execute() { - err := rootCmd.Execute() + //Base context: cancel on SIGINT / SIGTERM + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) + defer cancel() + + err := rootCmd.ExecuteContext(ctx) if err != nil { os.Exit(1) } @@ -62,12 +75,30 @@ var ( ) func init() { - globalOptions := GlobalOptions{} + // Default timeout for context.Context cancellation + // This is applied to individual operations within subcommands through cmd.Context() + defaultTimeout := 60 * time.Second + globalOptions = GlobalOptions{} + rootCmd = &cobra.Command{ Use: "pc", Short: "Manage your Pinecone vector database infrastructure from the command line", PersistentPreRun: func(cmd *cobra.Command, args []string) { pcio.SetQuiet(globalOptions.quiet) + + // Apply timeout to the command context + if globalOptions.timeout > 0 { + ctx, cancel := context.WithTimeout(cmd.Context(), globalOptions.timeout) + cancelRootFunc = cancel + cmd.SetContext(ctx) + } + }, + PersistentPostRun: func(cmd *cobra.Command, args []string) { + // Cancel the root context when the command completes + if cancelRootFunc != nil { + cancelRootFunc() + cancelRootFunc = nil + } }, Example: help.Examples(` pc login @@ -77,6 +108,7 @@ func init() { Long: rootHelp, } + // Help template and rendering rootCmd.SetHelpTemplate(help.HelpTemplate) help.EnableHelpRendering(rootCmd) @@ -111,4 +143,5 @@ func init() { // Global flags rootCmd.PersistentFlags().BoolVarP(&globalOptions.quiet, "quiet", "q", false, "suppress output") + rootCmd.PersistentFlags().DurationVar(&globalOptions.timeout, "timeout", defaultTimeout, "timeout for commands, defaults to 60s (0 to disable)") } diff --git a/internal/pkg/cli/command/target/target.go b/internal/pkg/cli/command/target/target.go index 17c002d..c562a48 100644 --- a/internal/pkg/cli/command/target/target.go +++ b/internal/pkg/cli/command/target/target.go @@ -71,6 +71,7 @@ func NewTargetCmd() *cobra.Command { Example: targetExample, GroupID: help.GROUP_AUTH.ID, Run: func(cmd *cobra.Command, args []string) { + ctx := cmd.Context() log.Debug(). Str("org", options.org). Str("project", options.project). @@ -131,7 +132,7 @@ func NewTargetCmd() *cobra.Command { exit.ErrorMsg("You must be logged in or have service account credentials configured to set a target context") } - ac := sdk.NewPineconeAdminClient() + ac := sdk.NewPineconeAdminClient(ctx) // Fetch the user's organizations orgs, err := ac.Organization.List(cmd.Context()) @@ -157,7 +158,7 @@ func NewTargetCmd() *cobra.Command { // If the org chosen differs from the current orgId in the token, we need to login again if currentTokenOrgId != "" && currentTokenOrgId != targetOrg.Id { oauth.Logout() - err = login.GetAndSetAccessToken(&targetOrg.Id) + err = login.GetAndSetAccessToken(ctx, &targetOrg.Id) if err != nil { msg.FailMsg("Failed to get access token: %s", err) exit.Error(err, "Error getting access token") @@ -165,7 +166,7 @@ func NewTargetCmd() *cobra.Command { } } - ac := sdk.NewPineconeAdminClient() + ac := sdk.NewPineconeAdminClient(ctx) // Fetch the user's projects projects, err := ac.Project.List(cmd.Context()) if err != nil { @@ -203,7 +204,7 @@ func NewTargetCmd() *cobra.Command { // If the org chosen differs from the current orgId in the token, we need to login again if currentTokenOrgId != org.Id { oauth.Logout() - err = login.GetAndSetAccessToken(&org.Id) + err = login.GetAndSetAccessToken(ctx, &org.Id) if err != nil { msg.FailMsg("Failed to get access token: %s", err) exit.Error(err, "Error getting access token") @@ -229,7 +230,7 @@ func NewTargetCmd() *cobra.Command { if options.project != "" || options.projectID != "" { // We need to reinstantiate the admin client to ensure any auth changes that have happened above // are properly reflected - ac := sdk.NewPineconeAdminClient() + ac := sdk.NewPineconeAdminClient(ctx) // Fetch the user's projects projects, err := ac.Project.List(cmd.Context()) diff --git a/internal/pkg/utils/login/login.go b/internal/pkg/utils/login/login.go index 91ff2ad..4d030a1 100644 --- a/internal/pkg/utils/login/login.go +++ b/internal/pkg/utils/login/login.go @@ -43,7 +43,7 @@ type IO struct { type Options struct{} func Run(ctx context.Context, io IO, opts Options) { - err := GetAndSetAccessToken(nil) + err := GetAndSetAccessToken(ctx, nil) if err != nil { msg.FailMsg("Error acquiring access token while logging in: %s", err) exit.Error(err, "Error acquiring access token while logging in") @@ -62,7 +62,7 @@ func Run(ctx context.Context, io IO, opts Options) { } msg.SuccessMsg("Logged in as " + style.Emphasis(claims.Email) + ". Defaulted to organization ID: " + style.Emphasis(claims.OrgId)) - ac := sdk.NewPineconeAdminClient() + ac := sdk.NewPineconeAdminClient(ctx) if err != nil { msg.FailMsg("Error creating Pinecone admin client: %s", err) exit.Error(err, "Error creating Pinecone admin client") @@ -121,8 +121,7 @@ func Run(ctx context.Context, io IO, opts Options) { // Takes an optional orgId, and attempts to acquire an access token scoped to the orgId if provided. // If a token is successfully acquired it's set in the secrets store, and the user is considered logged in with state.AuthUserToken. -func GetAndSetAccessToken(orgId *string) error { - ctx := context.Background() +func GetAndSetAccessToken(ctx context.Context, orgId *string) error { a := oauth.Auth{} // CSRF state diff --git a/internal/pkg/utils/sdk/client.go b/internal/pkg/utils/sdk/client.go index 1acf11e..daf916b 100644 --- a/internal/pkg/utils/sdk/client.go +++ b/internal/pkg/utils/sdk/client.go @@ -24,7 +24,7 @@ const ( CLISourceTag = "pinecone-cli" ) -func NewPineconeClient() *pinecone.Client { +func NewPineconeClient(ctx context.Context) *pinecone.Client { targetOrgId := state.TargetOrg.Get().Id targetProjectId := state.TargetProj.Get().Id log.Debug(). @@ -32,7 +32,6 @@ func NewPineconeClient() *pinecone.Client { Str("targetProjectId", targetProjectId). Msg("Loading target context") - ctx := context.Background() oauth2Token, err := oauth.Token(ctx) if err != nil { log.Error().Err(err).Msg("Error retrieving oauth token") @@ -65,12 +64,11 @@ func NewPineconeClient() *pinecone.Client { exit.ErrorMsg("No target project set") } - return NewPineconeClientForProjectById(targetProjectId) + return NewPineconeClientForProjectById(ctx, targetProjectId) } -func NewPineconeClientForProjectById(projectId string) *pinecone.Client { - ac := NewPineconeAdminClient() - ctx := context.Background() +func NewPineconeClientForProjectById(ctx context.Context, projectId string) *pinecone.Client { + ac := NewPineconeAdminClient(ctx) project, err := ac.Project.Describe(ctx, projectId) if err != nil { @@ -121,8 +119,7 @@ func NewClientForAPIKey(apiKey string) *pinecone.Client { return pc } -func NewPineconeAdminClient() *pinecone.AdminClient { - ctx := context.Background() +func NewPineconeAdminClient(ctx context.Context) *pinecone.AdminClient { oauth2Token, err := oauth.Token(ctx) if err != nil { log.Error().Err(err).Msg("Error retrieving oauth token") @@ -153,6 +150,22 @@ func NewPineconeAdminClient() *pinecone.AdminClient { return ac } +func NewIndexConnection(ctx context.Context, pc *pinecone.Client, indexName string, namespace string) (*pinecone.IndexConnection, error) { + index, err := pc.DescribeIndex(ctx, indexName) + if err != nil { + return nil, pcio.Errorf("failed to describe index: %w", err) + } + + ic, err := pc.Index(pinecone.NewIndexConnParams{ + Host: index.Host, + Namespace: namespace, + }) + if err != nil { + return nil, pcio.Errorf("failed to create index connection: %w", err) + } + return ic, nil +} + func getCLIAPIKeyForProject(ctx context.Context, ac *pinecone.AdminClient, project *pinecone.Project) (string, error) { projectAPIKeysMap := secrets.ManagedAPIKeys.Get() var managedKey secrets.ManagedKey From 1920cdd575131327cab315d7e9292c53781c7813 Mon Sep 17 00:00:00 2001 From: Austin DeNoble Date: Fri, 14 Nov 2025 15:26:19 -0500 Subject: [PATCH 05/18] implement basic upsert functionality under the pc index upsert command --- internal/pkg/cli/command/index/cmd.go | 1 + internal/pkg/cli/command/index/upsert.go | 164 +++++++++++++++++++++++ 2 files changed, 165 insertions(+) create mode 100644 internal/pkg/cli/command/index/upsert.go diff --git a/internal/pkg/cli/command/index/cmd.go b/internal/pkg/cli/command/index/cmd.go index 284eb3c..283b2c6 100644 --- a/internal/pkg/cli/command/index/cmd.go +++ b/internal/pkg/cli/command/index/cmd.go @@ -39,6 +39,7 @@ func NewIndexCmd() *cobra.Command { cmd.AddCommand(NewCreatePodCmd()) cmd.AddCommand(NewConfigureIndexCmd()) cmd.AddCommand(NewDeleteCmd()) + cmd.AddCommand(NewUpsertCmd()) return cmd } diff --git a/internal/pkg/cli/command/index/upsert.go b/internal/pkg/cli/command/index/upsert.go new file mode 100644 index 0000000..75502a4 --- /dev/null +++ b/internal/pkg/cli/command/index/upsert.go @@ -0,0 +1,164 @@ +package index + +import ( + "encoding/json" + "os" + + "github.com/pinecone-io/cli/internal/pkg/utils/exit" + "github.com/pinecone-io/cli/internal/pkg/utils/help" + "github.com/pinecone-io/cli/internal/pkg/utils/msg" + "github.com/pinecone-io/cli/internal/pkg/utils/pcio" + "github.com/pinecone-io/cli/internal/pkg/utils/sdk" + "github.com/pinecone-io/cli/internal/pkg/utils/style" + "github.com/pinecone-io/cli/internal/pkg/utils/text" + "github.com/spf13/cobra" + + "github.com/pinecone-io/go-pinecone/v5/pinecone" +) + +type upsertCmdOptions struct { + file string + name string + namespace string + json bool +} + +type upsertFile struct { + Vectors []upsertVector `json:"vectors"` + Namespace string `json:"namespace"` +} + +type upsertVector struct { + ID string `json:"id"` + Values []float32 `json:"values"` + SparseValues *sparseValues `json:"sparse_values,omitempty"` + Metadata map[string]any `json:"metadata,omitempty"` +} + +type sparseValues struct { + Indices []uint32 `json:"indices"` + Values []float32 `json:"values"` +} + +func NewUpsertCmd() *cobra.Command { + options := upsertCmdOptions{} + + cmd := &cobra.Command{ + Use: "upsert [file]", + Short: "Upsert vectors into an index from a JSON file", + Example: help.Examples(` + pc index upsert --name my-index --namespace my-namespace ./vectors.json + `), + Run: func(cmd *cobra.Command, args []string) { + runUpsertCmd(cmd, options) + }, + } + + cmd.Flags().StringVarP(&options.name, "name", "n", "", "name of index to upsert into") + cmd.Flags().StringVar(&options.namespace, "namespace", "", "namespace to upsert into") + cmd.Flags().StringVarP(&options.file, "file", "f", "", "file to upsert from") + cmd.Flags().BoolVar(&options.json, "json", false, "output as JSON") + _ = cmd.MarkFlagRequired("name") + _ = cmd.MarkFlagRequired("file") + + return cmd +} + +func runUpsertCmd(cmd *cobra.Command, options upsertCmdOptions) { + filePath := options.file + raw, err := os.ReadFile(filePath) + if err != nil { + msg.FailMsg("Failed to read file %s: %s", style.Emphasis(filePath), err) + exit.Error().Err(err).Msgf("Failed to read file %s", filePath) + } + + var payload upsertFile + if err := json.Unmarshal(raw, &payload); err != nil { + msg.FailMsg("Failed to parse JSON from %s: %s", style.Emphasis(filePath), err) + exit.Error().Err(err).Msg("Failed to parse JSON for upsert") + } + + // Default namespace + ns := payload.Namespace + if options.namespace != "" { + ns = options.namespace + } + // Default if no namespace provided + if ns == "" { + ns = "__default__" + } + + if len(payload.Vectors) == 0 { + msg.FailMsg("No vectors found in %s", style.Emphasis(filePath)) + exit.Error().Msg("No vectors provided for upsert") + } + + // Map to SDK types + mapped := make([]*pinecone.Vector, 0, len(payload.Vectors)) + for _, v := range payload.Vectors { + values := v.Values + metadata, err := pinecone.NewMetadata(v.Metadata) + if err != nil { + msg.FailMsg("Failed to parse metadata: %s", err) + exit.Error().Err(err).Msg("Failed to parse metadata") + } + + var vector pinecone.Vector + vector.Id = v.ID + if v.Values != nil { + vector.Values = &values + } + if v.SparseValues != nil { + vector.SparseValues = &pinecone.SparseValues{ + Indices: v.SparseValues.Indices, + Values: v.SparseValues.Values, + } + } + vector.Metadata = metadata + mapped = append(mapped, &vector) + } + + // Get Pinecone client + pc := sdk.NewPineconeClient() + // TODO - Refactor this into an all-in-one function in sdk package + // Get index and establish IndexConnection + index, err := pc.DescribeIndex(cmd.Context(), options.name) + if err != nil { + msg.FailMsg("Failed to describe index %s: %s", style.Emphasis(options.name), err) + exit.Error().Err(err).Msg("Failed to describe index") + } + + ic, err := pc.Index(pinecone.NewIndexConnParams{ + Host: index.Host, + }) + if err != nil { + msg.FailMsg("Failed to create index connection: %s", err) + exit.Error().Err(err).Msg("Failed to create index connection") + } + // TODO - Isolate all of this ^^^^^^^^^^^^^^^^ + + batchSize := 1000 + batches := make([][]*pinecone.Vector, 0, (len(mapped)+batchSize-1)/batchSize) + for i := 0; i < len(mapped); i += batchSize { + end := i + batchSize + if end > len(mapped) { + end = len(mapped) + } + batches = append(batches, mapped[i:end]) + } + + for i, batch := range batches { + resp, err := ic.UpsertVectors(cmd.Context(), batch) + if err != nil { + msg.FailMsg("Failed to upsert %d vectors in batch %d: %s", len(batch), i+1, err) + exit.Error().Err(err).Msgf("Failed to upsert %d vectors in batch %d", len(batch), i+1) + } else { + if options.json { + json := text.IndentJSON(resp) + pcio.Println(json) + } else { + msg.SuccessMsg("Upserted %d vectors into namespace %s in %d batches", len(batch), ns, i+1) + } + } + } +} From a9b29ad40d3c89f6b037f10e377fb41314768e8d Mon Sep 17 00:00:00 2001 From: Austin DeNoble Date: Sun, 16 Nov 2025 15:24:15 -0500 Subject: [PATCH 06/18] update off rebase, add basic index fetch command --- internal/pkg/cli/command/index/cmd.go | 1 + internal/pkg/cli/command/index/fetch.go | 88 +++++++++++++++++++ internal/pkg/cli/command/index/upsert.go | 14 +-- .../pkg/utils/presenters/fetch_vectors.go | 57 ++++++++++++ internal/pkg/utils/presenters/managed_keys.go | 1 - 5 files changed, 153 insertions(+), 8 deletions(-) create mode 100644 internal/pkg/cli/command/index/fetch.go create mode 100644 internal/pkg/utils/presenters/fetch_vectors.go delete mode 100644 internal/pkg/utils/presenters/managed_keys.go diff --git a/internal/pkg/cli/command/index/cmd.go b/internal/pkg/cli/command/index/cmd.go index 283b2c6..8b8d10c 100644 --- a/internal/pkg/cli/command/index/cmd.go +++ b/internal/pkg/cli/command/index/cmd.go @@ -40,6 +40,7 @@ func NewIndexCmd() *cobra.Command { cmd.AddCommand(NewConfigureIndexCmd()) cmd.AddCommand(NewDeleteCmd()) cmd.AddCommand(NewUpsertCmd()) + cmd.AddCommand(NewFetchCmd()) return cmd } diff --git a/internal/pkg/cli/command/index/fetch.go b/internal/pkg/cli/command/index/fetch.go new file mode 100644 index 0000000..34bcc74 --- /dev/null +++ b/internal/pkg/cli/command/index/fetch.go @@ -0,0 +1,88 @@ +package index + +import ( + "context" + + "github.com/pinecone-io/cli/internal/pkg/utils/exit" + "github.com/pinecone-io/cli/internal/pkg/utils/help" + "github.com/pinecone-io/cli/internal/pkg/utils/msg" + "github.com/pinecone-io/cli/internal/pkg/utils/pcio" + "github.com/pinecone-io/cli/internal/pkg/utils/presenters" + "github.com/pinecone-io/cli/internal/pkg/utils/sdk" + "github.com/pinecone-io/cli/internal/pkg/utils/style" + "github.com/pinecone-io/cli/internal/pkg/utils/text" + "github.com/pinecone-io/go-pinecone/v5/pinecone" + "github.com/spf13/cobra" +) + +type fetchCmdOptions struct { + name string + namespace string + ids []string + json bool +} + +func NewFetchCmd() *cobra.Command { + options := fetchCmdOptions{} + cmd := &cobra.Command{ + Use: "fetch", + Short: "Fetch vectors by ID from an index", + Example: help.Examples(` + pc index fetch --name my-index --ids 123, 456, 789 + `), + Run: func(cmd *cobra.Command, args []string) { + runFetchCmd(cmd.Context(), options) + }, + } + + cmd.Flags().StringSliceVarP(&options.ids, "ids", "i", []string{}, "IDs of vectors to fetch") + cmd.Flags().StringVarP(&options.name, "name", "n", "", "name of the index to fetch from") + cmd.Flags().StringVar(&options.namespace, "namespace", "", "namespace to fetch from") + cmd.Flags().BoolVarP(&options.json, "json", "j", false, "output as JSON") + _ = cmd.MarkFlagRequired("name") + _ = cmd.MarkFlagRequired("ids") + + return cmd +} + +func runFetchCmd(ctx context.Context, options fetchCmdOptions) { + pc := sdk.NewPineconeClient() + + index, err := pc.DescribeIndex(ctx, options.name) + if err != nil { + msg.FailMsg("Failed to describe index %s: %s", style.Emphasis(options.name), err) + exit.Error(err, "Failed to describe index") + } + + ic, err := pc.Index(pinecone.NewIndexConnParams{ + Host: index.Host, + }) + if err != nil { + msg.FailMsg("Failed to create index connection: %s", err) + exit.Error(err, "Failed to create index connection") + } + + // Default namespace + ns := options.namespace + if options.namespace != "" { + ns = options.namespace + } + if ns == "" { + ns = "__default__" + } + if ns != ic.Namespace() { + ic = ic.WithNamespace(ns) + } + + vectors, err := ic.FetchVectors(ctx, options.ids) + if err != nil { + exit.Error(err, "Failed to fetch vectors") + } + + if options.json { + json := text.IndentJSON(vectors) + pcio.Println(json) + } else { + presenters.PrintFetchVectorsTable(vectors) + } +} diff --git a/internal/pkg/cli/command/index/upsert.go b/internal/pkg/cli/command/index/upsert.go index 75502a4..78605c7 100644 --- a/internal/pkg/cli/command/index/upsert.go +++ b/internal/pkg/cli/command/index/upsert.go @@ -69,13 +69,13 @@ func runUpsertCmd(cmd *cobra.Command, options upsertCmdOptions) { raw, err := os.ReadFile(filePath) if err != nil { msg.FailMsg("Failed to read file %s: %s", style.Emphasis(filePath), err) - exit.Error().Err(err).Msgf("Failed to read file %s", filePath) + exit.Errorf(err, "Failed to read file %s", filePath) } var payload upsertFile if err := json.Unmarshal(raw, &payload); err != nil { msg.FailMsg("Failed to parse JSON from %s: %s", style.Emphasis(filePath), err) - exit.Error().Err(err).Msg("Failed to parse JSON for upsert") + exit.Error(err, "Failed to parse JSON for upsert") } // Default namespace @@ -90,7 +90,7 @@ func runUpsertCmd(cmd *cobra.Command, options upsertCmdOptions) { if len(payload.Vectors) == 0 { msg.FailMsg("No vectors found in %s", style.Emphasis(filePath)) - exit.Error().Msg("No vectors provided for upsert") + exit.ErrorMsg("No vectors provided for upsert") } // Map to SDK types @@ -100,7 +100,7 @@ func runUpsertCmd(cmd *cobra.Command, options upsertCmdOptions) { metadata, err := pinecone.NewMetadata(v.Metadata) if err != nil { msg.FailMsg("Failed to parse metadata: %s", err) - exit.Error().Err(err).Msg("Failed to parse metadata") + exit.Error(err, "Failed to parse metadata") } var vector pinecone.Vector @@ -125,7 +125,7 @@ func runUpsertCmd(cmd *cobra.Command, options upsertCmdOptions) { index, err := pc.DescribeIndex(cmd.Context(), options.name) if err != nil { msg.FailMsg("Failed to describe index %s: %s", style.Emphasis(options.name), err) - exit.Error().Err(err).Msg("Failed to describe index") + exit.Error(err, "Failed to describe index") } ic, err := pc.Index(pinecone.NewIndexConnParams{ @@ -133,7 +133,7 @@ func runUpsertCmd(cmd *cobra.Command, options upsertCmdOptions) { }) if err != nil { msg.FailMsg("Failed to create index connection: %s", err) - exit.Error().Err(err).Msg("Failed to create index connection") + exit.Error(err, "Failed to create index connection") } // TODO - Isolate all of this ^^^^^^^^^^^^^^^^ @@ -151,7 +151,7 @@ func runUpsertCmd(cmd *cobra.Command, options upsertCmdOptions) { resp, err := ic.UpsertVectors(cmd.Context(), batch) if err != nil { msg.FailMsg("Failed to upsert %d vectors in batch %d: %s", len(batch), i+1, err) - exit.Error().Err(err).Msgf("Failed to upsert %d vectors in batch %d", len(batch), i+1) + exit.Errorf(err, "Failed to upsert %d vectors in batch %d", len(batch), i+1) } else { if options.json { json := text.IndentJSON(resp) diff --git a/internal/pkg/utils/presenters/fetch_vectors.go b/internal/pkg/utils/presenters/fetch_vectors.go new file mode 100644 index 0000000..ce2bee9 --- /dev/null +++ b/internal/pkg/utils/presenters/fetch_vectors.go @@ -0,0 +1,57 @@ +package presenters + +import ( + "strings" + + "github.com/pinecone-io/cli/internal/pkg/utils/pcio" + "github.com/pinecone-io/cli/internal/pkg/utils/text" + "github.com/pinecone-io/go-pinecone/v5/pinecone" +) + +func PrintFetchVectorsTable(resp *pinecone.FetchVectorsResponse) { + writer := NewTabWriter() + + // Header Block + if resp.Namespace != "" { + pcio.Fprintf(writer, "Namespace: %s\n", resp.Namespace) + } + if resp.Usage != nil { + pcio.Fprintf(writer, "Usage: %d (read units)\n", resp.Usage.ReadUnits) + } + + // Table Header + columns := []string{"ID", "DIMENSION", "VALUES", "SPARSE VALUES", "METADATA"} + pcio.Fprintln(writer, strings.Join(columns, "\t")) + + // Rows + for id, vector := range resp.Vectors { + dim := 0 + if vector.Values != nil { + dim = len(*vector.Values) + } + sparseDim := 0 + if vector.SparseValues != nil { + sparseDim = len(vector.SparseValues.Values) + } + metadata := "" + if vector.Metadata != nil { + metadata = text.InlineJSON(vector.Metadata) + } + preview := previewSlice(vector.Values, 3) + row := []string{id, pcio.Sprintf("%d", dim), preview, pcio.Sprintf("%d", sparseDim), metadata} + pcio.Fprintln(writer, strings.Join(row, "\t")) + } + + writer.Flush() +} + +func previewSlice(values *[]float32, limit int) string { + if values == nil || len(*values) == 0 { + return "" + } + vals := *values + if len(vals) > limit { + vals = vals[:limit] + } + return text.InlineJSON(vals) + "..." +} diff --git a/internal/pkg/utils/presenters/managed_keys.go b/internal/pkg/utils/presenters/managed_keys.go deleted file mode 100644 index 0cc9e34..0000000 --- a/internal/pkg/utils/presenters/managed_keys.go +++ /dev/null @@ -1 +0,0 @@ -package presenters From ce67b83439e848e68a836c7f61a389e7602950a1 Mon Sep 17 00:00:00 2001 From: Austin DeNoble Date: Sun, 16 Nov 2025 17:44:14 -0500 Subject: [PATCH 07/18] encapsulate logic for establishing and IndexConnection with a name and namespace --- internal/pkg/cli/command/index/fetch.go | 23 +++++--------------- internal/pkg/cli/command/index/upsert.go | 27 ++++++------------------ 2 files changed, 12 insertions(+), 38 deletions(-) diff --git a/internal/pkg/cli/command/index/fetch.go b/internal/pkg/cli/command/index/fetch.go index 34bcc74..aaa2dbd 100644 --- a/internal/pkg/cli/command/index/fetch.go +++ b/internal/pkg/cli/command/index/fetch.go @@ -9,9 +9,7 @@ import ( "github.com/pinecone-io/cli/internal/pkg/utils/pcio" "github.com/pinecone-io/cli/internal/pkg/utils/presenters" "github.com/pinecone-io/cli/internal/pkg/utils/sdk" - "github.com/pinecone-io/cli/internal/pkg/utils/style" "github.com/pinecone-io/cli/internal/pkg/utils/text" - "github.com/pinecone-io/go-pinecone/v5/pinecone" "github.com/spf13/cobra" ) @@ -48,20 +46,6 @@ func NewFetchCmd() *cobra.Command { func runFetchCmd(ctx context.Context, options fetchCmdOptions) { pc := sdk.NewPineconeClient() - index, err := pc.DescribeIndex(ctx, options.name) - if err != nil { - msg.FailMsg("Failed to describe index %s: %s", style.Emphasis(options.name), err) - exit.Error(err, "Failed to describe index") - } - - ic, err := pc.Index(pinecone.NewIndexConnParams{ - Host: index.Host, - }) - if err != nil { - msg.FailMsg("Failed to create index connection: %s", err) - exit.Error(err, "Failed to create index connection") - } - // Default namespace ns := options.namespace if options.namespace != "" { @@ -70,8 +54,11 @@ func runFetchCmd(ctx context.Context, options fetchCmdOptions) { if ns == "" { ns = "__default__" } - if ns != ic.Namespace() { - ic = ic.WithNamespace(ns) + + ic, err := sdk.NewIndexConnection(ctx, pc, options.name, ns) + if err != nil { + msg.FailMsg("Failed to create index connection: %s", err) + exit.Error(err, "Failed to create index connection") } vectors, err := ic.FetchVectors(ctx, options.ids) diff --git a/internal/pkg/cli/command/index/upsert.go b/internal/pkg/cli/command/index/upsert.go index 78605c7..02f67bd 100644 --- a/internal/pkg/cli/command/index/upsert.go +++ b/internal/pkg/cli/command/index/upsert.go @@ -83,10 +83,16 @@ func runUpsertCmd(cmd *cobra.Command, options upsertCmdOptions) { if options.namespace != "" { ns = options.namespace } - // Default if no namespace provided if ns == "" { ns = "__default__" } + // Get IndexConnection + pc := sdk.NewPineconeClient() + ic, err := sdk.NewIndexConnection(cmd.Context(), pc, options.name, ns) + if err != nil { + msg.FailMsg("Failed to create index connection: %s", err) + exit.Error(err, "Failed to create index connection") + } if len(payload.Vectors) == 0 { msg.FailMsg("No vectors found in %s", style.Emphasis(filePath)) @@ -118,25 +124,6 @@ func runUpsertCmd(cmd *cobra.Command, options upsertCmdOptions) { mapped = append(mapped, &vector) } - // Get Pinecone client - pc := sdk.NewPineconeClient() - // TODO - Refactor this into an all-in-one function in sdk package - // Get index and establish IndexConnection - index, err := pc.DescribeIndex(cmd.Context(), options.name) - if err != nil { - msg.FailMsg("Failed to describe index %s: %s", style.Emphasis(options.name), err) - exit.Error(err, "Failed to describe index") - } - - ic, err := pc.Index(pinecone.NewIndexConnParams{ - Host: index.Host, - }) - if err != nil { - msg.FailMsg("Failed to create index connection: %s", err) - exit.Error(err, "Failed to create index connection") - } - // TODO - Isolate all of this ^^^^^^^^^^^^^^^^ - batchSize := 1000 batches := make([][]*pinecone.Vector, 0, (len(mapped)+batchSize-1)/batchSize) for i := 0; i < len(mapped); i += batchSize { From 023d8b553a6d42ed12cb7b4419f132d0be63f2bc Mon Sep 17 00:00:00 2001 From: Austin DeNoble Date: Tue, 18 Nov 2025 11:28:01 -0500 Subject: [PATCH 08/18] add fetch-by-metadata functionality to the fetch command, unify presenter --- internal/pkg/cli/command/index/fetch.go | 96 ++++++++++++++++--- internal/pkg/cli/command/index/upsert.go | 11 ++- .../pkg/utils/presenters/fetch_vectors.go | 42 ++++++-- 3 files changed, 125 insertions(+), 24 deletions(-) diff --git a/internal/pkg/cli/command/index/fetch.go b/internal/pkg/cli/command/index/fetch.go index aaa2dbd..fd194ce 100644 --- a/internal/pkg/cli/command/index/fetch.go +++ b/internal/pkg/cli/command/index/fetch.go @@ -2,6 +2,8 @@ package index import ( "context" + "encoding/json" + "os" "github.com/pinecone-io/cli/internal/pkg/utils/exit" "github.com/pinecone-io/cli/internal/pkg/utils/help" @@ -9,24 +11,32 @@ import ( "github.com/pinecone-io/cli/internal/pkg/utils/pcio" "github.com/pinecone-io/cli/internal/pkg/utils/presenters" "github.com/pinecone-io/cli/internal/pkg/utils/sdk" + "github.com/pinecone-io/cli/internal/pkg/utils/style" "github.com/pinecone-io/cli/internal/pkg/utils/text" + "github.com/pinecone-io/go-pinecone/v5/pinecone" "github.com/spf13/cobra" ) type fetchCmdOptions struct { - name string - namespace string - ids []string - json bool + name string + namespace string + ids []string + filter string + filterFile string + limit uint32 + paginationToken string + json bool } func NewFetchCmd() *cobra.Command { options := fetchCmdOptions{} cmd := &cobra.Command{ Use: "fetch", - Short: "Fetch vectors by ID from an index", + Short: "Fetch vectors by ID or metadata filter from an index", Example: help.Examples(` - pc index fetch --name my-index --ids 123, 456, 789 + pc index fetch --name my-index --ids 123,456,789 + pc index fetch --name my-index --filter '{"key": "value"}' + pc index fetch --name my-index --filter-file ./filter.json `), Run: func(cmd *cobra.Command, args []string) { runFetchCmd(cmd.Context(), options) @@ -34,17 +44,22 @@ func NewFetchCmd() *cobra.Command { } cmd.Flags().StringSliceVarP(&options.ids, "ids", "i", []string{}, "IDs of vectors to fetch") + cmd.Flags().StringVarP(&options.filter, "filter", "f", "", "metadata filter to apply to the fetch") + cmd.Flags().StringVarP(&options.filterFile, "filter-file", "F", "", "file containing metadata filter to apply to the fetch") cmd.Flags().StringVarP(&options.name, "name", "n", "", "name of the index to fetch from") cmd.Flags().StringVar(&options.namespace, "namespace", "", "namespace to fetch from") + cmd.Flags().Uint32VarP(&options.limit, "limit", "l", 0, "maximum number of vectors to fetch") + cmd.Flags().StringVarP(&options.paginationToken, "pagination-token", "p", "", "pagination token to continue a previous listing operation") cmd.Flags().BoolVarP(&options.json, "json", "j", false, "output as JSON") + + cmd.MarkFlagsMutuallyExclusive("ids", "filter", "filter-file") _ = cmd.MarkFlagRequired("name") - _ = cmd.MarkFlagRequired("ids") return cmd } func runFetchCmd(ctx context.Context, options fetchCmdOptions) { - pc := sdk.NewPineconeClient() + pc := sdk.NewPineconeClient(ctx) // Default namespace ns := options.namespace @@ -55,21 +70,76 @@ func runFetchCmd(ctx context.Context, options fetchCmdOptions) { ns = "__default__" } + if options.filter != "" || options.filterFile != "" && (options.limit > 0 || options.paginationToken != "") { + msg.FailMsg("Filter and limit/pagination token cannot be used together") + exit.ErrorMsg("Filter and limit/pagination token cannot be used together") + } + if options.filter != "" && options.filterFile != "" { + msg.FailMsg("Filter and filter file cannot be used together") + exit.ErrorMsg("Filter and filter file cannot be used together") + } + ic, err := sdk.NewIndexConnection(ctx, pc, options.name, ns) if err != nil { msg.FailMsg("Failed to create index connection: %s", err) exit.Error(err, "Failed to create index connection") } - vectors, err := ic.FetchVectors(ctx, options.ids) - if err != nil { - exit.Error(err, "Failed to fetch vectors") + if len(options.ids) > 0 { + vectors, err := ic.FetchVectors(ctx, options.ids) + if err != nil { + exit.Error(err, "Failed to fetch vectors") + } + printFetchVectorsResults(presenters.NewFetchVectorsResultsFromFetch(vectors), options) + } + + if options.filter != "" || options.filterFile != "" { + if options.filterFile != "" { + raw, err := os.ReadFile(options.filterFile) + if err != nil { + msg.FailMsg("Failed to read filter file %s: %s", style.Emphasis(options.filterFile), err) + exit.Errorf(err, "Failed to read filter file %s", options.filterFile) + } + options.filter = string(raw) + } + + var filterMap map[string]any + + if err := json.Unmarshal([]byte(options.filter), &filterMap); err != nil { + msg.FailMsg("Failed to parse filter: %s", err) + exit.Errorf(err, "Failed to parse filter") + } + + filter, err := pinecone.NewMetadataFilter(filterMap) + if err != nil { + msg.FailMsg("Failed to create filter: %s", err) + exit.Errorf(err, "Failed to create filter") + } + + req := &pinecone.FetchVectorsByMetadataRequest{ + Filter: filter, + } + + if options.limit > 0 { + req.Limit = &options.limit + } + if options.paginationToken != "" { + req.PaginationToken = &options.paginationToken + } + + vectors, err := ic.FetchVectorsByMetadata(ctx, req) + if err != nil { + exit.Error(err, "Failed to fetch vectors by metadata") + } + printFetchVectorsResults(presenters.NewFetchVectorsResultsFromFetchByMetadata(vectors), options) } +} +func printFetchVectorsResults(results *presenters.FetchVectorsResults, options fetchCmdOptions) { if options.json { - json := text.IndentJSON(vectors) + json := text.IndentJSON(results) pcio.Println(json) } else { - presenters.PrintFetchVectorsTable(vectors) + presenters.PrintFetchVectorsTable(results) } } diff --git a/internal/pkg/cli/command/index/upsert.go b/internal/pkg/cli/command/index/upsert.go index 02f67bd..9d10412 100644 --- a/internal/pkg/cli/command/index/upsert.go +++ b/internal/pkg/cli/command/index/upsert.go @@ -1,6 +1,7 @@ package index import ( + "context" "encoding/json" "os" @@ -50,7 +51,7 @@ func NewUpsertCmd() *cobra.Command { pc index upsert --name my-index --namespace my-namespace ./vectors.json `), Run: func(cmd *cobra.Command, args []string) { - runUpsertCmd(cmd, options) + runUpsertCmd(cmd.Context(), options) }, } @@ -64,7 +65,7 @@ func NewUpsertCmd() *cobra.Command { return cmd } -func runUpsertCmd(cmd *cobra.Command, options upsertCmdOptions) { +func runUpsertCmd(ctx context.Context, options upsertCmdOptions) { filePath := options.file raw, err := os.ReadFile(filePath) if err != nil { @@ -87,8 +88,8 @@ func runUpsertCmd(cmd *cobra.Command, options upsertCmdOptions) { ns = "__default__" } // Get IndexConnection - pc := sdk.NewPineconeClient() - ic, err := sdk.NewIndexConnection(cmd.Context(), pc, options.name, ns) + pc := sdk.NewPineconeClient(ctx) + ic, err := sdk.NewIndexConnection(ctx, pc, options.name, ns) if err != nil { msg.FailMsg("Failed to create index connection: %s", err) exit.Error(err, "Failed to create index connection") @@ -135,7 +136,7 @@ func runUpsertCmd(cmd *cobra.Command, options upsertCmdOptions) { } for i, batch := range batches { - resp, err := ic.UpsertVectors(cmd.Context(), batch) + resp, err := ic.UpsertVectors(ctx, batch) if err != nil { msg.FailMsg("Failed to upsert %d vectors in batch %d: %s", len(batch), i+1, err) exit.Errorf(err, "Failed to upsert %d vectors in batch %d", len(batch), i+1) diff --git a/internal/pkg/utils/presenters/fetch_vectors.go b/internal/pkg/utils/presenters/fetch_vectors.go index ce2bee9..82831cf 100644 --- a/internal/pkg/utils/presenters/fetch_vectors.go +++ b/internal/pkg/utils/presenters/fetch_vectors.go @@ -8,15 +8,45 @@ import ( "github.com/pinecone-io/go-pinecone/v5/pinecone" ) -func PrintFetchVectorsTable(resp *pinecone.FetchVectorsResponse) { +type FetchVectorsResults struct { + Vectors map[string]*pinecone.Vector `json:"vectors,omitempty"` + Namespace string `json:"namespace"` + Usage *pinecone.Usage `json:"usage,omitempty"` + Pagination *pinecone.Pagination `json:"pagination,omitempty"` +} + +func NewFetchVectorsResultsFromFetch(resp *pinecone.FetchVectorsResponse) *FetchVectorsResults { + if resp == nil { + return &FetchVectorsResults{} + } + return &FetchVectorsResults{ + Vectors: resp.Vectors, + Namespace: resp.Namespace, + Usage: resp.Usage, + } +} + +func NewFetchVectorsResultsFromFetchByMetadata(resp *pinecone.FetchVectorsByMetadataResponse) *FetchVectorsResults { + if resp == nil { + return &FetchVectorsResults{} + } + return &FetchVectorsResults{ + Vectors: resp.Vectors, + Namespace: resp.Namespace, + Usage: resp.Usage, + Pagination: resp.Pagination, + } +} + +func PrintFetchVectorsTable(results *FetchVectorsResults) { writer := NewTabWriter() // Header Block - if resp.Namespace != "" { - pcio.Fprintf(writer, "Namespace: %s\n", resp.Namespace) + if results.Namespace != "" { + pcio.Fprintf(writer, "Namespace: %s\n", results.Namespace) } - if resp.Usage != nil { - pcio.Fprintf(writer, "Usage: %d (read units)\n", resp.Usage.ReadUnits) + if results.Usage != nil { + pcio.Fprintf(writer, "Usage: %d (read units)\n", results.Usage.ReadUnits) } // Table Header @@ -24,7 +54,7 @@ func PrintFetchVectorsTable(resp *pinecone.FetchVectorsResponse) { pcio.Fprintln(writer, strings.Join(columns, "\t")) // Rows - for id, vector := range resp.Vectors { + for id, vector := range results.Vectors { dim := 0 if vector.Values != nil { dim = len(*vector.Values) From d1294738118ed3b2d7c7acef864203e9e8204f67 Mon Sep 17 00:00:00 2001 From: Austin DeNoble Date: Wed, 19 Nov 2025 15:23:42 -0500 Subject: [PATCH 09/18] add list-vectors and query commands + presentation logic --- internal/pkg/cli/command/index/cmd.go | 2 + .../pkg/cli/command/index/list_vectors.go | 92 ++++++++++ internal/pkg/cli/command/index/query.go | 170 ++++++++++++++++++ .../pkg/utils/presenters/fetch_vectors.go | 4 +- internal/pkg/utils/presenters/list_vectors.go | 40 +++++ .../pkg/utils/presenters/query_vectors.go | 66 +++++++ 6 files changed, 372 insertions(+), 2 deletions(-) create mode 100644 internal/pkg/cli/command/index/list_vectors.go create mode 100644 internal/pkg/cli/command/index/query.go create mode 100644 internal/pkg/utils/presenters/list_vectors.go create mode 100644 internal/pkg/utils/presenters/query_vectors.go diff --git a/internal/pkg/cli/command/index/cmd.go b/internal/pkg/cli/command/index/cmd.go index 8b8d10c..9cbe91b 100644 --- a/internal/pkg/cli/command/index/cmd.go +++ b/internal/pkg/cli/command/index/cmd.go @@ -41,6 +41,8 @@ func NewIndexCmd() *cobra.Command { cmd.AddCommand(NewDeleteCmd()) cmd.AddCommand(NewUpsertCmd()) cmd.AddCommand(NewFetchCmd()) + cmd.AddCommand(NewQueryCmd()) + cmd.AddCommand(NewListVectorsCmd()) return cmd } diff --git a/internal/pkg/cli/command/index/list_vectors.go b/internal/pkg/cli/command/index/list_vectors.go new file mode 100644 index 0000000..3afcfed --- /dev/null +++ b/internal/pkg/cli/command/index/list_vectors.go @@ -0,0 +1,92 @@ +package index + +import ( + "context" + + "github.com/pinecone-io/cli/internal/pkg/utils/exit" + "github.com/pinecone-io/cli/internal/pkg/utils/help" + "github.com/pinecone-io/cli/internal/pkg/utils/msg" + "github.com/pinecone-io/cli/internal/pkg/utils/pcio" + "github.com/pinecone-io/cli/internal/pkg/utils/presenters" + "github.com/pinecone-io/cli/internal/pkg/utils/sdk" + "github.com/pinecone-io/cli/internal/pkg/utils/text" + "github.com/pinecone-io/go-pinecone/v5/pinecone" + "github.com/spf13/cobra" +) + +type listVectorsCmdOptions struct { + name string + namespace string + limit uint32 + paginationToken string + json bool +} + +func NewListVectorsCmd() *cobra.Command { + options := listVectorsCmdOptions{} + cmd := &cobra.Command{ + Use: "list-vectors", + Short: "List vectors in an index", + Example: help.Examples(` + pc index list-vectors --name my-index --namespace my-namespace + `), + Run: func(cmd *cobra.Command, args []string) { + runListVectorsCmd(cmd.Context(), options) + }, + } + + cmd.Flags().StringVarP(&options.name, "name", "n", "", "name of the index to list vectors from") + cmd.Flags().StringVar(&options.namespace, "namespace", "", "namespace to list vectors from") + cmd.Flags().Uint32VarP(&options.limit, "limit", "l", 0, "maximum number of vectors to list") + cmd.Flags().StringVarP(&options.paginationToken, "pagination-token", "p", "", "pagination token to continue a previous listing operation") + cmd.Flags().BoolVar(&options.json, "json", false, "output as JSON") + + _ = cmd.MarkFlagRequired("name") + + return cmd +} + +func runListVectorsCmd(ctx context.Context, options listVectorsCmdOptions) { + pc := sdk.NewPineconeClient(ctx) + + // Default namespace + ns := options.namespace + if options.namespace != "" { + ns = options.namespace + } + if ns == "" { + ns = "__default__" + } + + // Get IndexConnection + ic, err := sdk.NewIndexConnection(ctx, pc, options.name, ns) + if err != nil { + msg.FailMsg("Failed to create index connection: %s", err) + exit.Error(err, "Failed to create index connection") + } + + var limit *uint32 + if options.limit > 0 { + limit = &options.limit + } + var paginationToken *string + if options.paginationToken != "" { + paginationToken = &options.paginationToken + } + + resp, err := ic.ListVectors(ctx, &pinecone.ListVectorsRequest{ + Limit: limit, + PaginationToken: paginationToken, + }) + if err != nil { + msg.FailMsg("Failed to list vectors: %s", err) + exit.Error(err, "Failed to list vectors") + } + + if options.json { + json := text.IndentJSON(resp) + pcio.Println(json) + } else { + presenters.PrintListVectorsTable(resp) + } +} diff --git a/internal/pkg/cli/command/index/query.go b/internal/pkg/cli/command/index/query.go new file mode 100644 index 0000000..ba7ae7e --- /dev/null +++ b/internal/pkg/cli/command/index/query.go @@ -0,0 +1,170 @@ +package index + +import ( + "context" + "encoding/json" + "os" + + "github.com/pinecone-io/cli/internal/pkg/utils/exit" + "github.com/pinecone-io/cli/internal/pkg/utils/help" + "github.com/pinecone-io/cli/internal/pkg/utils/msg" + "github.com/pinecone-io/cli/internal/pkg/utils/pcio" + "github.com/pinecone-io/cli/internal/pkg/utils/presenters" + "github.com/pinecone-io/cli/internal/pkg/utils/sdk" + "github.com/pinecone-io/cli/internal/pkg/utils/style" + "github.com/pinecone-io/cli/internal/pkg/utils/text" + "github.com/pinecone-io/go-pinecone/v5/pinecone" + "github.com/spf13/cobra" +) + +type queryCmdOptions struct { + id string + vector []float32 + sparseIndices []int32 + sparseValues []float32 + name string + namespace string + topK uint32 + filter string + filterFile string + includeValues bool + includeMetadata bool + json bool +} + +func NewQueryCmd() *cobra.Command { + options := queryCmdOptions{} + cmd := &cobra.Command{ + Use: "query", + Short: "Query an index by vector values", + Example: help.Examples(` + + `), + Run: func(cmd *cobra.Command, args []string) { + runQueryCmd(cmd.Context(), options) + }, + } + + cmd.Flags().StringVarP(&options.name, "name", "n", "", "name of the index to query") + cmd.Flags().StringVar(&options.namespace, "namespace", "", "index namespace to query") + cmd.Flags().Uint32VarP(&options.topK, "top-k", "k", 10, "maximum number of results to return") + cmd.Flags().StringVarP(&options.filter, "filter", "f", "", "metadata filter to apply to the query") + cmd.Flags().StringVar(&options.filterFile, "filter-file", "", "file containing metadata filter to apply to the query") + cmd.Flags().BoolVar(&options.includeValues, "include-values", false, "include vector values in the query results") + cmd.Flags().BoolVar(&options.includeMetadata, "include-metadata", false, "include metadata in the query results") + cmd.Flags().StringVarP(&options.id, "id", "i", "", "ID of the vector to query against") + cmd.Flags().Float32SliceVarP(&options.vector, "vector", "v", []float32{}, "vector values to query against") + cmd.Flags().Int32SliceVarP(&options.sparseIndices, "sparse-indices", "i", []int32{}, "sparse indices to query against") + cmd.Flags().Float32SliceVarP(&options.sparseValues, "sparse-values", "v", []float32{}, "sparse values to query against") + cmd.Flags().BoolVar(&options.json, "json", false, "output as JSON") + + _ = cmd.MarkFlagRequired("name") + cmd.MarkFlagsMutuallyExclusive("id", "vector", "sparse-values") + + return cmd +} + +func runQueryCmd(ctx context.Context, options queryCmdOptions) { + pc := sdk.NewPineconeClient(ctx) + + // Default namespace + ns := options.namespace + if options.namespace != "" { + ns = options.namespace + } + if ns == "" { + ns = "__default__" + } + + // Get IndexConnection + ic, err := sdk.NewIndexConnection(ctx, pc, options.name, ns) + if err != nil { + msg.FailMsg("Failed to create index connection: %s", err) + exit.Error(err, "Failed to create index connection") + } + + var queryResponse *pinecone.QueryVectorsResponse + + // Build metadata filter if provided + var filter *pinecone.MetadataFilter + if options.filter != "" || options.filterFile != "" { + if options.filterFile != "" { + raw, err := os.ReadFile(options.filterFile) + if err != nil { + msg.FailMsg("Failed to read filter file %s: %s", style.Emphasis(options.filterFile), err) + exit.Errorf(err, "Failed to read filter file %s", options.filterFile) + } + options.filter = string(raw) + } + + var filterMap map[string]any + if err := json.Unmarshal([]byte(options.filter), &filterMap); err != nil { + msg.FailMsg("Failed to parse filter: %s", err) + exit.Errorf(err, "Failed to parse filter") + } + filter, err = pinecone.NewMetadataFilter(filterMap) + if err != nil { + msg.FailMsg("Failed to create filter: %s", err) + exit.Errorf(err, "Failed to create filter") + } + } + + // Query by vector ID + if options.id != "" { + req := &pinecone.QueryByVectorIdRequest{ + VectorId: options.id, + TopK: options.topK, + IncludeValues: options.includeValues, + IncludeMetadata: options.includeMetadata, + MetadataFilter: filter, + } + + queryResponse, err = ic.QueryByVectorId(ctx, req) + if err != nil { + exit.Error(err, "Failed to query by vector ID") + } + } + + // Query by vector values + if len(options.vector) > 0 || len(options.sparseIndices) > 0 || len(options.sparseValues) > 0 { + sparseIndices, err := toUint32Slice(options.sparseIndices) + if err != nil { + exit.Error(err, "Failed to convert sparse indices to uint32") + } + + req := &pinecone.QueryByVectorValuesRequest{ + Vector: options.vector, + SparseValues: &pinecone.SparseValues{ + Indices: sparseIndices, + Values: options.sparseValues, + }, + TopK: options.topK, + IncludeValues: options.includeValues, + IncludeMetadata: options.includeMetadata, + MetadataFilter: filter, + } + + queryResponse, err = ic.QueryByVectorValues(ctx, req) + if err != nil { + exit.Error(err, "Failed to query by vector values") + } + } + + if options.json { + json := text.IndentJSON(queryResponse) + pcio.Println(json) + } else { + presenters.PrintQueryVectorsTable(queryResponse) + } +} + +func toUint32Slice(in []int32) ([]uint32, error) { + out := make([]uint32, len(in)) + for i, v := range in { + if v < 0 { + return nil, pcio.Errorf("sparse indices must be non-negative") + } + out[i] = uint32(v) + } + return out, nil +} diff --git a/internal/pkg/utils/presenters/fetch_vectors.go b/internal/pkg/utils/presenters/fetch_vectors.go index 82831cf..98e0d48 100644 --- a/internal/pkg/utils/presenters/fetch_vectors.go +++ b/internal/pkg/utils/presenters/fetch_vectors.go @@ -67,7 +67,7 @@ func PrintFetchVectorsTable(results *FetchVectorsResults) { if vector.Metadata != nil { metadata = text.InlineJSON(vector.Metadata) } - preview := previewSlice(vector.Values, 3) + preview := previewSliceFloat32(vector.Values, 3) row := []string{id, pcio.Sprintf("%d", dim), preview, pcio.Sprintf("%d", sparseDim), metadata} pcio.Fprintln(writer, strings.Join(row, "\t")) } @@ -75,7 +75,7 @@ func PrintFetchVectorsTable(results *FetchVectorsResults) { writer.Flush() } -func previewSlice(values *[]float32, limit int) string { +func previewSliceFloat32(values *[]float32, limit int) string { if values == nil || len(*values) == 0 { return "" } diff --git a/internal/pkg/utils/presenters/list_vectors.go b/internal/pkg/utils/presenters/list_vectors.go new file mode 100644 index 0000000..2c403e0 --- /dev/null +++ b/internal/pkg/utils/presenters/list_vectors.go @@ -0,0 +1,40 @@ +package presenters + +import ( + "strings" + + "github.com/pinecone-io/cli/internal/pkg/utils/pcio" + "github.com/pinecone-io/go-pinecone/v5/pinecone" +) + +func PrintListVectorsTable(resp *pinecone.ListVectorsResponse) { + writer := NewTabWriter() + + // Header block + if resp.Namespace != "" { + pcio.Fprintf(writer, "Namespace: %s\n", resp.Namespace) + } + if resp.Usage != nil { + pcio.Fprintf(writer, "Usage: %d (read units)\n", resp.Usage.ReadUnits) + } + + // Table header + columns := []string{"ID"} + pcio.Fprintln(writer, strings.Join(columns, "\t")) + + // Rows + for _, vectorId := range resp.VectorIds { + id := "" + if vectorId != nil { + id = *vectorId + } + pcio.Fprintln(writer, id) + } + + // Pagination footer + if resp.NextPaginationToken != nil { + pcio.Fprintf(writer, "Next pagination token: %s\n", *resp.NextPaginationToken) + } + + writer.Flush() +} diff --git a/internal/pkg/utils/presenters/query_vectors.go b/internal/pkg/utils/presenters/query_vectors.go new file mode 100644 index 0000000..60b3da5 --- /dev/null +++ b/internal/pkg/utils/presenters/query_vectors.go @@ -0,0 +1,66 @@ +package presenters + +import ( + "strings" + + "github.com/pinecone-io/cli/internal/pkg/utils/pcio" + "github.com/pinecone-io/cli/internal/pkg/utils/text" + "github.com/pinecone-io/go-pinecone/v5/pinecone" +) + +func PrintQueryVectorsTable(resp *pinecone.QueryVectorsResponse) { + writer := NewTabWriter() + + // Header Block + if resp.Namespace != "" { + pcio.Fprintf(writer, "Namespace: %s\n", resp.Namespace) + } + if resp.Usage != nil { + pcio.Fprintf(writer, "Usage: %d (read units)\n", resp.Usage.ReadUnits) + } + + // Table Header + columns := []string{"ID", "SCORE", "VALUES", "SPARSE INDICES", "SPARSE VALUES", "METADATA"} + pcio.Fprintln(writer, strings.Join(columns, "\t")) + + // Rows + for _, match := range resp.Matches { + if match == nil || match.Vector == nil { + continue + } + + valuesPreview := "" + if match.Vector.Values != nil { + valuesPreview = previewSliceFloat32(match.Vector.Values, 3) + } + + sparseIndicesPreview := "" + if match.Vector.SparseValues != nil { + sparseIndicesPreview = previewSliceUint32(match.Vector.SparseValues.Indices, 3) + } + + sparseValuesPreview := "" + if match.Vector.SparseValues != nil { + sparseValuesPreview = previewSliceFloat32(&match.Vector.SparseValues.Values, 3) + } + + metadataPreview := "" + if match.Vector.Metadata != nil { + metadataPreview = text.InlineJSON(match.Vector.Metadata) + } + + row := []string{match.Vector.Id, pcio.Sprintf("%f", match.Score), valuesPreview, sparseIndicesPreview, sparseValuesPreview, metadataPreview} + pcio.Fprintln(writer, strings.Join(row, "\t")) + } +} + +func previewSliceUint32(values []uint32, limit int) string { + if len(values) == 0 { + return "" + } + vals := values + if len(vals) > limit { + vals = vals[:limit] + } + return text.InlineJSON(vals) + "..." +} From a8081520c0f035c2aadeef84a76de5dba6cc9385 Mon Sep 17 00:00:00 2001 From: Austin DeNoble Date: Wed, 19 Nov 2025 15:42:27 -0500 Subject: [PATCH 10/18] remove duplicate varP flags --- internal/pkg/cli/command/index/query.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/pkg/cli/command/index/query.go b/internal/pkg/cli/command/index/query.go index ba7ae7e..6359acb 100644 --- a/internal/pkg/cli/command/index/query.go +++ b/internal/pkg/cli/command/index/query.go @@ -54,8 +54,8 @@ func NewQueryCmd() *cobra.Command { cmd.Flags().BoolVar(&options.includeMetadata, "include-metadata", false, "include metadata in the query results") cmd.Flags().StringVarP(&options.id, "id", "i", "", "ID of the vector to query against") cmd.Flags().Float32SliceVarP(&options.vector, "vector", "v", []float32{}, "vector values to query against") - cmd.Flags().Int32SliceVarP(&options.sparseIndices, "sparse-indices", "i", []int32{}, "sparse indices to query against") - cmd.Flags().Float32SliceVarP(&options.sparseValues, "sparse-values", "v", []float32{}, "sparse values to query against") + cmd.Flags().Int32SliceVar(&options.sparseIndices, "sparse-indices", []int32{}, "sparse indices to query against") + cmd.Flags().Float32SliceVar(&options.sparseValues, "sparse-values", []float32{}, "sparse values to query against") cmd.Flags().BoolVar(&options.json, "json", false, "output as JSON") _ = cmd.MarkFlagRequired("name") From ebc786f387fee91402626d28c71cee2f3e4642c2 Mon Sep 17 00:00:00 2001 From: Austin DeNoble Date: Wed, 19 Nov 2025 16:03:03 -0500 Subject: [PATCH 11/18] add index describe-stats command and presenter --- .../pkg/cli/command/index/describe_stats.go | 93 +++++++++++++++++++ .../utils/presenters/describe_index_stats.go | 27 ++++++ 2 files changed, 120 insertions(+) create mode 100644 internal/pkg/cli/command/index/describe_stats.go create mode 100644 internal/pkg/utils/presenters/describe_index_stats.go diff --git a/internal/pkg/cli/command/index/describe_stats.go b/internal/pkg/cli/command/index/describe_stats.go new file mode 100644 index 0000000..81e0362 --- /dev/null +++ b/internal/pkg/cli/command/index/describe_stats.go @@ -0,0 +1,93 @@ +package index + +import ( + "context" + "encoding/json" + "os" + + "github.com/pinecone-io/cli/internal/pkg/utils/exit" + "github.com/pinecone-io/cli/internal/pkg/utils/help" + "github.com/pinecone-io/cli/internal/pkg/utils/msg" + "github.com/pinecone-io/cli/internal/pkg/utils/pcio" + "github.com/pinecone-io/cli/internal/pkg/utils/presenters" + "github.com/pinecone-io/cli/internal/pkg/utils/sdk" + "github.com/pinecone-io/cli/internal/pkg/utils/style" + "github.com/pinecone-io/cli/internal/pkg/utils/text" + "github.com/pinecone-io/go-pinecone/v5/pinecone" + "github.com/spf13/cobra" +) + +type describeStatsCmdOptions struct { + name string + filter string + filterFile string + json bool +} + +func NewDescribeIndexStatsCmd() *cobra.Command { + options := describeStatsCmdOptions{} + cmd := &cobra.Command{ + Use: "describe-stats", + Short: "Describe the stats of an index", + Example: help.Examples(` + pc index describe-stats --name "index-name" + `), + Run: func(cmd *cobra.Command, args []string) { + runDescribeIndexStatsCmd(cmd.Context(), options) + }, + } + + cmd.Flags().StringVarP(&options.name, "name", "n", "", "name of index to describe stats for") + cmd.Flags().StringVar(&options.filter, "filter", "", "filter to apply to the stats") + cmd.Flags().BoolVar(&options.json, "json", false, "output as JSON") + _ = cmd.MarkFlagRequired("name") + + return cmd +} + +func runDescribeIndexStatsCmd(ctx context.Context, options describeStatsCmdOptions) { + pc := sdk.NewPineconeClient(ctx) + + ic, err := sdk.NewIndexConnection(ctx, pc, options.name, "") + if err != nil { + msg.FailMsg("Failed to create index connection: %s", err) + exit.Error(err, "Failed to create index connection") + } + + // Build metadata filter if provided + var filter *pinecone.MetadataFilter + if options.filter != "" || options.filterFile != "" { + if options.filterFile != "" { + raw, err := os.ReadFile(options.filterFile) + if err != nil { + msg.FailMsg("Failed to read filter file %s: %s", style.Emphasis(options.filterFile), err) + exit.Errorf(err, "Failed to read filter file %s", options.filterFile) + } + options.filter = string(raw) + } + + var filterMap map[string]any + if err := json.Unmarshal([]byte(options.filter), &filterMap); err != nil { + msg.FailMsg("Failed to parse filter: %s", err) + exit.Errorf(err, "Failed to parse filter") + } + filter, err = pinecone.NewMetadataFilter(filterMap) + if err != nil { + msg.FailMsg("Failed to create filter: %s", err) + exit.Errorf(err, "Failed to create filter") + } + } + + resp, err := ic.DescribeIndexStatsFiltered(ctx, filter) + if err != nil { + msg.FailMsg("Failed to describe stats: %s", err) + exit.Error(err, "Failed to describe stats") + } + + if options.json { + json := text.IndentJSON(resp) + pcio.Println(json) + } else { + presenters.PrintDescribeIndexStatsTable(resp) + } +} diff --git a/internal/pkg/utils/presenters/describe_index_stats.go b/internal/pkg/utils/presenters/describe_index_stats.go new file mode 100644 index 0000000..929e35d --- /dev/null +++ b/internal/pkg/utils/presenters/describe_index_stats.go @@ -0,0 +1,27 @@ +package presenters + +import ( + "strings" + + "github.com/pinecone-io/cli/internal/pkg/utils/pcio" + "github.com/pinecone-io/cli/internal/pkg/utils/text" + "github.com/pinecone-io/go-pinecone/v5/pinecone" +) + +func PrintDescribeIndexStatsTable(resp *pinecone.DescribeIndexStatsResponse) { + writer := NewTabWriter() + + columns := []string{"ATTRIBUTE", "VALUE"} + header := strings.Join(columns, "\t") + "\n" + pcio.Fprint(writer, header) + + pcio.Fprintf(writer, "Dimension\t%d\n", resp.Dimension) + pcio.Fprintf(writer, "Index Fullness\t%f\n", resp.IndexFullness) + pcio.Fprintf(writer, "Total Vector Count\t%d\n", resp.TotalVectorCount) + + formatted := text.IndentJSON(resp.Namespaces) + formatted = strings.ReplaceAll(formatted, "\n", "\n\t") // indent lines under the namespace value + pcio.Fprintf(writer, "Namespaces\t%s\n", formatted) + + writer.Flush() +} From 2600550de5af809f112821ffed0e7b44958f848d Mon Sep 17 00:00:00 2001 From: Austin DeNoble Date: Wed, 19 Nov 2025 17:35:28 -0500 Subject: [PATCH 12/18] add update-vector command to cover both by ID and metadata filter, add new JSONObject to allow better representation of JSON / map-like flag inputs, use for filter and metadata in the update-vector file --- internal/pkg/cli/command/index/update.go | 186 ++++++++++++++++++ internal/pkg/utils/flags/flags.go | 45 +++++ .../pkg/utils/presenters/update_vector.go | 20 ++ 3 files changed, 251 insertions(+) create mode 100644 internal/pkg/cli/command/index/update.go create mode 100644 internal/pkg/utils/flags/flags.go create mode 100644 internal/pkg/utils/presenters/update_vector.go diff --git a/internal/pkg/cli/command/index/update.go b/internal/pkg/cli/command/index/update.go new file mode 100644 index 0000000..39fdc6b --- /dev/null +++ b/internal/pkg/cli/command/index/update.go @@ -0,0 +1,186 @@ +package index + +import ( + "context" + "encoding/json" + "os" + + "github.com/pinecone-io/cli/internal/pkg/utils/exit" + "github.com/pinecone-io/cli/internal/pkg/utils/flags" + "github.com/pinecone-io/cli/internal/pkg/utils/help" + "github.com/pinecone-io/cli/internal/pkg/utils/msg" + "github.com/pinecone-io/cli/internal/pkg/utils/pcio" + "github.com/pinecone-io/cli/internal/pkg/utils/presenters" + "github.com/pinecone-io/cli/internal/pkg/utils/sdk" + "github.com/pinecone-io/cli/internal/pkg/utils/style" + "github.com/pinecone-io/cli/internal/pkg/utils/text" + "github.com/pinecone-io/go-pinecone/v5/pinecone" + "github.com/spf13/cobra" +) + +type updateCmdOptions struct { + name string + namespace string + id string + values []float32 + sparseIndices []int32 + sparseValues []float32 + metadata flags.JSONObject + filter flags.JSONObject + filterFile string + dryRun bool + json bool +} + +func NewUpdateCmd() *cobra.Command { + options := updateCmdOptions{} + + cmd := &cobra.Command{ + Use: "update", + Short: "Update a vector by ID, or a set of vectors by metadata filter", + Example: help.Examples(``), + Run: func(cmd *cobra.Command, args []string) { + runUpdateCmd(cmd.Context(), options) + }, + } + + cmd.Flags().StringVarP(&options.name, "name", "n", "", "name of the index to update") + cmd.Flags().StringVar(&options.namespace, "namespace", "", "namespace to update the vector in") + cmd.Flags().StringVar(&options.id, "id", "", "ID of the vector to update") + cmd.Flags().Float32SliceVar(&options.values, "values", []float32{}, "values to update the vector with") + cmd.Flags().Int32SliceVar(&options.sparseIndices, "sparse-indices", []int32{}, "sparse indices to update the vector with") + cmd.Flags().Float32SliceVar(&options.sparseValues, "sparse-values", []float32{}, "sparse values to update the vector with") + cmd.Flags().Var(&options.metadata, "metadata", "metadata to update the vector with") + cmd.Flags().Var(&options.filter, "filter", "filter to update the vectors with") + cmd.Flags().StringVar(&options.filterFile, "filter-file", "", "file containing filter to update the vectors with") + cmd.Flags().BoolVar(&options.dryRun, "dry-run", false, "do not update the vectors, just return the number of vectors that would be updated") + cmd.Flags().BoolVar(&options.json, "json", false, "output as JSON") + + _ = cmd.MarkFlagRequired("name") + + return cmd +} + +func runUpdateCmd(ctx context.Context, options updateCmdOptions) { + pc := sdk.NewPineconeClient(ctx) + + // Default namespace + ns := options.namespace + if options.namespace != "" { + ns = options.namespace + } + if ns == "" { + ns = "__default__" + } + + // Validate update by ID or metadata filter + if options.id == "" && options.filter == nil && options.filterFile == "" { + msg.FailMsg("Either --id or --filter must be provided") + exit.ErrorMsg("Either --id or --filter must be provided") + } + + ic, err := sdk.NewIndexConnection(ctx, pc, options.name, ns) + if err != nil { + msg.FailMsg("Failed to create index connection: %s", err) + exit.Error(err, "Failed to create index connection") + } + + if options.id != "" && (options.filter != nil || options.filterFile != "") { + msg.FailMsg("ID and filter cannot be used together") + exit.ErrorMsg("ID and filter cannot be used together") + } + + // Update vector by ID + if options.id != "" { + metadata, err := pinecone.NewMetadata(options.metadata) + if err != nil { + msg.FailMsg("Failed to create metadata: %s", err) + exit.Errorf(err, "Failed to create metadata") + } + + var sparseValues *pinecone.SparseValues + if len(options.sparseIndices) > 0 || len(options.sparseValues) > 0 { + sparseIndices, err := toUint32Slice(options.sparseIndices) + if err != nil { + msg.FailMsg("Failed to convert sparse indices to uint32: %s", err) + exit.Errorf(err, "Failed to convert sparse indices to uint32") + } + sparseValues = &pinecone.SparseValues{ + Indices: sparseIndices, + Values: options.sparseValues, + } + } + + err = ic.UpdateVector(ctx, &pinecone.UpdateVectorRequest{ + Id: options.id, + Values: options.values, + SparseValues: sparseValues, + Metadata: metadata, + }) + if err != nil { + msg.FailMsg("Failed to update vector ID: %s - %v", options.id, err) + exit.Errorf(err, "Failed to update vector ID: %s", options.id) + } + + if !options.json { + msg.SuccessMsg("Vector ID: %s updated successfully", options.id) + } + return + } + + // Update vectors by metadata filter + if options.filter != nil || options.filterFile != "" { + var filterMap map[string]any + + // Build metadata filter if provided + if options.filterFile != "" { + raw, err := os.ReadFile(options.filterFile) + if err != nil { + msg.FailMsg("Failed to read filter file %s: %s", style.Emphasis(options.filterFile), err) + exit.Errorf(err, "Failed to read filter file %s", options.filterFile) + } + if err := json.Unmarshal(raw, &filterMap); err != nil { + msg.FailMsg("Failed to parse filter: %s", err) + exit.Errorf(err, "Failed to parse filter") + } + } else { // Otherwise use the filter passed in as a JSON string + filterMap = options.filter + } + + filter, err := pinecone.NewMetadataFilter(filterMap) + if err != nil { + msg.FailMsg("Failed to create filter: %s", err) + exit.Errorf(err, "Failed to create filter") + } + + metadata, err := pinecone.NewMetadata(options.metadata) + if err != nil { + msg.FailMsg("Failed to create metadata: %s", err) + exit.Errorf(err, "Failed to create metadata") + } + + var dryRun *bool + if options.dryRun { + dryRun = &options.dryRun + } + + resp, err := ic.UpdateVectorsByMetadata(ctx, &pinecone.UpdateVectorsByMetadataRequest{ + Filter: filter, + Metadata: metadata, + DryRun: dryRun, + }) + if err != nil { + msg.FailMsg("Failed to update vectors by metadata: %s - %v", filter.String(), err) + exit.Errorf(err, "Failed to update vectors by metadata: %s", filter.String()) + } + + if !options.json { + msg.SuccessMsg("Updated %d vectors by metadata filter: %s", resp.MatchedRecords, filter.String()) + presenters.PrintUpdateVectorsByMetadataTable(resp) + } else { + json := text.IndentJSON(resp) + pcio.Println(json) + } + return + } +} diff --git a/internal/pkg/utils/flags/flags.go b/internal/pkg/utils/flags/flags.go new file mode 100644 index 0000000..5dc2746 --- /dev/null +++ b/internal/pkg/utils/flags/flags.go @@ -0,0 +1,45 @@ +package flags + +import ( + "encoding/json" + "os" + "strings" + + "github.com/pinecone-io/cli/internal/pkg/utils/pcio" +) + +type JSONObject map[string]any + +func (m *JSONObject) Set(value string) error { + // allow passing "@file.json" to read a file path and parse as JSON + if strings.HasPrefix(value, "@") { + filePath := strings.TrimPrefix(value, "@") + raw, err := os.ReadFile(filePath) + if err != nil { + return err + } + return json.Unmarshal(raw, m) + } + + var tmp map[string]any + if err := json.Unmarshal([]byte(value), &tmp); err != nil { + return pcio.Errorf("failed to parse JSON: %w", err) + } + if *m == nil { + *m = make(map[string]any) + } + for k, v := range tmp { + (*m)[k] = v + } + return nil +} + +func (m *JSONObject) String() string { + if m == nil || len(*m) == 0 { + return "" + } + b, _ := json.Marshal(m) + return string(b) +} + +func (*JSONObject) Type() string { return "json" } diff --git a/internal/pkg/utils/presenters/update_vector.go b/internal/pkg/utils/presenters/update_vector.go new file mode 100644 index 0000000..5c9ede9 --- /dev/null +++ b/internal/pkg/utils/presenters/update_vector.go @@ -0,0 +1,20 @@ +package presenters + +import ( + "strings" + + "github.com/pinecone-io/cli/internal/pkg/utils/pcio" + "github.com/pinecone-io/go-pinecone/v5/pinecone" +) + +func PrintUpdateVectorsByMetadataTable(resp *pinecone.UpdateVectorsByMetadataResponse) { + writer := NewTabWriter() + + columns := []string{"ATTRIBUTE", "VALUE"} + header := strings.Join(columns, "\t") + "\n" + pcio.Fprint(writer, header) + + pcio.Fprintf(writer, "Matched Records\t%d\n", resp.MatchedRecords) + + writer.Flush() +} From 55048efa02982932aac8f13452008469fbccd268 Mon Sep 17 00:00:00 2001 From: Austin DeNoble Date: Wed, 19 Nov 2025 17:50:05 -0500 Subject: [PATCH 13/18] implement index delete-vector command --- .../pkg/cli/command/index/delete_vectors.go | 134 ++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 internal/pkg/cli/command/index/delete_vectors.go diff --git a/internal/pkg/cli/command/index/delete_vectors.go b/internal/pkg/cli/command/index/delete_vectors.go new file mode 100644 index 0000000..bdda909 --- /dev/null +++ b/internal/pkg/cli/command/index/delete_vectors.go @@ -0,0 +1,134 @@ +package index + +import ( + "context" + "encoding/json" + "os" + + "github.com/pinecone-io/cli/internal/pkg/utils/exit" + "github.com/pinecone-io/cli/internal/pkg/utils/flags" + "github.com/pinecone-io/cli/internal/pkg/utils/help" + "github.com/pinecone-io/cli/internal/pkg/utils/msg" + "github.com/pinecone-io/cli/internal/pkg/utils/sdk" + "github.com/pinecone-io/cli/internal/pkg/utils/style" + "github.com/pinecone-io/go-pinecone/v5/pinecone" + "github.com/spf13/cobra" +) + +type deleteVectorsCmdOptions struct { + name string + namespace string + ids []string + filter flags.JSONObject + filterFile string + deleteAll bool + json bool +} + +func NewDeleteVectorsCmd() *cobra.Command { + options := deleteVectorsCmdOptions{} + + cmd := &cobra.Command{ + Use: "delete-vectors", + Short: "Delete vectors from an index", + Example: help.Examples(` + pc index delete-vectors --name my-index --namespace my-namespace --ids my-id + pc index delete-vectors --namespace my-namespace --all-vectors + pc index delete-vectors --namespace my-namespace --filter '{"genre": "classical"}' + `), + Run: func(cmd *cobra.Command, args []string) { + runDeleteVectorsCmd(cmd.Context(), options) + }, + } + + cmd.Flags().StringVarP(&options.name, "name", "n", "", "name of the index to delete vectors from") + cmd.Flags().StringVar(&options.namespace, "namespace", "", "namespace to delete vectors from") + cmd.Flags().StringSliceVar(&options.ids, "ids", []string{}, "IDs of the vectors to delete") + cmd.Flags().Var(&options.filter, "filter", "filter to delete the vectors with") + cmd.Flags().StringVar(&options.filterFile, "filter-file", "", "file containing filter to delete the vectors with") + cmd.Flags().BoolVar(&options.deleteAll, "all-vectors", false, "delete all vectors from the namespace") + cmd.Flags().BoolVar(&options.json, "json", false, "output as JSON") + + _ = cmd.MarkFlagRequired("name") + + return cmd +} + +func runDeleteVectorsCmd(ctx context.Context, options deleteVectorsCmdOptions) { + pc := sdk.NewPineconeClient(ctx) + + // Default namespace + ns := options.namespace + if options.namespace != "" { + ns = options.namespace + } + if ns == "" { + ns = "__default__" + } + + ic, err := sdk.NewIndexConnection(ctx, pc, options.name, ns) + if err != nil { + msg.FailMsg("Failed to create index connection: %s", err) + exit.Error(err, "Failed to create index connection") + } + + // Delete all vectors in namespace + if options.deleteAll { + err = ic.DeleteAllVectorsInNamespace(ctx) + if err != nil { + msg.FailMsg("Failed to delete all vectors in namespace: %s", err) + exit.Error(err, "Failed to delete all vectors in namespace") + } + if !options.json { + msg.SuccessMsg("Deleted all vectors in namespace: %s", ns) + } + return + } + + // Delete vectors by ID + if len(options.ids) > 0 { + err = ic.DeleteVectorsById(ctx, options.ids) + if err != nil { + msg.FailMsg("Failed to delete vectors by IDs: %s", err) + exit.Error(err, "Failed to delete vectors by IDs") + } + if !options.json { + msg.SuccessMsg("Deleted vectors by IDs: %s", options.ids) + } + return + } + + // Delete vectors by filter + if options.filter != nil || options.filterFile != "" { + var filterMap map[string]any + if options.filterFile != "" { + raw, err := os.ReadFile(options.filterFile) + if err != nil { + msg.FailMsg("Failed to read filter file %s: %s", style.Emphasis(options.filterFile), err) + exit.Errorf(err, "Failed to read filter file %s", options.filterFile) + } + if err := json.Unmarshal(raw, &filterMap); err != nil { + msg.FailMsg("Failed to parse filter: %s", err) + exit.Errorf(err, "Failed to parse filter") + } + } else { + filterMap = options.filter + } + + filter, err := pinecone.NewMetadataFilter(filterMap) + if err != nil { + msg.FailMsg("Failed to create filter: %s", err) + exit.Errorf(err, "Failed to create filter") + } + + err = ic.DeleteVectorsByFilter(ctx, filter) + if err != nil { + msg.FailMsg("Failed to delete vectors by filter: %s", err) + exit.Error(err, "Failed to delete vectors by filter") + } + if !options.json { + msg.SuccessMsg("Deleted vectors by filter: %s", filter.String()) + } + return + } +} From 139eecdead24105f05d6527254c644e478d857ec Mon Sep 17 00:00:00 2001 From: Austin DeNoble Date: Wed, 19 Nov 2025 21:28:47 -0500 Subject: [PATCH 14/18] fix describe-stats presentation output, add new commands to root index command --- internal/pkg/cli/command/index/cmd.go | 3 +++ .../utils/presenters/describe_index_stats.go | 25 +++++++++++++++---- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/internal/pkg/cli/command/index/cmd.go b/internal/pkg/cli/command/index/cmd.go index 9cbe91b..a0a8293 100644 --- a/internal/pkg/cli/command/index/cmd.go +++ b/internal/pkg/cli/command/index/cmd.go @@ -43,6 +43,9 @@ func NewIndexCmd() *cobra.Command { cmd.AddCommand(NewFetchCmd()) cmd.AddCommand(NewQueryCmd()) cmd.AddCommand(NewListVectorsCmd()) + cmd.AddCommand(NewDeleteVectorsCmd()) + cmd.AddCommand(NewUpdateCmd()) + cmd.AddCommand(NewDescribeIndexStatsCmd()) return cmd } diff --git a/internal/pkg/utils/presenters/describe_index_stats.go b/internal/pkg/utils/presenters/describe_index_stats.go index 929e35d..422a1ba 100644 --- a/internal/pkg/utils/presenters/describe_index_stats.go +++ b/internal/pkg/utils/presenters/describe_index_stats.go @@ -4,7 +4,6 @@ import ( "strings" "github.com/pinecone-io/cli/internal/pkg/utils/pcio" - "github.com/pinecone-io/cli/internal/pkg/utils/text" "github.com/pinecone-io/go-pinecone/v5/pinecone" ) @@ -15,13 +14,29 @@ func PrintDescribeIndexStatsTable(resp *pinecone.DescribeIndexStatsResponse) { header := strings.Join(columns, "\t") + "\n" pcio.Fprint(writer, header) - pcio.Fprintf(writer, "Dimension\t%d\n", resp.Dimension) + dimension := uint32(0) + if resp.Dimension != nil { + dimension = *resp.Dimension + } + + pcio.Fprintf(writer, "Dimension\t%d\n", dimension) pcio.Fprintf(writer, "Index Fullness\t%f\n", resp.IndexFullness) pcio.Fprintf(writer, "Total Vector Count\t%d\n", resp.TotalVectorCount) - formatted := text.IndentJSON(resp.Namespaces) - formatted = strings.ReplaceAll(formatted, "\n", "\n\t") // indent lines under the namespace value - pcio.Fprintf(writer, "Namespaces\t%s\n", formatted) + if len(resp.Namespaces) == 0 { + pcio.Fprintf(writer, "Namespaces\t\n") + } else { + pcio.Fprintf(writer, "Namespaces\t\n") + pcio.Fprintf(writer, "\tNAME\tVECTOR COUNT\n") + + names := make([]string, 0, len(resp.Namespaces)) + for name := range resp.Namespaces { + names = append(names, name) + } + for _, name := range names { + pcio.Fprintf(writer, "\t%s\t%d\n", name, resp.Namespaces[name].VectorCount) + } + } writer.Flush() } From db67603e64e83fc24f31780a3d1f0e57c89ce529 Mon Sep 17 00:00:00 2001 From: Austin DeNoble Date: Thu, 20 Nov 2025 05:40:01 -0500 Subject: [PATCH 15/18] add custom flags JSONObject, Float32List, and Int32List to allow easier parsing of various input flags like filter, metadata, vector values, sparse values and indices, etc, add batch-size to upsert --- .../pkg/cli/command/index/delete_vectors.go | 36 ++---- .../pkg/cli/command/index/describe_stats.go | 31 +---- internal/pkg/cli/command/index/fetch.go | 44 ++----- internal/pkg/cli/command/index/query.go | 68 +++++----- internal/pkg/cli/command/index/update.go | 30 +---- internal/pkg/cli/command/index/upsert.go | 9 +- internal/pkg/utils/flags/flags.go | 121 +++++++++++++++++- .../pkg/utils/presenters/query_vectors.go | 72 ++++++++--- 8 files changed, 235 insertions(+), 176 deletions(-) diff --git a/internal/pkg/cli/command/index/delete_vectors.go b/internal/pkg/cli/command/index/delete_vectors.go index bdda909..1607544 100644 --- a/internal/pkg/cli/command/index/delete_vectors.go +++ b/internal/pkg/cli/command/index/delete_vectors.go @@ -2,27 +2,23 @@ package index import ( "context" - "encoding/json" - "os" "github.com/pinecone-io/cli/internal/pkg/utils/exit" "github.com/pinecone-io/cli/internal/pkg/utils/flags" "github.com/pinecone-io/cli/internal/pkg/utils/help" "github.com/pinecone-io/cli/internal/pkg/utils/msg" "github.com/pinecone-io/cli/internal/pkg/utils/sdk" - "github.com/pinecone-io/cli/internal/pkg/utils/style" "github.com/pinecone-io/go-pinecone/v5/pinecone" "github.com/spf13/cobra" ) type deleteVectorsCmdOptions struct { - name string - namespace string - ids []string - filter flags.JSONObject - filterFile string - deleteAll bool - json bool + name string + namespace string + ids []string + filter flags.JSONObject + deleteAll bool + json bool } func NewDeleteVectorsCmd() *cobra.Command { @@ -45,7 +41,6 @@ func NewDeleteVectorsCmd() *cobra.Command { cmd.Flags().StringVar(&options.namespace, "namespace", "", "namespace to delete vectors from") cmd.Flags().StringSliceVar(&options.ids, "ids", []string{}, "IDs of the vectors to delete") cmd.Flags().Var(&options.filter, "filter", "filter to delete the vectors with") - cmd.Flags().StringVar(&options.filterFile, "filter-file", "", "file containing filter to delete the vectors with") cmd.Flags().BoolVar(&options.deleteAll, "all-vectors", false, "delete all vectors from the namespace") cmd.Flags().BoolVar(&options.json, "json", false, "output as JSON") @@ -99,23 +94,8 @@ func runDeleteVectorsCmd(ctx context.Context, options deleteVectorsCmdOptions) { } // Delete vectors by filter - if options.filter != nil || options.filterFile != "" { - var filterMap map[string]any - if options.filterFile != "" { - raw, err := os.ReadFile(options.filterFile) - if err != nil { - msg.FailMsg("Failed to read filter file %s: %s", style.Emphasis(options.filterFile), err) - exit.Errorf(err, "Failed to read filter file %s", options.filterFile) - } - if err := json.Unmarshal(raw, &filterMap); err != nil { - msg.FailMsg("Failed to parse filter: %s", err) - exit.Errorf(err, "Failed to parse filter") - } - } else { - filterMap = options.filter - } - - filter, err := pinecone.NewMetadataFilter(filterMap) + if options.filter != nil { + filter, err := pinecone.NewMetadataFilter(options.filter) if err != nil { msg.FailMsg("Failed to create filter: %s", err) exit.Errorf(err, "Failed to create filter") diff --git a/internal/pkg/cli/command/index/describe_stats.go b/internal/pkg/cli/command/index/describe_stats.go index 81e0362..b4edf56 100644 --- a/internal/pkg/cli/command/index/describe_stats.go +++ b/internal/pkg/cli/command/index/describe_stats.go @@ -2,26 +2,23 @@ package index import ( "context" - "encoding/json" - "os" "github.com/pinecone-io/cli/internal/pkg/utils/exit" + "github.com/pinecone-io/cli/internal/pkg/utils/flags" "github.com/pinecone-io/cli/internal/pkg/utils/help" "github.com/pinecone-io/cli/internal/pkg/utils/msg" "github.com/pinecone-io/cli/internal/pkg/utils/pcio" "github.com/pinecone-io/cli/internal/pkg/utils/presenters" "github.com/pinecone-io/cli/internal/pkg/utils/sdk" - "github.com/pinecone-io/cli/internal/pkg/utils/style" "github.com/pinecone-io/cli/internal/pkg/utils/text" "github.com/pinecone-io/go-pinecone/v5/pinecone" "github.com/spf13/cobra" ) type describeStatsCmdOptions struct { - name string - filter string - filterFile string - json bool + name string + filter flags.JSONObject + json bool } func NewDescribeIndexStatsCmd() *cobra.Command { @@ -38,7 +35,7 @@ func NewDescribeIndexStatsCmd() *cobra.Command { } cmd.Flags().StringVarP(&options.name, "name", "n", "", "name of index to describe stats for") - cmd.Flags().StringVar(&options.filter, "filter", "", "filter to apply to the stats") + cmd.Flags().VarP(&options.filter, "filter", "f", "metadata filter to apply to the operation") cmd.Flags().BoolVar(&options.json, "json", false, "output as JSON") _ = cmd.MarkFlagRequired("name") @@ -56,22 +53,8 @@ func runDescribeIndexStatsCmd(ctx context.Context, options describeStatsCmdOptio // Build metadata filter if provided var filter *pinecone.MetadataFilter - if options.filter != "" || options.filterFile != "" { - if options.filterFile != "" { - raw, err := os.ReadFile(options.filterFile) - if err != nil { - msg.FailMsg("Failed to read filter file %s: %s", style.Emphasis(options.filterFile), err) - exit.Errorf(err, "Failed to read filter file %s", options.filterFile) - } - options.filter = string(raw) - } - - var filterMap map[string]any - if err := json.Unmarshal([]byte(options.filter), &filterMap); err != nil { - msg.FailMsg("Failed to parse filter: %s", err) - exit.Errorf(err, "Failed to parse filter") - } - filter, err = pinecone.NewMetadataFilter(filterMap) + if options.filter != nil { + filter, err = pinecone.NewMetadataFilter(options.filter) if err != nil { msg.FailMsg("Failed to create filter: %s", err) exit.Errorf(err, "Failed to create filter") diff --git a/internal/pkg/cli/command/index/fetch.go b/internal/pkg/cli/command/index/fetch.go index 4c08247..3f24280 100644 --- a/internal/pkg/cli/command/index/fetch.go +++ b/internal/pkg/cli/command/index/fetch.go @@ -2,16 +2,14 @@ package index import ( "context" - "encoding/json" - "os" "github.com/pinecone-io/cli/internal/pkg/utils/exit" + "github.com/pinecone-io/cli/internal/pkg/utils/flags" "github.com/pinecone-io/cli/internal/pkg/utils/help" "github.com/pinecone-io/cli/internal/pkg/utils/msg" "github.com/pinecone-io/cli/internal/pkg/utils/pcio" "github.com/pinecone-io/cli/internal/pkg/utils/presenters" "github.com/pinecone-io/cli/internal/pkg/utils/sdk" - "github.com/pinecone-io/cli/internal/pkg/utils/style" "github.com/pinecone-io/cli/internal/pkg/utils/text" "github.com/pinecone-io/go-pinecone/v5/pinecone" "github.com/spf13/cobra" @@ -21,8 +19,7 @@ type fetchCmdOptions struct { name string namespace string ids []string - filter string - filterFile string + filter flags.JSONObject limit uint32 paginationToken string json bool @@ -36,7 +33,7 @@ func NewFetchCmd() *cobra.Command { Example: help.Examples(` pc index fetch --name my-index --ids 123,456,789 pc index fetch --name my-index --filter '{"key": "value"}' - pc index fetch --name my-index --filter-file ./filter.json + pc index fetch --name my-index --filter @./filter.json `), Run: func(cmd *cobra.Command, args []string) { runFetchCmd(cmd.Context(), options) @@ -44,15 +41,14 @@ func NewFetchCmd() *cobra.Command { } cmd.Flags().StringSliceVarP(&options.ids, "ids", "i", []string{}, "IDs of vectors to fetch") - cmd.Flags().StringVarP(&options.filter, "filter", "f", "", "metadata filter to apply to the fetch") - cmd.Flags().StringVarP(&options.filterFile, "filter-file", "F", "", "file containing metadata filter to apply to the fetch") + cmd.Flags().VarP(&options.filter, "filter", "f", "metadata filter to apply to the fetch") cmd.Flags().StringVarP(&options.name, "name", "n", "", "name of the index to fetch from") cmd.Flags().StringVar(&options.namespace, "namespace", "", "namespace to fetch from") cmd.Flags().Uint32VarP(&options.limit, "limit", "l", 0, "maximum number of vectors to fetch") cmd.Flags().StringVarP(&options.paginationToken, "pagination-token", "p", "", "pagination token to continue a previous listing operation") cmd.Flags().BoolVarP(&options.json, "json", "j", false, "output as JSON") - cmd.MarkFlagsMutuallyExclusive("ids", "filter", "filter-file") + cmd.MarkFlagsMutuallyExclusive("ids", "filter") _ = cmd.MarkFlagRequired("name") return cmd @@ -70,13 +66,9 @@ func runFetchCmd(ctx context.Context, options fetchCmdOptions) { ns = "__default__" } - if options.filter != "" || options.filterFile != "" && (options.limit > 0 || options.paginationToken != "") { - msg.FailMsg("Filter and limit/pagination token cannot be used together") - exit.ErrorMsg("Filter and limit/pagination token cannot be used together") - } - if options.filter != "" && options.filterFile != "" { - msg.FailMsg("Filter and filter file cannot be used together") - exit.ErrorMsg("Filter and filter file cannot be used together") + if len(options.ids) > 0 && (options.limit > 0 || options.paginationToken != "") { + msg.FailMsg("ids and limit/pagination-token cannot be used together") + exit.ErrorMsg("ids and limit/pagination-token cannot be used together") } ic, err := sdk.NewIndexConnection(ctx, pc, options.name, ns) @@ -95,24 +87,8 @@ func runFetchCmd(ctx context.Context, options fetchCmdOptions) { } // Fetch vectors by metadata filter - if options.filter != "" || options.filterFile != "" { - if options.filterFile != "" { - raw, err := os.ReadFile(options.filterFile) - if err != nil { - msg.FailMsg("Failed to read filter file %s: %s", style.Emphasis(options.filterFile), err) - exit.Errorf(err, "Failed to read filter file %s", options.filterFile) - } - options.filter = string(raw) - } - - var filterMap map[string]any - - if err := json.Unmarshal([]byte(options.filter), &filterMap); err != nil { - msg.FailMsg("Failed to parse filter: %s", err) - exit.Errorf(err, "Failed to parse filter") - } - - filter, err := pinecone.NewMetadataFilter(filterMap) + if options.filter != nil { + filter, err := pinecone.NewMetadataFilter(options.filter) if err != nil { msg.FailMsg("Failed to create filter: %s", err) exit.Errorf(err, "Failed to create filter") diff --git a/internal/pkg/cli/command/index/query.go b/internal/pkg/cli/command/index/query.go index 6359acb..cf3e814 100644 --- a/internal/pkg/cli/command/index/query.go +++ b/internal/pkg/cli/command/index/query.go @@ -2,16 +2,14 @@ package index import ( "context" - "encoding/json" - "os" "github.com/pinecone-io/cli/internal/pkg/utils/exit" + "github.com/pinecone-io/cli/internal/pkg/utils/flags" "github.com/pinecone-io/cli/internal/pkg/utils/help" "github.com/pinecone-io/cli/internal/pkg/utils/msg" "github.com/pinecone-io/cli/internal/pkg/utils/pcio" "github.com/pinecone-io/cli/internal/pkg/utils/presenters" "github.com/pinecone-io/cli/internal/pkg/utils/sdk" - "github.com/pinecone-io/cli/internal/pkg/utils/style" "github.com/pinecone-io/cli/internal/pkg/utils/text" "github.com/pinecone-io/go-pinecone/v5/pinecone" "github.com/spf13/cobra" @@ -19,14 +17,13 @@ import ( type queryCmdOptions struct { id string - vector []float32 - sparseIndices []int32 - sparseValues []float32 + vector flags.Float32List + sparseIndices flags.Int32List + sparseValues flags.Float32List name string namespace string topK uint32 - filter string - filterFile string + filter flags.JSONObject includeValues bool includeMetadata bool json bool @@ -48,14 +45,13 @@ func NewQueryCmd() *cobra.Command { cmd.Flags().StringVarP(&options.name, "name", "n", "", "name of the index to query") cmd.Flags().StringVar(&options.namespace, "namespace", "", "index namespace to query") cmd.Flags().Uint32VarP(&options.topK, "top-k", "k", 10, "maximum number of results to return") - cmd.Flags().StringVarP(&options.filter, "filter", "f", "", "metadata filter to apply to the query") - cmd.Flags().StringVar(&options.filterFile, "filter-file", "", "file containing metadata filter to apply to the query") + cmd.Flags().VarP(&options.filter, "filter", "f", "metadata filter to apply to the query") cmd.Flags().BoolVar(&options.includeValues, "include-values", false, "include vector values in the query results") cmd.Flags().BoolVar(&options.includeMetadata, "include-metadata", false, "include metadata in the query results") cmd.Flags().StringVarP(&options.id, "id", "i", "", "ID of the vector to query against") - cmd.Flags().Float32SliceVarP(&options.vector, "vector", "v", []float32{}, "vector values to query against") - cmd.Flags().Int32SliceVar(&options.sparseIndices, "sparse-indices", []int32{}, "sparse indices to query against") - cmd.Flags().Float32SliceVar(&options.sparseValues, "sparse-values", []float32{}, "sparse values to query against") + cmd.Flags().VarP(&options.vector, "vector", "v", "vector values to query against") + cmd.Flags().Var(&options.sparseIndices, "sparse-indices", "sparse indices to query against") + cmd.Flags().Var(&options.sparseValues, "sparse-values", "sparse values to query against") cmd.Flags().BoolVar(&options.json, "json", false, "output as JSON") _ = cmd.MarkFlagRequired("name") @@ -87,22 +83,8 @@ func runQueryCmd(ctx context.Context, options queryCmdOptions) { // Build metadata filter if provided var filter *pinecone.MetadataFilter - if options.filter != "" || options.filterFile != "" { - if options.filterFile != "" { - raw, err := os.ReadFile(options.filterFile) - if err != nil { - msg.FailMsg("Failed to read filter file %s: %s", style.Emphasis(options.filterFile), err) - exit.Errorf(err, "Failed to read filter file %s", options.filterFile) - } - options.filter = string(raw) - } - - var filterMap map[string]any - if err := json.Unmarshal([]byte(options.filter), &filterMap); err != nil { - msg.FailMsg("Failed to parse filter: %s", err) - exit.Errorf(err, "Failed to parse filter") - } - filter, err = pinecone.NewMetadataFilter(filterMap) + if options.filter != nil { + filter, err = pinecone.NewMetadataFilter(options.filter) if err != nil { msg.FailMsg("Failed to create filter: %s", err) exit.Errorf(err, "Failed to create filter") @@ -127,17 +109,29 @@ func runQueryCmd(ctx context.Context, options queryCmdOptions) { // Query by vector values if len(options.vector) > 0 || len(options.sparseIndices) > 0 || len(options.sparseValues) > 0 { - sparseIndices, err := toUint32Slice(options.sparseIndices) - if err != nil { - exit.Error(err, "Failed to convert sparse indices to uint32") - } + var sparse *pinecone.SparseValues - req := &pinecone.QueryByVectorValuesRequest{ - Vector: options.vector, - SparseValues: &pinecone.SparseValues{ + // Only include sparse values if the user provided them + if len(options.sparseIndices) > 0 || len(options.sparseValues) > 0 { + if len(options.sparseIndices) == 0 || len(options.sparseValues) == 0 { + exit.Errorf(nil, "both --sparse-indices and --sparse-values are required when specifying sparse values") + } + if len(options.sparseIndices) != len(options.sparseValues) { + exit.Errorf(nil, "--sparse-indices and --sparse-values must be the same length") + } + sparseIndices, err := toUint32Slice(options.sparseIndices) + if err != nil { + exit.Error(err, "Failed to convert sparse indices to uint32") + } + sparse = &pinecone.SparseValues{ Indices: sparseIndices, Values: options.sparseValues, - }, + } + } + + req := &pinecone.QueryByVectorValuesRequest{ + Vector: options.vector, + SparseValues: sparse, TopK: options.topK, IncludeValues: options.includeValues, IncludeMetadata: options.includeMetadata, diff --git a/internal/pkg/cli/command/index/update.go b/internal/pkg/cli/command/index/update.go index 39fdc6b..0034df9 100644 --- a/internal/pkg/cli/command/index/update.go +++ b/internal/pkg/cli/command/index/update.go @@ -2,8 +2,6 @@ package index import ( "context" - "encoding/json" - "os" "github.com/pinecone-io/cli/internal/pkg/utils/exit" "github.com/pinecone-io/cli/internal/pkg/utils/flags" @@ -12,7 +10,6 @@ import ( "github.com/pinecone-io/cli/internal/pkg/utils/pcio" "github.com/pinecone-io/cli/internal/pkg/utils/presenters" "github.com/pinecone-io/cli/internal/pkg/utils/sdk" - "github.com/pinecone-io/cli/internal/pkg/utils/style" "github.com/pinecone-io/cli/internal/pkg/utils/text" "github.com/pinecone-io/go-pinecone/v5/pinecone" "github.com/spf13/cobra" @@ -27,7 +24,6 @@ type updateCmdOptions struct { sparseValues []float32 metadata flags.JSONObject filter flags.JSONObject - filterFile string dryRun bool json bool } @@ -52,7 +48,6 @@ func NewUpdateCmd() *cobra.Command { cmd.Flags().Float32SliceVar(&options.sparseValues, "sparse-values", []float32{}, "sparse values to update the vector with") cmd.Flags().Var(&options.metadata, "metadata", "metadata to update the vector with") cmd.Flags().Var(&options.filter, "filter", "filter to update the vectors with") - cmd.Flags().StringVar(&options.filterFile, "filter-file", "", "file containing filter to update the vectors with") cmd.Flags().BoolVar(&options.dryRun, "dry-run", false, "do not update the vectors, just return the number of vectors that would be updated") cmd.Flags().BoolVar(&options.json, "json", false, "output as JSON") @@ -74,7 +69,7 @@ func runUpdateCmd(ctx context.Context, options updateCmdOptions) { } // Validate update by ID or metadata filter - if options.id == "" && options.filter == nil && options.filterFile == "" { + if options.id == "" && options.filter == nil { msg.FailMsg("Either --id or --filter must be provided") exit.ErrorMsg("Either --id or --filter must be provided") } @@ -85,7 +80,7 @@ func runUpdateCmd(ctx context.Context, options updateCmdOptions) { exit.Error(err, "Failed to create index connection") } - if options.id != "" && (options.filter != nil || options.filterFile != "") { + if options.id != "" && options.filter != nil { msg.FailMsg("ID and filter cannot be used together") exit.ErrorMsg("ID and filter cannot be used together") } @@ -129,25 +124,8 @@ func runUpdateCmd(ctx context.Context, options updateCmdOptions) { } // Update vectors by metadata filter - if options.filter != nil || options.filterFile != "" { - var filterMap map[string]any - - // Build metadata filter if provided - if options.filterFile != "" { - raw, err := os.ReadFile(options.filterFile) - if err != nil { - msg.FailMsg("Failed to read filter file %s: %s", style.Emphasis(options.filterFile), err) - exit.Errorf(err, "Failed to read filter file %s", options.filterFile) - } - if err := json.Unmarshal(raw, &filterMap); err != nil { - msg.FailMsg("Failed to parse filter: %s", err) - exit.Errorf(err, "Failed to parse filter") - } - } else { // Otherwise use the filter passed in as a JSON string - filterMap = options.filter - } - - filter, err := pinecone.NewMetadataFilter(filterMap) + if options.filter != nil { + filter, err := pinecone.NewMetadataFilter(options.filter) if err != nil { msg.FailMsg("Failed to create filter: %s", err) exit.Errorf(err, "Failed to create filter") diff --git a/internal/pkg/cli/command/index/upsert.go b/internal/pkg/cli/command/index/upsert.go index 9d10412..5e9a157 100644 --- a/internal/pkg/cli/command/index/upsert.go +++ b/internal/pkg/cli/command/index/upsert.go @@ -21,6 +21,7 @@ type upsertCmdOptions struct { file string name string namespace string + batchSize int json bool } @@ -58,6 +59,7 @@ func NewUpsertCmd() *cobra.Command { cmd.Flags().StringVarP(&options.name, "name", "n", "", "name of index to upsert into") cmd.Flags().StringVar(&options.namespace, "namespace", "", "namespace to upsert into") cmd.Flags().StringVarP(&options.file, "file", "f", "", "file to upsert from") + cmd.Flags().IntVarP(&options.batchSize, "batch-size", "b", 1000, "size of batches to upsert (default: 1000)") cmd.Flags().BoolVar(&options.json, "json", false, "output as JSON") _ = cmd.MarkFlagRequired("name") _ = cmd.MarkFlagRequired("file") @@ -125,10 +127,9 @@ func runUpsertCmd(ctx context.Context, options upsertCmdOptions) { mapped = append(mapped, &vector) } - batchSize := 1000 - batches := make([][]*pinecone.Vector, 0, (len(mapped)+batchSize-1)/batchSize) - for i := 0; i < len(mapped); i += batchSize { - end := i + batchSize + batches := make([][]*pinecone.Vector, 0, (len(mapped)+options.batchSize-1)/options.batchSize) + for i := 0; i < len(mapped); i += options.batchSize { + end := i + options.batchSize if end > len(mapped) { end = len(mapped) } diff --git a/internal/pkg/utils/flags/flags.go b/internal/pkg/utils/flags/flags.go index 5dc2746..02954d9 100644 --- a/internal/pkg/utils/flags/flags.go +++ b/internal/pkg/utils/flags/flags.go @@ -2,13 +2,17 @@ package flags import ( "encoding/json" + "maps" "os" + "strconv" "strings" "github.com/pinecone-io/cli/internal/pkg/utils/pcio" ) type JSONObject map[string]any +type Float32List []float32 +type Int32List []int32 func (m *JSONObject) Set(value string) error { // allow passing "@file.json" to read a file path and parse as JSON @@ -28,9 +32,7 @@ func (m *JSONObject) Set(value string) error { if *m == nil { *m = make(map[string]any) } - for k, v := range tmp { - (*m)[k] = v - } + maps.Copy((*m), tmp) return nil } @@ -42,4 +44,115 @@ func (m *JSONObject) String() string { return string(b) } -func (*JSONObject) Type() string { return "json" } +func (*JSONObject) Type() string { return "json-object" } + +func (m *Float32List) Set(value string) error { + // allow passing "@file.json" to read a file path and parse as JSON + if strings.HasPrefix(value, "@") { + filePath := strings.TrimPrefix(value, "@") + raw, err := os.ReadFile(filePath) + if err != nil { + return err + } + value = string(raw) + } + + value = strings.TrimSpace(value) + if value == "" { + *m = (*m)[:0] + return nil + } + + // JSON array + if strings.HasPrefix(value, "[") && strings.HasSuffix(value, "]") { + var arr []float32 + if err := json.Unmarshal([]byte(value), &arr); err != nil { + return pcio.Errorf("failed to parse JSON float32 array: %w", err) + } + *m = append((*m)[:0], arr...) + return nil + } + + // CSV/whitespace separated list + vals := strings.FieldsFunc(value, func(r rune) bool { + return r == ',' || r == ' ' || r == '\t' || r == '\n' || r == '\r' + }) + out := make([]float32, 0, len(vals)) + for _, val := range vals { + if val == "" { + continue + } + f, err := strconv.ParseFloat(val, 32) + if err != nil { + return pcio.Errorf("invalid float32 %q: %w", val, err) + } + out = append(out, float32(f)) + } + *m = append((*m)[:0], out...) + return nil +} + +func (m *Float32List) String() string { + if m == nil || len(*m) == 0 { + return "" + } + b, _ := json.Marshal(m) + return string(b) +} + +func (*Float32List) Type() string { return "float32 json-array|csv-list" } + +func (m *Int32List) Set(value string) error { + // allow passing "@file.json" to read a file path and parse as JSON + if strings.HasPrefix(value, "@") { + filePath := strings.TrimPrefix(value, "@") + raw, err := os.ReadFile(filePath) + if err != nil { + return err + } + return json.Unmarshal(raw, m) + } + + value = strings.TrimSpace(value) + if value == "" { + *m = (*m)[:0] + return nil + } + + // JSON array + if strings.HasPrefix(value, "[") && strings.HasSuffix(value, "]") { + var arr []int32 + if err := json.Unmarshal([]byte(value), &arr); err != nil { + return pcio.Errorf("failed to parse JSON int32 array: %w", err) + } + *m = append((*m)[:0], arr...) + return nil + } + + // CSV/whitespace separated list + vals := strings.FieldsFunc(value, func(r rune) bool { + return r == ',' || r == ' ' || r == '\t' || r == '\n' || r == '\r' + }) + out := make([]int32, 0, len(vals)) + for _, val := range vals { + if val == "" { + continue + } + i, err := strconv.ParseInt(val, 10, 32) + if err != nil { + return pcio.Errorf("invalid int32 %q: %w", val, err) + } + out = append(out, int32(i)) + } + *m = append((*m)[:0], out...) + return nil +} + +func (m *Int32List) String() string { + if m == nil || len(*m) == 0 { + return "" + } + b, _ := json.Marshal(m) + return string(b) +} +func (*Int32List) Type() string { return "int32 json-array|csv-list" } diff --git a/internal/pkg/utils/presenters/query_vectors.go b/internal/pkg/utils/presenters/query_vectors.go index 60b3da5..03bfeda 100644 --- a/internal/pkg/utils/presenters/query_vectors.go +++ b/internal/pkg/utils/presenters/query_vectors.go @@ -19,39 +19,73 @@ func PrintQueryVectorsTable(resp *pinecone.QueryVectorsResponse) { pcio.Fprintf(writer, "Usage: %d (read units)\n", resp.Usage.ReadUnits) } + // Detect which columns to show + hasDense := false + hasSparse := false + hasMetadata := false + for _, m := range resp.Matches { + if m == nil || m.Vector == nil { + continue + } + if m.Vector.Values != nil && len(*m.Vector.Values) > 0 { + hasDense = true + } + if m.Vector.SparseValues != nil && + (len(m.Vector.SparseValues.Indices) > 0 || len(m.Vector.SparseValues.Values) > 0) { + hasSparse = true + } + if m.Vector.Metadata != nil { + hasMetadata = true + } + } + // Table Header - columns := []string{"ID", "SCORE", "VALUES", "SPARSE INDICES", "SPARSE VALUES", "METADATA"} - pcio.Fprintln(writer, strings.Join(columns, "\t")) + cols := []string{"ID", "SCORE"} + if hasDense { + cols = append(cols, "VALUES") + } + if hasSparse { + cols = append(cols, "SPARSE INDICES", "SPARSE VALUES") + } + if hasMetadata { + cols = append(cols, "METADATA") + } + pcio.Fprintln(writer, strings.Join(cols, "\t")) // Rows for _, match := range resp.Matches { if match == nil || match.Vector == nil { continue } + row := []string{match.Vector.Id, pcio.Sprintf("%f", match.Score)} - valuesPreview := "" - if match.Vector.Values != nil { - valuesPreview = previewSliceFloat32(match.Vector.Values, 3) + if hasDense { + values := "" + if match.Vector.Values != nil { + values = previewSliceFloat32(match.Vector.Values, 3) + } + row = append(row, values) } - - sparseIndicesPreview := "" - if match.Vector.SparseValues != nil { - sparseIndicesPreview = previewSliceUint32(match.Vector.SparseValues.Indices, 3) + if hasSparse { + iPreview, vPreview := "", "" + if match.Vector.SparseValues != nil { + iPreview = previewSliceUint32(match.Vector.SparseValues.Indices, 3) + vPreview = previewSliceFloat32(&match.Vector.SparseValues.Values, 3) + } + row = append(row, iPreview, vPreview) } - - sparseValuesPreview := "" - if match.Vector.SparseValues != nil { - sparseValuesPreview = previewSliceFloat32(&match.Vector.SparseValues.Values, 3) + if hasMetadata { + meta := "" + if match.Vector.Metadata != nil { + meta = text.InlineJSON(match.Vector.Metadata) + } + row = append(row, meta) } - metadataPreview := "" - if match.Vector.Metadata != nil { - metadataPreview = text.InlineJSON(match.Vector.Metadata) - } - - row := []string{match.Vector.Id, pcio.Sprintf("%f", match.Score), valuesPreview, sparseIndicesPreview, sparseValuesPreview, metadataPreview} pcio.Fprintln(writer, strings.Join(row, "\t")) } + + writer.Flush() } func previewSliceUint32(values []uint32, limit int) string { From d5f2b5c88019dc54d255ffb10f85068b135ec9b4 Mon Sep 17 00:00:00 2001 From: Austin DeNoble Date: Thu, 20 Nov 2025 06:07:52 -0500 Subject: [PATCH 16/18] improve fetch and query presentation logic --- .../pkg/utils/presenters/fetch_vectors.go | 36 +++++++++++++++-- .../pkg/utils/presenters/query_vectors.go | 39 +++++++++++++++++-- 2 files changed, 68 insertions(+), 7 deletions(-) diff --git a/internal/pkg/utils/presenters/fetch_vectors.go b/internal/pkg/utils/presenters/fetch_vectors.go index 98e0d48..84b556f 100644 --- a/internal/pkg/utils/presenters/fetch_vectors.go +++ b/internal/pkg/utils/presenters/fetch_vectors.go @@ -1,6 +1,7 @@ package presenters import ( + "sort" "strings" "github.com/pinecone-io/cli/internal/pkg/utils/pcio" @@ -63,10 +64,33 @@ func PrintFetchVectorsTable(results *FetchVectorsResults) { if vector.SparseValues != nil { sparseDim = len(vector.SparseValues.Values) } - metadata := "" + metadata := "" if vector.Metadata != nil { - metadata = text.InlineJSON(vector.Metadata) + m := vector.Metadata.AsMap() + if len(m) > 0 { + keys := make([]string, 0, len(m)) + for k := range m { + keys = append(keys, k) + } + sort.Strings(keys) + show := keys + if len(show) > 3 { + show = show[:3] + } + limited := make(map[string]any, len(show)) + for _, k := range show { + limited[k] = m[k] + } + + s := text.InlineJSON(limited) // compact one-line JSON + if len(keys) > 3 { + // put ellipsis inside the braces: {"a":1,"b":2,"c":3, ...} + s = strings.TrimRight(s, "}") + ", ...}" + } + metadata = s + } } + preview := previewSliceFloat32(vector.Values, 3) row := []string{id, pcio.Sprintf("%d", dim), preview, pcio.Sprintf("%d", sparseDim), metadata} pcio.Fprintln(writer, strings.Join(row, "\t")) @@ -80,8 +104,14 @@ func previewSliceFloat32(values *[]float32, limit int) string { return "" } vals := *values + truncated := false if len(vals) > limit { vals = vals[:limit] + truncated = true + } + text := text.InlineJSON(vals) + if truncated && strings.HasSuffix(text, "]") { + text = text[:len(text)-1] + ", ...]" } - return text.InlineJSON(vals) + "..." + return text } diff --git a/internal/pkg/utils/presenters/query_vectors.go b/internal/pkg/utils/presenters/query_vectors.go index 03bfeda..271101e 100644 --- a/internal/pkg/utils/presenters/query_vectors.go +++ b/internal/pkg/utils/presenters/query_vectors.go @@ -1,6 +1,7 @@ package presenters import ( + "sort" "strings" "github.com/pinecone-io/cli/internal/pkg/utils/pcio" @@ -75,11 +76,33 @@ func PrintQueryVectorsTable(resp *pinecone.QueryVectorsResponse) { row = append(row, iPreview, vPreview) } if hasMetadata { - meta := "" + metadata := "" if match.Vector.Metadata != nil { - meta = text.InlineJSON(match.Vector.Metadata) + m := match.Vector.Metadata.AsMap() + if len(m) > 0 { + keys := make([]string, 0, len(m)) + for k := range m { + keys = append(keys, k) + } + sort.Strings(keys) + show := keys + if len(show) > 3 { + show = show[:3] + } + limited := make(map[string]any, len(show)) + for _, k := range show { + limited[k] = m[k] + } + + s := text.InlineJSON(limited) // compact one-line JSON + if len(keys) > 3 { + // put ellipsis inside the braces: {"a":1,"b":2,"c":3, ...} + s = strings.TrimRight(s, "}") + ", ...}" + } + metadata = s + } } - row = append(row, meta) + row = append(row, metadata) } pcio.Fprintln(writer, strings.Join(row, "\t")) @@ -93,8 +116,16 @@ func previewSliceUint32(values []uint32, limit int) string { return "" } vals := values + truncated := false if len(vals) > limit { vals = vals[:limit] + truncated = true + } + + text := text.InlineJSON(vals) + if truncated && strings.HasSuffix(text, "]") { + text = text[:len(text)-1] + ", ...]" } - return text.InlineJSON(vals) + "..." + + return text } From 07dc1c079199bcc6beca2c25185edea6d6045cd1 Mon Sep 17 00:00:00 2001 From: Austin DeNoble Date: Thu, 20 Nov 2025 11:08:47 -0500 Subject: [PATCH 17/18] default namespace properly and fix logic error --- internal/pkg/cli/command/index/delete_vectors.go | 5 +---- internal/pkg/cli/command/index/fetch.go | 5 +---- internal/pkg/cli/command/index/list_vectors.go | 5 +---- internal/pkg/cli/command/index/query.go | 5 +---- internal/pkg/cli/command/index/update.go | 5 +---- internal/pkg/cli/command/index/upsert.go | 8 +++----- internal/pkg/utils/flags/flags.go | 1 + 7 files changed, 9 insertions(+), 25 deletions(-) diff --git a/internal/pkg/cli/command/index/delete_vectors.go b/internal/pkg/cli/command/index/delete_vectors.go index 1607544..3a6c601 100644 --- a/internal/pkg/cli/command/index/delete_vectors.go +++ b/internal/pkg/cli/command/index/delete_vectors.go @@ -38,7 +38,7 @@ func NewDeleteVectorsCmd() *cobra.Command { } cmd.Flags().StringVarP(&options.name, "name", "n", "", "name of the index to delete vectors from") - cmd.Flags().StringVar(&options.namespace, "namespace", "", "namespace to delete vectors from") + cmd.Flags().StringVar(&options.namespace, "namespace", "__default__", "namespace to delete vectors from") cmd.Flags().StringSliceVar(&options.ids, "ids", []string{}, "IDs of the vectors to delete") cmd.Flags().Var(&options.filter, "filter", "filter to delete the vectors with") cmd.Flags().BoolVar(&options.deleteAll, "all-vectors", false, "delete all vectors from the namespace") @@ -54,9 +54,6 @@ func runDeleteVectorsCmd(ctx context.Context, options deleteVectorsCmdOptions) { // Default namespace ns := options.namespace - if options.namespace != "" { - ns = options.namespace - } if ns == "" { ns = "__default__" } diff --git a/internal/pkg/cli/command/index/fetch.go b/internal/pkg/cli/command/index/fetch.go index 3f24280..68674e0 100644 --- a/internal/pkg/cli/command/index/fetch.go +++ b/internal/pkg/cli/command/index/fetch.go @@ -43,7 +43,7 @@ func NewFetchCmd() *cobra.Command { cmd.Flags().StringSliceVarP(&options.ids, "ids", "i", []string{}, "IDs of vectors to fetch") cmd.Flags().VarP(&options.filter, "filter", "f", "metadata filter to apply to the fetch") cmd.Flags().StringVarP(&options.name, "name", "n", "", "name of the index to fetch from") - cmd.Flags().StringVar(&options.namespace, "namespace", "", "namespace to fetch from") + cmd.Flags().StringVar(&options.namespace, "namespace", "__default__", "namespace to fetch from") cmd.Flags().Uint32VarP(&options.limit, "limit", "l", 0, "maximum number of vectors to fetch") cmd.Flags().StringVarP(&options.paginationToken, "pagination-token", "p", "", "pagination token to continue a previous listing operation") cmd.Flags().BoolVarP(&options.json, "json", "j", false, "output as JSON") @@ -59,9 +59,6 @@ func runFetchCmd(ctx context.Context, options fetchCmdOptions) { // Default namespace ns := options.namespace - if options.namespace != "" { - ns = options.namespace - } if ns == "" { ns = "__default__" } diff --git a/internal/pkg/cli/command/index/list_vectors.go b/internal/pkg/cli/command/index/list_vectors.go index 3afcfed..b5518e7 100644 --- a/internal/pkg/cli/command/index/list_vectors.go +++ b/internal/pkg/cli/command/index/list_vectors.go @@ -36,7 +36,7 @@ func NewListVectorsCmd() *cobra.Command { } cmd.Flags().StringVarP(&options.name, "name", "n", "", "name of the index to list vectors from") - cmd.Flags().StringVar(&options.namespace, "namespace", "", "namespace to list vectors from") + cmd.Flags().StringVar(&options.namespace, "namespace", "__default__", "namespace to list vectors from") cmd.Flags().Uint32VarP(&options.limit, "limit", "l", 0, "maximum number of vectors to list") cmd.Flags().StringVarP(&options.paginationToken, "pagination-token", "p", "", "pagination token to continue a previous listing operation") cmd.Flags().BoolVar(&options.json, "json", false, "output as JSON") @@ -51,9 +51,6 @@ func runListVectorsCmd(ctx context.Context, options listVectorsCmdOptions) { // Default namespace ns := options.namespace - if options.namespace != "" { - ns = options.namespace - } if ns == "" { ns = "__default__" } diff --git a/internal/pkg/cli/command/index/query.go b/internal/pkg/cli/command/index/query.go index cf3e814..64a20eb 100644 --- a/internal/pkg/cli/command/index/query.go +++ b/internal/pkg/cli/command/index/query.go @@ -43,7 +43,7 @@ func NewQueryCmd() *cobra.Command { } cmd.Flags().StringVarP(&options.name, "name", "n", "", "name of the index to query") - cmd.Flags().StringVar(&options.namespace, "namespace", "", "index namespace to query") + cmd.Flags().StringVar(&options.namespace, "namespace", "__default__", "index namespace to query") cmd.Flags().Uint32VarP(&options.topK, "top-k", "k", 10, "maximum number of results to return") cmd.Flags().VarP(&options.filter, "filter", "f", "metadata filter to apply to the query") cmd.Flags().BoolVar(&options.includeValues, "include-values", false, "include vector values in the query results") @@ -65,9 +65,6 @@ func runQueryCmd(ctx context.Context, options queryCmdOptions) { // Default namespace ns := options.namespace - if options.namespace != "" { - ns = options.namespace - } if ns == "" { ns = "__default__" } diff --git a/internal/pkg/cli/command/index/update.go b/internal/pkg/cli/command/index/update.go index 0034df9..f5d30fd 100644 --- a/internal/pkg/cli/command/index/update.go +++ b/internal/pkg/cli/command/index/update.go @@ -41,7 +41,7 @@ func NewUpdateCmd() *cobra.Command { } cmd.Flags().StringVarP(&options.name, "name", "n", "", "name of the index to update") - cmd.Flags().StringVar(&options.namespace, "namespace", "", "namespace to update the vector in") + cmd.Flags().StringVar(&options.namespace, "namespace", "__default__", "namespace to update the vector in") cmd.Flags().StringVar(&options.id, "id", "", "ID of the vector to update") cmd.Flags().Float32SliceVar(&options.values, "values", []float32{}, "values to update the vector with") cmd.Flags().Int32SliceVar(&options.sparseIndices, "sparse-indices", []int32{}, "sparse indices to update the vector with") @@ -61,9 +61,6 @@ func runUpdateCmd(ctx context.Context, options updateCmdOptions) { // Default namespace ns := options.namespace - if options.namespace != "" { - ns = options.namespace - } if ns == "" { ns = "__default__" } diff --git a/internal/pkg/cli/command/index/upsert.go b/internal/pkg/cli/command/index/upsert.go index 5e9a157..b36c2d9 100644 --- a/internal/pkg/cli/command/index/upsert.go +++ b/internal/pkg/cli/command/index/upsert.go @@ -57,7 +57,7 @@ func NewUpsertCmd() *cobra.Command { } cmd.Flags().StringVarP(&options.name, "name", "n", "", "name of index to upsert into") - cmd.Flags().StringVar(&options.namespace, "namespace", "", "namespace to upsert into") + cmd.Flags().StringVar(&options.namespace, "namespace", "__default__", "namespace to upsert into") cmd.Flags().StringVarP(&options.file, "file", "f", "", "file to upsert from") cmd.Flags().IntVarP(&options.batchSize, "batch-size", "b", 1000, "size of batches to upsert (default: 1000)") cmd.Flags().BoolVar(&options.json, "json", false, "output as JSON") @@ -82,13 +82,11 @@ func runUpsertCmd(ctx context.Context, options upsertCmdOptions) { } // Default namespace - ns := payload.Namespace - if options.namespace != "" { - ns = options.namespace - } + ns := options.namespace if ns == "" { ns = "__default__" } + // Get IndexConnection pc := sdk.NewPineconeClient(ctx) ic, err := sdk.NewIndexConnection(ctx, pc, options.name, ns) diff --git a/internal/pkg/utils/flags/flags.go b/internal/pkg/utils/flags/flags.go index 02954d9..2bed2fe 100644 --- a/internal/pkg/utils/flags/flags.go +++ b/internal/pkg/utils/flags/flags.go @@ -155,4 +155,5 @@ func (m *Int32List) String() string { b, _ := json.Marshal(m) return string(b) } + func (*Int32List) Type() string { return "int32 json-array|csv-list" } From ab144f005d3d214975495f7161bb333f133faab9 Mon Sep 17 00:00:00 2001 From: Austin DeNoble Date: Thu, 20 Nov 2025 11:13:20 -0500 Subject: [PATCH 18/18] fix batch upsert message --- internal/pkg/cli/command/index/upsert.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/pkg/cli/command/index/upsert.go b/internal/pkg/cli/command/index/upsert.go index b36c2d9..b890161 100644 --- a/internal/pkg/cli/command/index/upsert.go +++ b/internal/pkg/cli/command/index/upsert.go @@ -144,7 +144,7 @@ func runUpsertCmd(ctx context.Context, options upsertCmdOptions) { json := text.IndentJSON(resp) pcio.Println(json) } else { - msg.SuccessMsg("Upserted %d vectors into namespace %s in %d batches", len(batch), ns, i+1) + msg.SuccessMsg("Upserted %d vectors into namespace %s (batch %d of %d)", len(batch), ns, i+1, len(batches)) } } }