{"record":{"id":"7de43016b678a4ed","repo":"wavetermdev/waveterm","slug":"cannot-insert-t-value-with-empty-id","errorCode":null,"errorMessage":"cannot insert %T value with empty id","messagePattern":"cannot insert %T value with empty id","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/wstore/wstore_dbops.go","lineNumber":348,"sourceCode":"\nfunc DBUpdateFnErr[T waveobj.WaveObj](ctx context.Context, id string, updateFn func(T) error) error {\n\treturn WithTx(ctx, func(tx *TxWrap) error {\n\t\tval, err := DBMustGet[T](tx.Context(), id)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\terr = updateFn(val)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn DBUpdate(tx.Context(), val)\n\t})\n}\n\nfunc DBInsert(ctx context.Context, val waveobj.WaveObj) error {\n\toid := waveobj.GetOID(val)\n\tif oid == \"\" {\n\t\treturn fmt.Errorf(\"cannot insert %T value with empty id\", val)\n\t}\n\tjsonData, err := waveobj.ToJson(val)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn WithTx(ctx, func(tx *TxWrap) error {\n\t\ttable := waveObjTableName(val)\n\t\twaveobj.SetVersion(val, 1)\n\t\tquery := fmt.Sprintf(\"INSERT INTO %s (oid, version, data) VALUES (?, ?, ?)\", table)\n\t\ttx.Exec(query, oid, 1, jsonData)\n\t\twaveobj.ContextAddUpdate(ctx, waveobj.WaveObjUpdate{UpdateType: waveobj.UpdateType_Update, OType: val.GetOType(), OID: oid, Obj: val})\n\t\treturn nil\n\t})\n}\n\nfunc DBFindTabForBlockId(ctx context.Context, blockId string) (string, error) {\n\treturn WithTxRtn(ctx, func(tx *TxWrap) (string, error) {\n\t\titerNum := 1","sourceCodeStart":330,"sourceCodeEnd":366,"githubUrl":"https://github.com/wavetermdev/waveterm/blob/a4447c1563b2df285ab89e76c82f91e1a1a49c1e/pkg/wstore/wstore_dbops.go#L330-L366","documentation":"DBInsert writes a new WaveObj row keyed by its OID. An empty ID would produce an unusable row, so the call is rejected immediately with the offending Go type in the message. Every stored wave object must carry a non-empty unique identifier.","triggerScenarios":"Calling wstore.DBInsert with a WaveObj whose OID was never generated — e.g. a struct literal, an error path returning a zero value, or a caller in StartJob/CreateClient/InitMainServer-style flows building the object before ID assignment.","commonSituations":"Forgetting waveobj.GenOID() (or equivalent) on new objects; refactoring a constructor so ID assignment was dropped; creating objects from templates that omit the oid field.","solutions":["Set the OID before insert (the project's ID generator, e.g. waveobj.GenOID())","Assert oid != \"\" in your factory/helper that returns new wave objects","Use a constructor function that always assigns an ID instead of raw struct literals"],"exampleFix":"// before\nws := &waveobj.Workspace{Name: \"default\"}\nwstore.DBInsert(ctx, ws)\n// after\nws := &waveobj.Workspace{OID: waveobj.GenOID(), Name: \"default\"}\nwstore.DBInsert(ctx, ws)","handlingStrategy":"validation","validationCode":"if waveobj.GetOID(obj) == \"\" {\n\tobj.SetOID(waveobj.GenOID())\n}","typeGuard":"func insertable(w waveobj.WaveObj) bool { return waveobj.GetOID(w) != \"\" }","tryCatchPattern":"if err := wstore.DBInsert(ctx, obj); err != nil {\n\tif strings.Contains(err.Error(), \"cannot insert\") {\n\t\treturn fmt.Errorf(\"factory returned %T without id\", obj)\n\t}\n\treturn err\n}","preventionTips":["Assign waveobj.GenOID() when constructing new objects","Centralize object creation in factories that always set IDs","Unit-test that factories never return zero-value objects"],"tags":["validation","waveobj","primary-key"],"backgroundTag":"missing-object-id","analyzedSha":"a4447c1563b2df285ab89e76c82f91e1a1a49c1e","analyzedAt":"2026-09-01T15:26:23.972Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}