perf: enable WAL mode, batch price lookup, add DB diagnostics to schema_state

- Set PRAGMA journal_mode=WAL + synchronous=NORMAL on SQLite open;
  eliminates read blocking during background pricelist sync writes
- Replace N+1 per-lot price loop in QuoteService local fallback with
  GetLocalPricesForLots batch query (120 queries → 3 per price-levels call)
- Add CountAllPricelistItems, CountComponents, DBFileSizeBytes to LocalDB
- Report local_pricelist_count, pricelist_items_count, components_count,
  db_size_bytes in qt_client_schema_state for performance diagnostics

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Mikhail Chusavitin
2026-04-02 10:53:36 +03:00
parent 1ea21ece33
commit a360992a01
3 changed files with 79 additions and 7 deletions

View File

@@ -116,6 +116,14 @@ func New(dbPath string) (*LocalDB, error) {
return nil, fmt.Errorf("opening sqlite database: %w", err)
}
// Enable WAL mode so background sync writes never block UI reads.
if err := db.Exec("PRAGMA journal_mode=WAL").Error; err != nil {
slog.Warn("failed to enable WAL mode", "error", err)
}
if err := db.Exec("PRAGMA synchronous=NORMAL").Error; err != nil {
slog.Warn("failed to set synchronous=NORMAL", "error", err)
}
if err := ensureLocalProjectsTable(db); err != nil {
return nil, fmt.Errorf("ensure local_projects table: %w", err)
}
@@ -1152,6 +1160,29 @@ func (l *LocalDB) CountLocalPricelists() int64 {
return count
}
// CountAllPricelistItems returns total rows across all local_pricelist_items.
func (l *LocalDB) CountAllPricelistItems() int64 {
var count int64
l.db.Model(&LocalPricelistItem{}).Count(&count)
return count
}
// CountComponents returns the number of rows in local_components.
func (l *LocalDB) CountComponents() int64 {
var count int64
l.db.Model(&LocalComponent{}).Count(&count)
return count
}
// DBFileSizeBytes returns the size of the SQLite database file in bytes.
func (l *LocalDB) DBFileSizeBytes() int64 {
info, err := os.Stat(l.path)
if err != nil {
return 0
}
return info.Size()
}
// GetLatestLocalPricelist returns the most recently synced pricelist
func (l *LocalDB) GetLatestLocalPricelist() (*LocalPricelist, error) {
var pricelist LocalPricelist
@@ -1319,6 +1350,32 @@ func (l *LocalDB) GetLocalPriceForLot(pricelistID uint, lotName string) (float64
return item.Price, nil
}
// GetLocalPricesForLots returns prices for multiple lots from a local pricelist in a single query.
// Uses the composite index (pricelist_id, lot_name). Missing lots are omitted from the result.
func (l *LocalDB) GetLocalPricesForLots(pricelistID uint, lotNames []string) (map[string]float64, error) {
result := make(map[string]float64, len(lotNames))
if len(lotNames) == 0 {
return result, nil
}
type row struct {
LotName string `gorm:"column:lot_name"`
Price float64 `gorm:"column:price"`
}
var rows []row
if err := l.db.Model(&LocalPricelistItem{}).
Select("lot_name, price").
Where("pricelist_id = ? AND lot_name IN ?", pricelistID, lotNames).
Find(&rows).Error; err != nil {
return nil, err
}
for _, r := range rows {
if r.Price > 0 {
result[r.LotName] = r.Price
}
}
return result, nil
}
// GetLocalLotCategoriesByServerPricelistID returns lot_category for each lot_name from a local pricelist resolved by server ID.
// Missing lots are not included in the map; caller is responsible for strict validation.
func (l *LocalDB) GetLocalLotCategoriesByServerPricelistID(serverPricelistID uint, lotNames []string) (map[string]string, error) {