fix(db): scope row locks to base records

This commit is contained in:
lizerk 2026-07-20 14:52:17 +03:00
parent 716c8337de
commit e421a1b55c
5 changed files with 24 additions and 13 deletions

View File

@ -178,7 +178,7 @@ def create_qr_login_token(
def exchange_qr_login_token(*, raw_token: str, request) -> tuple[str, str]:
token_hash = hash_qr_login_token(raw_token)
qr_token = (
QrLoginToken.objects.select_for_update()
QrLoginToken.objects.select_for_update(of=("self",))
.select_related("user")
.filter(token_hash=token_hash)
.first()
@ -330,7 +330,7 @@ def poll_qr_login_request(
request,
) -> dict[str, str | None]:
qr_request = (
QrLoginRequest.objects.select_for_update()
QrLoginRequest.objects.select_for_update(of=("self",))
.select_related("approved_by")
.filter(
client_secret_hash=hash_qr_login_request_client_secret(raw_client_secret),

View File

@ -150,7 +150,9 @@ def _audit_api_mutation(*, actor, action, instance, period=None, metadata=None):
def _lock_period_accepting_draft_edits(period_id):
period = get_object_or_404(
PayrollPeriod.objects.select_for_update().select_related("current_run"),
PayrollPeriod.objects.select_for_update(of=("self",)).select_related(
"current_run"
),
pk=period_id,
)
if period.status == PayrollPeriodStatus.CLOSED:
@ -699,7 +701,12 @@ class DraftDetailView(PayrollAdminAPIView):
period = _lock_period_accepting_draft_edits(reference.period_id)
else:
period = None
instance = get_object_or_404(scoped_queryset.select_for_update())
# Read serializers join nullable audit/revision relations. PostgreSQL
# cannot apply FOR UPDATE to the nullable side of those outer joins,
# and only the draft itself needs to be locked here.
instance = get_object_or_404(
scoped_queryset.select_for_update(of=("self",))
)
if period is not None:
instance._state.fields_cache["period"] = period
self.validate_period_update(instance, period)
@ -1001,7 +1008,9 @@ class EmployeePayRateReviseView(PayrollAdminAPIView):
def post(self, request, pk):
self.require_permission(request, PAYROLL_PERMISSIONS["manage_inputs"])
source = get_object_or_404(
EmployeePayRate.objects.select_for_update().select_related("employee"),
EmployeePayRate.objects.select_for_update(of=("self",)).select_related(
"employee"
),
pk=pk,
status=ApprovalStatus.APPROVED,
)
@ -1071,7 +1080,7 @@ class PayrollWorkRecordReviseView(PayrollAdminAPIView):
)
period = _lock_period_accepting_draft_edits(reference.period_id)
source = get_object_or_404(
PayrollWorkRecord.objects.select_for_update().select_related(
PayrollWorkRecord.objects.select_for_update(of=("self",)).select_related(
"period", "employee"
),
pk=pk,

View File

@ -600,7 +600,7 @@ class TaskViewSet(viewsets.ModelViewSet):
visible_task = self.get_object()
with transaction.atomic():
task = (
Task.objects.select_for_update()
Task.objects.select_for_update(of=("self",))
.select_related("board", "column", "assignee")
.get(id=visible_task.id)
)

View File

@ -433,8 +433,10 @@ def build_attendance_work_preview(
"employee", "employee__position", "created_by"
)
if lock:
attendance = attendance.select_for_update()
work = work.select_for_update()
# Both projections contain nullable outer joins (analysis run and
# employee position). Lock only the source rows being imported.
attendance = attendance.select_for_update(of=("self",))
work = work.select_for_update(of=("self",))
attendance_records = list(attendance.order_by("employee_id", "date", "id"))
work_records = list(work.order_by("employee_id", "revision", "id"))
@ -594,7 +596,7 @@ def apply_attendance_work_preview(
)
period = (
PayrollPeriod.objects.select_for_update()
PayrollPeriod.objects.select_for_update(of=("self",))
.select_related("current_run")
.get(pk=period_id)
)

View File

@ -757,7 +757,7 @@ def bulk_set_point_rate(
selected_employee_ids = sorted(set(employee_ids))
rates = list(
EmployeePayRate.objects.select_for_update()
EmployeePayRate.objects.select_for_update(of=("self",))
.filter(
employee_id__in=selected_employee_ids,
rate_code=base_rate_code(),
@ -984,7 +984,7 @@ def bulk_set_pay_rate(
).order_by("pk")
}
rates = list(
EmployeePayRate.objects.select_for_update()
EmployeePayRate.objects.select_for_update(of=("self",))
.filter(
employee_id__in=employees,
rate_code=base_rate_code(),
@ -1374,7 +1374,7 @@ def save_own_daily_work_entry(
"Для сохранения выработки необходимо войти в систему.",
)
period = (
PayrollPeriod.objects.select_for_update()
PayrollPeriod.objects.select_for_update(of=("self",))
.select_related("current_run")
.get(pk=period_id)
)