{"record":{"id":"f5e545656f005035","repo":"gastownhall/beads","slug":"schema-acquire-migration-lock-w-returned-null","errorCode":null,"errorMessage":"schema: acquire migration lock: %w: returned NULL","messagePattern":"schema: acquire migration lock: %w: returned NULL","errorType":"exception","errorClass":"ErrMigrationLockUnavailable","httpStatus":null,"severity":"warning","filePath":"internal/storage/schema/lock.go","lineNumber":355,"sourceCode":"\t).Scan(&ancestorCount); err != nil {\n\t\treturn false\n\t}\n\tif ancestorCount != 1 {\n\t\treturn false\n\t}\n\n\treturn c.consumed.CompareAndSwap(false, true)\n}\n\n// AcquireMigrationLock acquires the named schema migration lock on the pinned\n// connection's current Dolt/MySQL session.\nfunc AcquireMigrationLock(ctx context.Context, conn *sql.Conn, lockName string) error {\n\tvar locked sql.NullInt64\n\tif err := conn.QueryRowContext(ctx, \"SELECT GET_LOCK(?, ?)\", lockName, migrationLockAcquireTimeoutSeconds).Scan(&locked); err != nil {\n\t\treturn fmt.Errorf(\"schema: acquire migration lock: %w: %w\", ErrMigrationLockUnavailable, err)\n\t}\n\tif !locked.Valid {\n\t\treturn fmt.Errorf(\"schema: acquire migration lock: %w: returned NULL\", ErrMigrationLockUnavailable)\n\t}\n\tif locked.Int64 != 1 {\n\t\treturn fmt.Errorf(\"schema: acquire migration lock: %w: timeout\", ErrMigrationLockUnavailable)\n\t}\n\treturn nil\n}\n\n// ReleaseMigrationLock releases the named schema migration lock from the same\n// pinned Dolt/MySQL session used to acquire it.\nfunc ReleaseMigrationLock(conn *sql.Conn, lockName string) error {\n\tcleanupCtx, cancel := context.WithTimeout(context.Background(), migrationLockCleanupTimeout)\n\tdefer cancel()\n\n\tvar released sql.NullInt64\n\tif err := conn.QueryRowContext(cleanupCtx, \"SELECT RELEASE_LOCK(?)\", lockName).Scan(&released); err != nil {\n\t\tdiscardConn(conn)\n\t\treturn fmt.Errorf(\"schema: release migration lock: %w: %w\", ErrMigrationLockRelease, err)\n\t}","sourceCodeStart":337,"sourceCodeEnd":373,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/schema/lock.go#L337-L373","documentation":"GET_LOCK returned NULL instead of 0/1, which MySQL/Dolt uses to signal an error condition (e.g. the lock name was NULL/invalid or an out-of-memory/server error). The library wraps ErrMigrationLockUnavailable so callers treat it like any other transient lock-acquisition failure.","triggerScenarios":"AcquireMigrationLock called with an empty or invalid lockName, or a server-side error during GET_LOCK makes it return NULL. MigrationLockName normally guarantees a valid 64-char-max name, so NULL usually indicates a server problem or a hand-built name.","commonSituations":"Passing an empty database name into a custom lock name; non-ASCII/oversized lock names from custom callers; Dolt server-side anomalies reproducing MySQL NULL semantics.","solutions":["Use schema.MigrationLockName(databaseName) to build the lock name instead of a hand-rolled string.","Verify databaseName passed to MigrateUpWithLock is non-empty.","Check server logs for the underlying GET_LOCK error condition.","Retry via IsMigrationLockError — the sentinel marks it retryable."],"exampleFix":"// before\nschema.AcquireMigrationLock(ctx, conn, db) // raw db name as lock name\n// after\nlockName := schema.MigrationLockName(db)\nerr := schema.AcquireMigrationLock(ctx, conn, lockName)","handlingStrategy":"validation","validationCode":"if db == \"\" { return errors.New(\"database name required for migration lock\") }\nlockName := schema.MigrationLockName(db) // guarantees valid, ≤64-byte name\nif len(lockName) == 0 || len(lockName) > 64 { return fmt.Errorf(\"bad lock name %q\", lockName) }","typeGuard":"func isLockUnavailable(err error) bool { return errors.Is(err, schema.ErrMigrationLockUnavailable) }","tryCatchPattern":"if err := schema.AcquireMigrationLock(ctx, conn, lockName); err != nil {\n    if schema.IsMigrationLockError(err) { /* retry with backoff or abort gracefully */ }\n    return err\n}","preventionTips":["Always derive lock names via schema.MigrationLockName, never hand-build them.","Reject empty database names before calling MigrateUpWithLock.","Check server logs when GET_LOCK returns NULL — it indicates a server-side error.","Treat the error as retryable via the ErrMigrationLockUnavailable sentinel."],"tags":["go","database","dolt","locking","retryable"],"backgroundTag":"migration-lock-unavailable","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}