Files
QuoteForge/bible/03-database.md
Michael Chus 5e56f386cc feat: implement vendor spec BOM import and PN→LOT resolution (Phase 1)
- Migration 029: local_partnumber_books, local_partnumber_book_items,
  vendor_spec TEXT column on local_configurations
- Models: LocalPartnumberBook, LocalPartnumberBookItem, VendorSpec,
  VendorSpecItem with JSON Valuer/Scanner
- Repository: PartnumberBookRepository (GetActiveBook, FindLotByPartnumber,
  SaveBook/Items, ListBooks, CountBookItems)
- Service: VendorSpecResolver 3-step resolution (book → manual suggestion
  → unresolved) + AggregateLOTs with is_primary_pn qty logic
- Sync: PullPartnumberBooks append-only pull from qt_partnumber_books
- Handlers: VendorSpecHandler (GET/PUT/resolve/apply), PartnumberBooksHandler
- Routes: /api/configs/:uuid/vendor-spec*, /api/partnumber-books,
  /api/sync/partnumber-books, /partnumber-books page
- UI: 3 top-level tabs [Estimate][BOM вендора][Ценообразование]; Excel paste,
  PN resolution, inline LOT autocomplete, pricing table
- Bible: 03-database.md updated, 09-vendor-spec.md added

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 10:22:22 +03:00

6.4 KiB

03 — Database

SQLite (local, client-side)

File: qfs.db in the user-state directory (see 05-config.md).

Tables

Components and Reference Data

Table Purpose Key Fields
local_components Component metadata (NO prices) lot_name (PK), lot_description, category, model
connection_settings MariaDB connection settings key-value store
app_settings Application settings key (PK), value, updated_at

Pricelists

Table Purpose Key Fields
local_pricelists Pricelist headers id, server_id (unique), source, version, created_at
local_pricelist_items Pricelist line items ← sole source of prices id, pricelist_id (FK), lot_name, price, lot_category

Partnumber Books (PN → LOT mapping, pull-only from PriceForge)

Table Purpose Key Fields
local_partnumber_books Version snapshots of PN→LOT mappings id, server_id (unique), version, created_at, is_active
local_partnumber_book_items PN→LOT mapping rows id, book_id (FK), partnumber, lot_name, is_primary_pn

Active book: WHERE is_active=1 ORDER BY created_at DESC, id DESC LIMIT 1

is_primary_pn=1 means this PN's quantity in the vendor BOM determines qty(LOT). If only non-primary PNs are present for a LOT, qty defaults to 1.

Configurations and Projects

Table Purpose Key Fields
local_configurations Saved configurations id, uuid (unique), items (JSON), vendor_spec (JSON), line_no, pricelist_id, warehouse_pricelist_id, competitor_pricelist_id, current_version_id, sync_status
local_configuration_versions Immutable snapshots (revisions) id, configuration_id (FK), version_no, data (JSON), change_note, created_at
local_projects Projects id, uuid (unique), name, code, sync_status

Sync

Table Purpose
pending_changes Queue of changes to push to MariaDB
local_schema_migrations Applied migrations (idempotency guard)

Key SQLite Indexes

-- Pricelists
INDEX local_pricelist_items(pricelist_id)
UNIQUE INDEX local_pricelists(server_id)
INDEX local_pricelists(source, created_at)   -- used for "latest by source" queries
-- latest-by-source runtime query also applies deterministic tie-break by id DESC

-- Configurations
INDEX local_configurations(pricelist_id)
INDEX local_configurations(warehouse_pricelist_id)
INDEX local_configurations(competitor_pricelist_id)
INDEX local_configurations(project_uuid, line_no)  -- project ordering (Line column)
UNIQUE INDEX local_configurations(uuid)

items JSON Structure in Configurations

{
  "items": [
    {
      "lot_name": "CPU_AMD_9654",
      "quantity": 2,
      "unit_price": 123456.78,
      "section": "Processors"
    }
  ]
}

Prices are stored inside the items JSON field and refreshed from the pricelist on configuration refresh.


MariaDB (server-side, sync-only)

Database: RFQ_LOG

Tables and Permissions

