wavetermdev/waveterm · error

cannot update %T value with empty id

Error message

cannot update %T value with empty id

What it means

DBUpdate persists a WaveObj keyed by its OID. If the object's ID is empty there is no primary key to update, so the call fails fast with the concrete Go type named in the message. It guards against upserting orphan rows that could never be read back.

Source

Thrown at pkg/wstore/wstore_dbops.go:304

		defer func() {
			panichandler.PanicHandler("DBDelete:filestore.DeleteZone", recover())
		}()
		// we spawn a go routine here because we don't want to reuse the DB connection
		// since DBDelete is called in a transaction from DeleteTab
		deleteCtx, cancelFn := context.WithTimeout(context.Background(), 2*time.Second)
		defer cancelFn()
		err := filestore.WFS.DeleteZone(deleteCtx, id)
		if err != nil {
			log.Printf("error deleting filestore zone (after deleting block): %v", err)
		}
	}()
	return nil
}

func DBUpdate(ctx context.Context, val waveobj.WaveObj) error {
	oid := waveobj.GetOID(val)
	if oid == "" {
		return fmt.Errorf("cannot update %T value with empty id", val)
	}
	jsonData, err := waveobj.ToJson(val)
	if err != nil {
		return err
	}
	return WithTx(ctx, func(tx *TxWrap) error {
		table := waveObjTableName(val)
		query := fmt.Sprintf("UPDATE %s SET data = ?, version = version+1 WHERE oid = ? RETURNING version", table)
		newVersion := tx.GetInt(query, jsonData, oid)
		waveobj.SetVersion(val, newVersion)
		waveobj.ContextAddUpdate(ctx, waveobj.WaveObjUpdate{UpdateType: waveobj.UpdateType_Update, OType: val.GetOType(), OID: oid, Obj: val})
		return nil
	})
}

func DBUpdateFn[T waveobj.WaveObj](ctx context.Context, id string, updateFn func(T)) error {
	return WithTx(ctx, func(tx *TxWrap) error {
		val, err := DBMustGet[T](tx.Context(), id)

View on GitHub (pinned to a4447c1563)

Solutions

  1. Assign a valid OID (e.g. via waveobj.GenWaveOType/GenerateOID helpers used elsewhere) before calling DBUpdate
  2. Use DBInsert instead of DBUpdate if the object is new and has no ID yet
  3. Check waveobj.GetOID(obj) != "" before persisting
  4. Fix deserialization so the oid field is preserved when loading objects

Example fix

// before
blk := &waveobj.Block{Meta: meta}
err := wstore.DBUpdate(ctx, blk)
// after
blk := &waveobj.Block{OID: waveobj.GenOID(), Meta: meta}
err := wstore.DBInsert(ctx, blk) // or set the existing OID before DBUpdate
Defensive patterns

Strategy: validation

Validate before calling

if waveobj.GetOID(obj) == "" {
	return fmt.Errorf("object %T missing oid before update", obj)
}

Type guard

func hasOID(w waveobj.WaveObj) bool { return waveobj.GetOID(w) != "" }

Try / catch

if err := wstore.DBUpdate(ctx, obj); err != nil {
	if strings.Contains(err.Error(), "empty id") {
		return fmt.Errorf("%T was not assigned an id", obj)
	}
	return err
}

Prevention

When it happens

Trigger: Passing a newly-constructed WaveObj (Tab, Block, Window, etc.) with its OID field never set, or an object deserialized from JSON that lacked an oid field, into wstore.DBUpdate.

Common situations: Building a waveobj struct literal and forgetting to assign the generated ID; copying fields but not the OID between objects; a constructor that returns the zero value on an error path.

Related errors


AI-assisted analysis of wavetermdev/waveterm@a4447c1563 (2026-09-01). Data as JSON: /api/errors/f39f446ac5d55cd0. Report an issue: GitHub.