From dbcbb60dd63ca41eb23862243b6244729c819b7e Mon Sep 17 00:00:00 2001 From: Hami0095 Date: Mon, 14 Sep 2026 15:17:24 +0200 Subject: [PATCH] fix(import): apply clear/replace existing on the final batch Rules and synonyms are imported in batches of 1000. The clear flag was attached only to the request sent when the buffer filled, so the final partial batch was saved without it. For any file holding fewer than 1000 entries there is no full batch at all, so the only request sent carried no flag and --clear-existing-rules / --replace-existing-synonyms did nothing. The existing ClearRules/ClearSynonyms fallback does not cover this: it is guarded by totalCount == 0, so it only fires for an empty input file. The flag is attached only when it is still set, so a multi-batch import's later requests keep omitting the parameter rather than sending clearExistingRules=false, preserving the current wire format. Fixes #191 Co-Authored-By: Claude Sonnet 5 --- pkg/cmd/rules/import/import.go | 10 ++++++---- pkg/cmd/rules/import/import_test.go | 24 ++++++++++++++++++++++++ pkg/cmd/synonyms/import/import.go | 10 ++++++---- pkg/cmd/synonyms/import/import_test.go | 24 ++++++++++++++++++++++++ 4 files changed, 60 insertions(+), 8 deletions(-) diff --git a/pkg/cmd/rules/import/import.go b/pkg/cmd/rules/import/import.go index 06059fd5..1481b601 100644 --- a/pkg/cmd/rules/import/import.go +++ b/pkg/cmd/rules/import/import.go @@ -206,10 +206,12 @@ func runImportCmd(opts *ImportOptions) error { opts.IO.StopProgressIndicator() return err } - res, err := client.SaveRules( - client.NewApiSaveRulesRequest(opts.Index, rules). - WithForwardToReplicas(opts.ForwardToReplicas), - ) + request := client.NewApiSaveRulesRequest(opts.Index, rules). + WithForwardToReplicas(opts.ForwardToReplicas) + if clearExistingRules { + request = request.WithClearExistingRules(true) + } + res, err := client.SaveRules(request) if err != nil { opts.IO.StopProgressIndicator() return err diff --git a/pkg/cmd/rules/import/import_test.go b/pkg/cmd/rules/import/import_test.go index 81931df5..0c684c72 100644 --- a/pkg/cmd/rules/import/import_test.go +++ b/pkg/cmd/rules/import/import_test.go @@ -178,6 +178,30 @@ func Test_runExportCmd(t *testing.T) { wantOut: "✓ Successfully imported 0 rules to foo\n", setup: func(r *httpmock.Registry) {}, }, + { + name: "from small batch clear existing", + cli: "foo -c -y -F -", + stdin: `{"objectID":"test"}`, + wantOut: "✓ Successfully imported 1 rules to foo\n", + setup: func(r *httpmock.Registry) { + r.Register(httpmock.Matcher(func(req *http.Request) bool { + return httpmock.REST("POST", "1/indexes/foo/rules/batch")(req) && + req.URL.Query().Get("clearExistingRules") == "true" + }), httpmock.JSONResponse(search.UpdatedAtResponse{})) + }, + }, + { + name: "from small batch without clear existing", + cli: "foo -F -", + stdin: `{"objectID":"test"}`, + wantOut: "✓ Successfully imported 1 rules to foo\n", + setup: func(r *httpmock.Registry) { + r.Register(httpmock.Matcher(func(req *http.Request) bool { + return httpmock.REST("POST", "1/indexes/foo/rules/batch")(req) && + req.URL.Query().Get("clearExistingRules") == "" + }), httpmock.JSONResponse(search.UpdatedAtResponse{})) + }, + }, { name: "from large batch clear existing", cli: "foo -c -y -F -", diff --git a/pkg/cmd/synonyms/import/import.go b/pkg/cmd/synonyms/import/import.go index 3ac65f83..b6cc2ae9 100644 --- a/pkg/cmd/synonyms/import/import.go +++ b/pkg/cmd/synonyms/import/import.go @@ -182,10 +182,12 @@ func runImportCmd(opts *ImportOptions) error { opts.IO.StopProgressIndicator() return err } - res, err := client.SaveSynonyms( - client.NewApiSaveSynonymsRequest(opts.Index, synonyms). - WithForwardToReplicas(opts.ForwardToReplicas), - ) + request := client.NewApiSaveSynonymsRequest(opts.Index, synonyms). + WithForwardToReplicas(opts.ForwardToReplicas) + if clearExistingSynonyms { + request = request.WithReplaceExistingSynonyms(true) + } + res, err := client.SaveSynonyms(request) if err != nil { opts.IO.StopProgressIndicator() return err diff --git a/pkg/cmd/synonyms/import/import_test.go b/pkg/cmd/synonyms/import/import_test.go index bd030b92..ada83913 100644 --- a/pkg/cmd/synonyms/import/import_test.go +++ b/pkg/cmd/synonyms/import/import_test.go @@ -193,6 +193,30 @@ func Test_runExportCmd(t *testing.T) { wantOut: "✓ Successfully imported 0 synonyms to foo\n", setup: func(r *httpmock.Registry) {}, }, + { + name: "from small batch with clear existing", + cli: "foo -r -F -", + stdin: `{"objectID":"test","type":"synonym","synonyms":["test"]}`, + wantOut: "✓ Successfully imported 1 synonyms to foo\n", + setup: func(r *httpmock.Registry) { + r.Register(httpmock.Matcher(func(req *http.Request) bool { + return httpmock.REST("POST", "1/indexes/foo/synonyms/batch")(req) && + req.URL.Query().Get("replaceExistingSynonyms") == "true" + }), httpmock.JSONResponse(search.UpdatedAtResponse{})) + }, + }, + { + name: "from small batch without clear existing", + cli: "foo -F -", + stdin: `{"objectID":"test","type":"synonym","synonyms":["test"]}`, + wantOut: "✓ Successfully imported 1 synonyms to foo\n", + setup: func(r *httpmock.Registry) { + r.Register(httpmock.Matcher(func(req *http.Request) bool { + return httpmock.REST("POST", "1/indexes/foo/synonyms/batch")(req) && + req.URL.Query().Get("replaceExistingSynonyms") == "" + }), httpmock.JSONResponse(search.UpdatedAtResponse{})) + }, + }, { name: "from large batch with clear existing", cli: "foo -r -F -",