# دیتابیس — تسک ۰۶

این تسک **جدول جدیدی نمی‌سازد** جز کش. مصرف‌کنندهٔ جدول‌های تسک ۰۲/۰۳ و
`resource_occupancy` تسک ۰۷ است.

⚠️ **وابستگی معکوس:** موتور جستجو به `resource_occupancy` نیاز دارد ولی آن جدول در تسک ۰۷
ساخته می‌شود. راه‌حل: **مهاجرت جدول `resource_occupancy` در همین تسک انجام شود** و تسک ۰۷
فقط منطق نوشتن در آن را اضافه کند. تعریف کامل جدول در
[task-07/database.md](../task-07-hold-and-book/database.md) است؛ اینجا فقط ایندکس‌های
لازم برای خواندن ذکر می‌شوند.

## ایندکس‌های حیاتی خواندن

```sql
-- کوئری داغ: اشغال‌های این منابع در این بازه
KEY idx_occupancy_resource_range (resource_id, start_at, end_at, status)
```

پرس‌وجو:

```sql
SELECT resource_id, start_at, end_at, units
FROM resource_occupancy
WHERE resource_id IN (?, ?, …)
  AND start_at < :to AND end_at > :from
  AND status IN ('hold', 'booked')
```

`resource_id` ستون اول است چون `IN` روی آن، محدودکننده‌ترین شرط است. اضافه کردن
`entity_type` به ابتدای این ایندکس **اشتباه** است: اینجا فیلتر tenant از راه منابع
(که خودشان محیط دارند) اعمال شده و ستون tenant-پیشرو فقط ایندکس را بی‌اثر می‌کند.
یک ایندکس دوم tenant-پیشرو برای لیست‌های پنل جدا تعریف می‌شود (تسک ۰۷).

## تنظیمات جدید روی `weekly_schedules.setting.meta`

بدون تغییر schema (ستون JSON موجود):

```json
{
  "meta": {
    "booking_mode": "resource",
    "slot_granularity": 15,
    "picker_strategy": "least_gap",
    "buffer_minutes": 0
  }
}
```

`setMeta()` باید کلیدهای جدید را با اعتبارسنجی بپذیرد:
- `slot_granularity` ∈ {5, 10, 15, 20, 30, 60}
- `picker_strategy` ∈ کلیدهای ثبت‌شدهٔ `ResourcePickerInterface`

مقدار نامعتبر → مقدار فعلی حفظ می‌شود (همان الگوی موجود `setMeta`).

## کش

Redis، بدون جدول. کلیدها:

```
cp:avail:res:{resourceId}:win:{Y-m-d}     → JSON بازه‌های آزاد   TTL تا پایان روز
cp:avail:month:{branchId}:{serviceId}:{Y-m} → JSON بولین per روز  TTL 300s
```

ابطال:

| رویداد | کلیدهای باطل |
|---|---|
| تغییر `resource_calendars` | همهٔ `win` آن منبع |
| ثبت/حذف `resource_exceptions` | `win` آن منبع در بازهٔ استثنا |
| تغییر `branch_working_hours` | `win` همهٔ منابع آن شعبه |
| تغییر `tenant_holiday_overrides` | `win` همهٔ منابع آن محیط در آن روز |
| ثبت/لغو نوبت | فقط `month` — **`win` هرگز** (اشغال کش نمی‌شود) |

آخرین سطر مهم‌ترین است: اگر کسی وسوسه شد اشغال را هم کش کند، نتیجه‌اش نمایش وقتِ
گرفته‌شده و شکست رزرو در مرحلهٔ آخر است.

## تست کارایی — داده مصنوعی

```bash
ddev exec php bin/console app:dev:seed-availability-benchmark --force
```

می‌سازد:
- ۱ شعبه · ۳ اتاق (ظرفیت ۱) · ۲ اپراتور · ۳ دستگاه در یک استخر
- ۱ سرویس با ۴ بخش (سناریوی مستند)
- ۵۰۰ نوبت پراکنده در ۳۰ روز آینده

`AvailabilityPerformanceTest` روی همین داده اجرا می‌شود و **دو** چیز را می‌سنجد:

```php
self::assertLessThan(500, $elapsedMs, 'جستجوی ۳۰ روزه باید زیر نیم ثانیه باشد');
self::assertLessThanOrEqual(5, $queryCount, 'تعداد کوئری نباید با تعداد روز رشد کند');
```

شرط دوم مهم‌تر از اولی است: زمان روی ماشین‌های مختلف فرق می‌کند، تعداد کوئری نه.
