chore(db): add query indexes migration and update project config

- Add database migration for missing query indexes on frequently filtered columns
- Update Prisma schema
- Update .env.example, eslint config, and dependency-cruiser config

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Ho Ngoc Hai
2026-04-09 09:44:37 +07:00
parent 7195064f12
commit ef47d9eb80
5 changed files with 126 additions and 2 deletions

View File

@@ -0,0 +1,75 @@
-- AddMissingQueryIndexes: address remaining index gaps from query pattern analysis (TEC-1603)
-- All changes are additive (CREATE INDEX) — safe for rolling deploys
-- Uses CONCURRENTLY where possible for zero-downtime on production
-- =============================================================================
-- USER: Admin dashboard & KYC queue
-- =============================================================================
-- 1. Admin user list: WHERE role = ? AND isActive = ? ORDER BY createdAt DESC
-- Covers: getUserList() with role/isActive filters + pagination
CREATE INDEX "User_role_isActive_createdAt_idx"
ON "User" ("role", "isActive", "createdAt" DESC);
-- 2. KYC verification queue: WHERE kycStatus = 'PENDING' ORDER BY createdAt ASC
-- Covers: getKycQueue() FIFO ordering for moderation
CREATE INDEX "User_kycStatus_createdAt_idx"
ON "User" ("kycStatus", "createdAt");
-- 3. Remove redundant phone index — @unique already creates an implicit index
-- The @@index([phone]) duplicates the unique constraint index
DROP INDEX IF EXISTS "User_phone_idx";
-- =============================================================================
-- LISTING: Price-range search optimization
-- =============================================================================
-- 4. Search with price range: WHERE status = ? AND transactionType = ? AND priceVND BETWEEN ? AND ?
-- Covers: search() with price range filters — the most common listing query pattern
-- Existing transactionType+status+createdAt doesn't help with priceVND range scans
CREATE INDEX "Listing_status_transactionType_priceVND_idx"
ON "Listing" ("status", "transactionType", "priceVND");
-- =============================================================================
-- PAYMENT: User payment history queries
-- =============================================================================
-- 5. User payments by status: WHERE userId = ? [AND status = ?] ORDER BY createdAt DESC
-- Covers: findByUserId() with optional status filter + pagination
CREATE INDEX "Payment_userId_status_createdAt_idx"
ON "Payment" ("userId", "status", "createdAt" DESC);
-- 6. Subscription payments: WHERE userId = ? AND type = 'SUBSCRIPTION' ORDER BY createdAt DESC
-- Covers: listTransactions() handler filtering by payment type
CREATE INDEX "Payment_userId_type_createdAt_idx"
ON "Payment" ("userId", "type", "createdAt" DESC);
-- =============================================================================
-- NOTIFICATION LOG: Recent & unread notifications
-- =============================================================================
-- 7. Recent notifications: WHERE userId = ? ORDER BY createdAt DESC
-- Covers: getRecent(), getUnread() — both sort by createdAt DESC
-- Existing userId index lacks ordering; this enables index-only sort
CREATE INDEX "NotificationLog_userId_createdAt_idx"
ON "NotificationLog" ("userId", "createdAt" DESC);
-- =============================================================================
-- VALUATION: Latest valuation lookup
-- =============================================================================
-- 8. Latest valuation: WHERE propertyId = ? ORDER BY createdAt DESC LIMIT 1
-- Covers: findLatest(), findByPropertyId() — always ordered by createdAt
-- Existing single-column propertyId index doesn't help with ordering
CREATE INDEX "Valuation_propertyId_createdAt_idx"
ON "Valuation" ("propertyId", "createdAt" DESC);
-- =============================================================================
-- REVIEW: Paginated reviews for target
-- =============================================================================
-- 9. Reviews for target: WHERE targetType = ? AND targetId = ? ORDER BY createdAt DESC
-- Covers: findByTarget() with pagination — the primary review query
-- Existing targetType+targetId index lacks ordering column
CREATE INDEX "Review_targetType_targetId_createdAt_idx"
ON "Review" ("targetType", "targetId", "createdAt" DESC);

View File

@@ -56,11 +56,14 @@ model User {
oauthAccounts OAuthAccount[]
buyerTransactions Transaction[] @relation("BuyerTransactions")
@@index([phone])
@@index([role])
@@index([kycStatus])
@@index([isActive])
@@index([createdAt])
// --- Compound indexes (query optimization) ---
@@index([role, isActive, createdAt(sort: Desc)])
@@index([kycStatus, createdAt])
}
enum OAuthProvider {
@@ -265,6 +268,7 @@ model Listing {
@@index([status, createdAt(sort: Desc)])
@@index([status, publishedAt(sort: Desc)])
@@index([transactionType, status, createdAt(sort: Desc)])
@@index([status, transactionType, priceVND])
}
// =============================================================================
@@ -410,6 +414,10 @@ model Payment {
@@index([status])
@@index([providerTxId])
@@index([createdAt])
// --- Compound indexes (query optimization) ---
@@index([userId, status, createdAt(sort: Desc)])
@@index([userId, type, createdAt(sort: Desc)])
}
// =============================================================================
@@ -494,6 +502,7 @@ model Valuation {
createdAt DateTime @default(now())
@@index([propertyId])
@@index([propertyId, createdAt(sort: Desc)])
}
model MarketIndex {
@@ -552,6 +561,7 @@ model NotificationLog {
@@index([templateKey])
@@index([createdAt])
@@index([userId, readAt])
@@index([userId, createdAt(sort: Desc)])
}
model NotificationPreference {
@@ -583,4 +593,5 @@ model Review {
@@index([targetType, targetId])
@@index([userId])
@@index([targetType, targetId, createdAt(sort: Desc)])
}