Skip to content

Commit f181672

Browse files
committed
[ENH]: Make error messages in attach_function better
1 parent 7198072 commit f181672

File tree

9 files changed

+260
-138
lines changed

9 files changed

+260
-138
lines changed

chromadb/test/distributed/test_task_api.py

Lines changed: 64 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -173,19 +173,25 @@ def test_functions_one_attached_function_per_collection(
173173

174174
# Attempt to create a second task with a different name should fail
175175
# (only one attached function allowed per collection)
176-
with pytest.raises(ChromaError, match="already has an attached function"):
176+
with pytest.raises(
177+
ChromaError,
178+
match="collection already has an attached function: name=task_1, function=record_counter, output_collection=output_1",
179+
):
177180
collection.attach_function(
178181
name="task_2",
179182
function_id="record_counter",
180183
output_collection="output_2",
181184
params=None,
182185
)
183186

184-
# Attempt to create a task with the same name but different params should also fail
185-
with pytest.raises(ChromaError, match="already exists"):
187+
# Attempt to create a task with the same name but different function_id should also fail
188+
with pytest.raises(
189+
ChromaError,
190+
match=r"collection already has an attached function: name=task_1, function=record_counter, output_collection=output_1",
191+
):
186192
collection.attach_function(
187193
name="task_1",
188-
function_id="record_counter",
194+
function_id="statistics",
189195
output_collection="output_different", # Different output collection
190196
params=None,
191197
)
@@ -208,6 +214,55 @@ def test_functions_one_attached_function_per_collection(
208214
assert attached_fn2.detach(delete_output_collection=True) is True
209215

210216

217+
def test_attach_function_with_invalid_params(basic_http_client: System) -> None:
218+
"""Test that attach_function with non-empty params raises an error"""
219+
client = ClientCreator.from_system(basic_http_client)
220+
client.reset()
221+
222+
collection = client.create_collection(name="test_invalid_params")
223+
collection.add(ids=["id1"], documents=["test document"])
224+
225+
# Attempt to create task with non-empty params should fail
226+
# (no functions currently accept parameters)
227+
with pytest.raises(
228+
ChromaError,
229+
match="params must be empty - no functions currently accept parameters",
230+
):
231+
collection.attach_function(
232+
name="invalid_params_task",
233+
function_id="record_counter",
234+
output_collection="output_collection",
235+
params={"some_key": "some_value"},
236+
)
237+
238+
239+
def test_attach_function_output_collection_already_exists(
240+
basic_http_client: System,
241+
) -> None:
242+
"""Test that attach_function fails when output collection name already exists"""
243+
client = ClientCreator.from_system(basic_http_client)
244+
client.reset()
245+
246+
# Create a collection that will be used as input
247+
input_collection = client.create_collection(name="input_collection")
248+
input_collection.add(ids=["id1"], documents=["test document"])
249+
250+
# Create another collection with the name we want to use for output
251+
client.create_collection(name="existing_output_collection")
252+
253+
# Attempt to create task with output collection name that already exists
254+
with pytest.raises(
255+
ChromaError,
256+
match=r"Output collection \[existing_output_collection\] already exists",
257+
):
258+
input_collection.attach_function(
259+
name="my_task",
260+
function_id="record_counter",
261+
output_collection="existing_output_collection",
262+
params=None,
263+
)
264+
265+
211266
def test_function_remove_nonexistent(basic_http_client: System) -> None:
212267
"""Test removing a task that doesn't exist raises NotFoundError"""
213268
client = ClientCreator.from_system(basic_http_client)
@@ -228,6 +283,7 @@ def test_function_remove_nonexistent(basic_http_client: System) -> None:
228283
with pytest.raises(NotFoundError, match="does not exist"):
229284
attached_fn.detach(delete_output_collection=True)
230285

286+
231287
def test_attach_to_output_collection_fails(basic_http_client: System) -> None:
232288
"""Test that attaching a function to an output collection fails"""
233289
client = ClientCreator.from_system(basic_http_client)
@@ -245,7 +301,9 @@ def test_attach_to_output_collection_fails(basic_http_client: System) -> None:
245301
)
246302
output_collection = client.get_collection(name="output_collection")
247303

248-
with pytest.raises(ChromaError, match="cannot attach function to an output collection"):
304+
with pytest.raises(
305+
ChromaError, match="cannot attach function to an output collection"
306+
):
249307
_ = output_collection.attach_function(
250308
name="test_function_2",
251309
function_id="record_counter",
@@ -305,4 +363,4 @@ def test_delete_orphaned_output_collection(basic_http_client: System) -> None:
305363

306364
with pytest.raises(NotFoundError):
307365
# Try to use the function - it should fail since it's detached
308-
client.get_collection("output_collection")
366+
client.get_collection("output_collection")

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)