Table Purpose Permissions
lot Component catalog SELECT
qt_lot_metadata Extended component data SELECT
qt_categories Component categories SELECT
qt_pricelists Pricelists SELECT
qt_pricelist_items Pricelist line items SELECT
qt_configurations Saved configurations (includes line_no) SELECT, INSERT, UPDATE
qt_projects Projects SELECT, INSERT, UPDATE
qt_client_local_migrations Migration catalog SELECT only
qt_client_schema_state Applied migrations state SELECT, INSERT, UPDATE
qt_pricelist_sync_status Pricelist sync status SELECT, INSERT, UPDATE

Grant Permissions to Existing User

GRANT SELECT ON RFQ_LOG.lot TO '<DB_USER>'@'%';
GRANT SELECT ON RFQ_LOG.qt_lot_metadata TO '<DB_USER>'@'%';
GRANT SELECT ON RFQ_LOG.qt_categories TO '<DB_USER>'@'%';
GRANT SELECT ON RFQ_LOG.qt_pricelists TO '<DB_USER>'@'%';
GRANT SELECT ON RFQ_LOG.qt_pricelist_items TO '<DB_USER>'@'%';

GRANT SELECT, INSERT, UPDATE ON RFQ_LOG.qt_configurations TO '<DB_USER>'@'%';
GRANT SELECT, INSERT, UPDATE ON RFQ_LOG.qt_projects TO '<DB_USER>'@'%';

GRANT SELECT ON RFQ_LOG.qt_client_local_migrations TO '<DB_USER>'@'%';
GRANT SELECT, INSERT, UPDATE ON RFQ_LOG.qt_client_schema_state TO '<DB_USER>'@'%';
GRANT SELECT, INSERT, UPDATE ON RFQ_LOG.qt_pricelist_sync_status TO '<DB_USER>'@'%';

FLUSH PRIVILEGES;

Create a New User

CREATE USER IF NOT EXISTS 'quote_user'@'%' IDENTIFIED BY '<DB_PASSWORD>';

GRANT SELECT ON RFQ_LOG.lot TO 'quote_user'@'%';
GRANT SELECT ON RFQ_LOG.qt_lot_metadata TO 'quote_user'@'%';
GRANT SELECT ON RFQ_LOG.qt_categories TO 'quote_user'@'%';
GRANT SELECT ON RFQ_LOG.qt_pricelists TO 'quote_user'@'%';
GRANT SELECT ON RFQ_LOG.qt_pricelist_items TO 'quote_user'@'%';
GRANT SELECT, INSERT, UPDATE ON RFQ_LOG.qt_configurations TO 'quote_user'@'%';
GRANT SELECT, INSERT, UPDATE ON RFQ_LOG.qt_projects TO 'quote_user'@'%';
GRANT SELECT ON RFQ_LOG.qt_client_local_migrations TO 'quote_user'@'%';
GRANT SELECT, INSERT, UPDATE ON RFQ_LOG.qt_client_schema_state TO 'quote_user'@'%';
GRANT SELECT, INSERT, UPDATE ON RFQ_LOG.qt_pricelist_sync_status TO 'quote_user'@'%';

FLUSH PRIVILEGES;
SHOW GRANTS FOR 'quote_user'@'%';

Note: If you see Access denied for user ...@'<ip>', check for conflicting user entries (user@localhost vs user@'%').


Migrations

SQLite Migrations (local)

  • Stored in migrations/ (SQL files)
  • Applied via -migrate flag or automatically on first run
  • Idempotent: checked by id in local_schema_migrations
  • Already-applied migrations are skipped
go run ./cmd/qfs -migrate

Centralized Migrations (server-side)

  • Stored in qt_client_local_migrations (MariaDB)
  • Applied automatically during sync readiness check
  • min_app_version — minimum app version required for the migration

DB Debugging

# Inspect schema
sqlite3 ~/.local/state/quoteforge/qfs.db ".schema local_components"
sqlite3 ~/.local/state/quoteforge/qfs.db ".schema local_configurations"

# Check pricelist item count
sqlite3 ~/.local/state/quoteforge/qfs.db "SELECT COUNT(*) FROM local_pricelist_items"

# Check pending sync queue
sqlite3 ~/.local/state/quoteforge/qfs.db "SELECT COUNT(*) FROM pending_changes"