{"record":{"id":"d813ce10d8dec9e6","repo":"gastownhall/beads","slug":"schema-release-migration-lock-w-returned-null","errorCode":null,"errorMessage":"schema: release migration lock: %w: returned NULL","messagePattern":"schema: release migration lock: %w: returned NULL","errorType":"exception","errorClass":"ErrMigrationLockRelease","httpStatus":null,"severity":"warning","filePath":"internal/storage/schema/lock.go","lineNumber":376,"sourceCode":"\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}\n\tif !released.Valid {\n\t\tdiscardConn(conn)\n\t\treturn fmt.Errorf(\"schema: release migration lock: %w: returned NULL\", ErrMigrationLockRelease)\n\t}\n\tif released.Int64 != 1 {\n\t\tdiscardConn(conn)\n\t\treturn fmt.Errorf(\"schema: release migration lock: %w: returned %d\", ErrMigrationLockRelease, released.Int64)\n\t}\n\treturn nil\n}\n\nfunc discardConn(conn *sql.Conn) {\n\t_ = conn.Raw(func(driverConn any) error {\n\t\treturn driver.ErrBadConn\n\t})\n}\n","sourceCodeStart":358,"sourceCodeEnd":390,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/schema/lock.go#L358-L390","documentation":"RELEASE_LOCK returned NULL, which in MySQL semantics means the current session does not own the named lock (or the lock does not exist). The library wraps ErrMigrationLockRelease and discards the connection since ownership cannot be established. It indicates the acquire/release pairing was broken.","triggerScenarios":"Calling ReleaseMigrationLock on a connection different from the one that called AcquireMigrationLock; the acquiring session already timed out or was reset; the lock was already released on this session.","commonSituations":"Connection-pool mixing — re-acquiring a *sql.Conn from the pool instead of reusing the pinned conn; a prior release attempt that half-succeeded; process reconnect after a transient network error replaced the session.","solutions":["Ensure the same *sql.Conn used for AcquireMigrationLock is passed to ReleaseMigrationLock (pin it across the migration).","Treat NULL as benign if the lock ownership already ended with session teardown; log and proceed.","Audit code paths that may release the lock twice or release before the migration finishes.","Add logging/correlation of conn identity between acquire and release to spot pool mixing."],"exampleFix":"// before: conn fetched fresh from pool for release\nreleaseConn, _ := db.Conn(ctx)\nschema.ReleaseMigrationLock(releaseConn, lockName)\n// after: reuse the pinned acquisition connection\nschema.ReleaseMigrationLock(pinnedConn, lockName)","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"if err := schema.ReleaseMigrationLock(conn, name); err != nil {\n    if errors.Is(err, schema.ErrMigrationLockRelease) && strings.Contains(err.Error(), \"returned NULL\") {\n        // we no longer owned the lock; log and continue\n    }\n}","preventionTips":["Pair each Acquire with exactly one Release on the same conn.","Never fetch a fresh conn from the pool for release.","Skip release if acquire failed.","Track lock ownership in code (bool) before releasing."],"tags":["database","locking","session","dolt"],"backgroundTag":"lock-not-owned","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}