Skip to content

Commit 54d8c8c

Browse files
authored
[ENH]: Make error messages in attach_function clearer (#5973)
## Description of changes _Summarize the changes made by this PR._ - Improvements & Bug fixes - Errors fixed: - Param validation - Output collection already exists error looks nicer now - Function already exists error looks cleaner - Invalid function to be attached error added - Collection already has an attached function - New functionality - ... ## Test plan _How are these changes tested?_ - [ ] Tests pass locally with `pytest` for python, `yarn test` for js, `cargo test` for rust ## Migration plan _Are there any migrations, or any forwards/backwards compatibility changes needed in order to make sure this change deploys reliably?_ ## Observability plan _What is the plan to instrument and monitor this change?_ ## Documentation Changes _Are all docstrings for user-facing APIs updated if required? Do we need to make documentation changes in the_ [_docs section](https://github.com/chroma-core/chroma/tree/main/docs/docs.trychroma.com)?_
1 parent a059f2c commit 54d8c8c

File tree

10 files changed

+270
-139
lines changed

10 files changed

+270
-139
lines changed

chromadb/test/distributed/test_task_api.py

Lines changed: 64 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@
77

88
import pytest
99
from chromadb.api.client import Client as ClientCreator
10-
from chromadb.api.functions import RECORD_COUNTER_FUNCTION, Function
10+
from chromadb.api.functions import (
11+
RECORD_COUNTER_FUNCTION,
12+
STATISTICS_FUNCTION,
13+
Function,
14+
)
1115
from chromadb.config import System
1216
from chromadb.errors import ChromaError, NotFoundError
1317
from chromadb.test.utils.wait_for_version_increase import (
@@ -181,18 +185,24 @@ def test_functions_one_attached_function_per_collection(
181185

182186
# Attempt to create a second task with a different name should fail
183187
# (only one attached function allowed per collection)
184-
with pytest.raises(ChromaError, match="already has an attached function"):
188+
with pytest.raises(
189+
ChromaError,
190+
match="collection already has an attached function: name=task_1, function=record_counter, output_collection=output_1",
191+
):
185192
collection.attach_function(
186193
function=RECORD_COUNTER_FUNCTION,
187194
name="task_2",
188195
output_collection="output_2",
189196
params=None,
190197
)
191198

192-
# Attempt to create a task with the same name but different params should also fail
193-
with pytest.raises(ChromaError, match="already exists"):
199+
# Attempt to create a task with the same name but different function_id should also fail
200+
with pytest.raises(
201+
ChromaError,
202+
match=r"collection already has an attached function: name=task_1, function=record_counter, output_collection=output_1",
203+
):
194204
collection.attach_function(
195-
function=RECORD_COUNTER_FUNCTION,
205+
function=STATISTICS_FUNCTION,
196206
name="task_1",
197207
output_collection="output_different", # Different output collection
198208
params=None,
@@ -222,6 +232,55 @@ def test_functions_one_attached_function_per_collection(
222232
)
223233

224234

235+
def test_attach_function_with_invalid_params(basic_http_client: System) -> None:
236+
"""Test that attach_function with non-empty params raises an error"""
237+
client = ClientCreator.from_system(basic_http_client)
238+
client.reset()
239+
240+
collection = client.create_collection(name="test_invalid_params")
241+
collection.add(ids=["id1"], documents=["test document"])
242+
243+
# Attempt to create task with non-empty params should fail
244+
# (no functions currently accept parameters)
245+
with pytest.raises(
246+
ChromaError,
247+
match="params must be empty - no functions currently accept parameters",
248+
):
249+
collection.attach_function(
250+
name="invalid_params_task",
251+
function=RECORD_COUNTER_FUNCTION,
252+
output_collection="output_collection",
253+
params={"some_key": "some_value"},
254+
)
255+
256+
257+
def test_attach_function_output_collection_already_exists(
258+
basic_http_client: System,
259+
) -> None:
260+
"""Test that attach_function fails when output collection name already exists"""
261+
client = ClientCreator.from_system(basic_http_client)
262+
client.reset()
263+
264+
# Create a collection that will be used as input
265+
input_collection = client.create_collection(name="input_collection")
266+
input_collection.add(ids=["id1"], documents=["test document"])
267+
268+
# Create another collection with the name we want to use for output
269+
client.create_collection(name="existing_output_collection")
270+
271+
# Attempt to create task with output collection name that already exists
272+
with pytest.raises(
273+
ChromaError,
274+
match=r"Output collection \[existing_output_collection\] already exists",
275+
):
276+
input_collection.attach_function(
277+
name="my_task",
278+
function=RECORD_COUNTER_FUNCTION,
279+
output_collection="existing_output_collection",
280+
params=None,
281+
)
282+
283+
225284
def test_function_remove_nonexistent(basic_http_client: System) -> None:
226285
"""Test removing a task that doesn't exist raises NotFoundError"""
227286
client = ClientCreator.from_system(basic_http_client)

go/pkg/sysdb/coordinator/create_task_test.go

Lines changed: 14 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -147,9 +147,7 @@ func (suite *AttachFunctionTestSuite) TestAttachFunction_SuccessfulCreation() {
147147
MinRecordsForInvocation := uint64(100)
148148

149149
params := &structpb.Struct{
150-
Fields: map[string]*structpb.Value{
151-
"param1": structpb.NewStringValue("value1"),
152-
},
150+
Fields: map[string]*structpb.Value{},
153151
}
154152

155153
request := &coordinatorpb.AttachFunctionRequest{
@@ -241,7 +239,7 @@ func (suite *AttachFunctionTestSuite) TestAttachFunction_IdempotentRequest_Alrea
241239
tenantID := "test-tenant"
242240
databaseName := "test-database"
243241
databaseID := "database-uuid"
244-
functionID := uuid.New()
242+
functionID := dbmodel.FunctionRecordCounter
245243
MinRecordsForInvocation := uint64(100)
246244

247245
params := &structpb.Struct{
@@ -288,10 +286,8 @@ func (suite *AttachFunctionTestSuite) TestAttachFunction_IdempotentRequest_Alrea
288286
suite.mockAttachedFunctionDb.On("GetByCollectionID", inputCollectionID).
289287
Return([]*dbmodel.AttachedFunction{existingAttachedFunction}, nil).Once()
290288

291-
// Validate function by ID
292-
suite.mockMetaDomain.On("FunctionDb", txCtx).Return(suite.mockFunctionDb).Once()
293-
suite.mockFunctionDb.On("GetByID", functionID).
294-
Return(&dbmodel.Function{ID: functionID, Name: functionName}, nil).Once()
289+
// Note: validateAttachedFunctionMatchesRequest uses dbmodel.GetFunctionNameByID (static lookup),
290+
// not FunctionDb.GetByID, so no mock needed for FunctionDb here
295291

296292
// Validate database matches
297293
suite.mockMetaDomain.On("DatabaseDb", txCtx).Return(suite.mockDatabaseDb).Once()
@@ -317,7 +313,6 @@ func (suite *AttachFunctionTestSuite) TestAttachFunction_IdempotentRequest_Alrea
317313
// Verify all read mocks were called
318314
suite.mockMetaDomain.AssertExpectations(suite.T())
319315
suite.mockAttachedFunctionDb.AssertExpectations(suite.T())
320-
suite.mockFunctionDb.AssertExpectations(suite.T())
321316
suite.mockDatabaseDb.AssertExpectations(suite.T())
322317
}
323318

@@ -339,16 +334,10 @@ func (suite *AttachFunctionTestSuite) TestAttachFunction_RecoveryFlow() {
339334
tenantID := "test-tenant"
340335
databaseName := "test-database"
341336
databaseID := "database-uuid"
342-
functionID := uuid.New()
337+
functionID := dbmodel.FunctionRecordCounter
343338
MinRecordsForInvocation := uint64(100)
344339
now := time.Now()
345340

346-
params := &structpb.Struct{
347-
Fields: map[string]*structpb.Value{
348-
"param1": structpb.NewStringValue("value1"),
349-
},
350-
}
351-
352341
request := &coordinatorpb.AttachFunctionRequest{
353342
Name: attachedFunctionName,
354343
InputCollectionId: inputCollectionID,
@@ -357,7 +346,6 @@ func (suite *AttachFunctionTestSuite) TestAttachFunction_RecoveryFlow() {
357346
TenantId: tenantID,
358347
Database: databaseName,
359348
MinRecordsForInvocation: MinRecordsForInvocation,
360-
Params: params,
361349
}
362350

363351
// ========== FIRST ATTEMPT: Heap Push Fails ==========
@@ -423,10 +411,8 @@ func (suite *AttachFunctionTestSuite) TestAttachFunction_RecoveryFlow() {
423411
suite.mockAttachedFunctionDb.On("GetByCollectionID", inputCollectionID).
424412
Return([]*dbmodel.AttachedFunction{incompleteAttachedFunction}, nil).Once()
425413

426-
// Validate function matches
427-
suite.mockMetaDomain.On("FunctionDb", txCtx).Return(suite.mockFunctionDb).Once()
428-
suite.mockFunctionDb.On("GetByID", functionID).
429-
Return(&dbmodel.Function{ID: functionID, Name: functionName}, nil).Once()
414+
// Note: validateAttachedFunctionMatchesRequest uses dbmodel.GetFunctionNameByID (static lookup),
415+
// not FunctionDb.GetByID, so no mock needed for FunctionDb here
430416

431417
// Validate database matches
432418
suite.mockMetaDomain.On("DatabaseDb", txCtx).Return(suite.mockDatabaseDb).Once()
@@ -467,7 +453,7 @@ func (suite *AttachFunctionTestSuite) TestAttachFunction_IdempotentRequest_Param
467453
tenantID := "test-tenant"
468454
databaseName := "test-database"
469455
databaseID := "database-uuid"
470-
existingOperatorID := uuid.New()
456+
existingOperatorID := dbmodel.FunctionRecordCounter
471457
MinRecordsForInvocation := uint64(100)
472458
now := time.Now()
473459

@@ -514,31 +500,22 @@ func (suite *AttachFunctionTestSuite) TestAttachFunction_IdempotentRequest_Param
514500
suite.mockAttachedFunctionDb.On("GetByCollectionID", inputCollectionID).
515501
Return([]*dbmodel.AttachedFunction{existingAttachedFunction}, nil).Once()
516502

517-
// Validate function - returns DIFFERENT function name
518-
suite.mockMetaDomain.On("FunctionDb", txCtx).Return(suite.mockFunctionDb).Once()
519-
suite.mockFunctionDb.On("GetByID", existingOperatorID).
520-
Return(&dbmodel.Function{
521-
ID: existingOperatorID,
522-
Name: existingOperatorName, // Different from request
523-
}, nil).Once()
524-
525-
// Database lookup happens before the error is returned
526-
suite.mockMetaDomain.On("DatabaseDb", txCtx).Return(suite.mockDatabaseDb).Once()
527-
suite.mockDatabaseDb.On("GetDatabases", tenantID, databaseName).
528-
Return([]*dbmodel.Database{{ID: databaseID, Name: databaseName}}, nil).Once()
503+
// Note: validateAttachedFunctionMatchesRequest uses dbmodel.GetFunctionNameByID (static lookup)
504+
// which returns "record_counter" for FunctionRecordCounter - different from requestedOperatorName
505+
// Validation returns (false, nil) early, so DatabaseDb.GetDatabases is NOT called
529506

530507
_ = txFunc(txCtx)
531-
}).Return(status.Errorf(codes.AlreadyExists, "different function is attached with this name: existing=%s, requested=%s", existingOperatorName, requestedOperatorName)).Once()
508+
}).Return(status.Errorf(codes.AlreadyExists, "collection already has an attached function: name=%s, function=%s, output_collection=%s", attachedFunctionName, existingOperatorName, outputCollectionName)).Once()
532509

533510
// Execute AttachFunction
534511
response, err := suite.coordinator.AttachFunction(ctx, request)
535512

536513
// Assertions - should fail with AlreadyExists error
537514
suite.Error(err)
538515
suite.Nil(response)
539-
suite.Contains(err.Error(), "different function is attached with this name")
516+
suite.Contains(err.Error(), "collection already has an attached function")
540517
suite.Contains(err.Error(), existingOperatorName)
541-
suite.Contains(err.Error(), requestedOperatorName)
518+
suite.Contains(err.Error(), outputCollectionName)
542519

543520
// Verify no writes occurred (Transaction IS called but Insert/Push are not)
544521
suite.mockTxImpl.AssertNumberOfCalls(suite.T(), "Transaction", 1)
@@ -547,7 +524,6 @@ func (suite *AttachFunctionTestSuite) TestAttachFunction_IdempotentRequest_Param
547524
// Verify read mocks were called
548525
suite.mockMetaDomain.AssertExpectations(suite.T())
549526
suite.mockAttachedFunctionDb.AssertExpectations(suite.T())
550-
suite.mockFunctionDb.AssertExpectations(suite.T())
551527
}
552528

553529
func TestAttachFunctionTestSuite(t *testing.T) {

0 commit comments

Comments
 (0)