11 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 |
Read-only cache contract:
local_components,local_pricelists,local_pricelist_items,local_partnumber_books, andlocal_partnumber_book_itemsare synchronized caches, not user-authored data.- Startup must prefer application availability over preserving a broken cache schema.
- If one of these tables cannot be migrated safely, the client may quarantine or drop it and recreate it empty; the next sync repopulates it.
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 |
Canonical PN catalog rows | id, partnumber, lots_json, description |
Active book: WHERE is_active=1 ORDER BY created_at DESC, id DESC LIMIT 1
Configurations and Projects
| Table | Purpose | Key Fields |
|---|---|---|
local_configurations |
Saved configurations | id, uuid (unique), items (JSON), vendor_spec (JSON: PN/qty/description + canonical lot_mappings[]), 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 |
stock_log |
Latest stock qty by partnumber (pricelist enrichment during sync only) | SELECT |
qt_configurations |
Saved configurations (includes line_no) |
SELECT, INSERT, UPDATE |
qt_projects |
Projects | SELECT, INSERT, UPDATE |
qt_client_schema_state |
Applied migrations state + client operational status per username + hostname |
SELECT, INSERT, UPDATE |
qt_pricelist_sync_status |
Pricelist sync status | SELECT, INSERT, UPDATE |
qt_partnumber_books |
Partnumber book headers with snapshot membership in partnumbers_json (written by PriceForge) |
SELECT |
qt_partnumber_book_items |
Canonical PN catalog with lots_json composition (written by PriceForge) |
SELECT |
qt_vendor_partnumber_seen |
Vendor PN tracking for unresolved/ignored BOM rows (is_ignored) |
INSERT only for new partnumber; existing rows must not be modified |
Legacy server tables not used by QuoteForge runtime anymore:
qt_bomqt_lot_bundlesqt_lot_bundle_items
QuoteForge canonical BOM storage is:
qt_configurations.vendor_spec- row-level PN -> multiple LOT decomposition in
vendor_spec[].lot_mappings[]
Partnumber book server read contract:
- Read active or target book from
qt_partnumber_books. - Parse
partnumbers_json. - Load payloads from
qt_partnumber_book_items WHERE partnumber IN (...).
Pricelist stock enrichment contract:
- Sync pulls base pricelist rows from
qt_pricelist_items. - Sync reads latest stock quantities from
stock_log. - Sync resolves
partnumber -> lotthrough the local mirror ofqt_partnumber_book_items(local_partnumber_book_items.lots_json). - Sync stores enriched
available_qtyandpartnumbersintolocal_pricelist_items.
Runtime rule:
- pricelist UI and quote logic read only
local_pricelist_items; - runtime code must not query
stock_log,qt_pricelist_items, orqt_partnumber_book_itemsdirectly outside sync.
qt_partnumber_book_items no longer contains book_id or lot_name.
It stores one row per partnumber with:
partnumberlots_jsonas[{"lot_name":"CPU_X","qty":2}, ...]description
qt_client_schema_state current contract:
- identity key:
username + hostname - client/runtime state:
app_version,last_checked_at,updated_at - operational state:
last_sync_at,last_sync_status - queue health:
pending_changes_count,pending_errors_count - local dataset size:
configurations_count,projects_count - price context:
estimate_pricelist_version,warehouse_pricelist_version,competitor_pricelist_version - last known sync problem:
last_sync_error_code,last_sync_error_text
last_sync_error_* source priority:
- blocked readiness state from
local_sync_guard_state - latest non-empty
pending_changes.last_error NULLwhen no known sync problem exists
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 ON RFQ_LOG.stock_log 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, 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>'@'%';
GRANT SELECT ON RFQ_LOG.qt_partnumber_books TO '<DB_USER>'@'%';
GRANT SELECT ON RFQ_LOG.qt_partnumber_book_items TO '<DB_USER>'@'%';
GRANT INSERT, UPDATE ON RFQ_LOG.qt_vendor_partnumber_seen 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 ON RFQ_LOG.stock_log 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, 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'@'%';
GRANT SELECT ON RFQ_LOG.qt_partnumber_books TO 'quote_user'@'%';
GRANT SELECT ON RFQ_LOG.qt_partnumber_book_items TO 'quote_user'@'%';
GRANT INSERT, UPDATE ON RFQ_LOG.qt_vendor_partnumber_seen TO 'quote_user'@'%';
FLUSH PRIVILEGES;
SHOW GRANTS FOR 'quote_user'@'%';
Note: If pricelists sync but stock enrichment is empty, verify SELECT on qt_pricelist_items, qt_partnumber_books, qt_partnumber_book_items, and stock_log.
Note: If you see Access denied for user ...@'<ip>', check for conflicting user entries (user@localhost vs user@'%').
Migrations
SQLite Migrations (local) — два уровня, выполняются при каждом старте
1. GORM AutoMigrate (internal/localdb/localdb.go) — первый и основной уровень.
Список Go-моделей передаётся в db.AutoMigrate(...). GORM создаёт отсутствующие таблицы и добавляет новые колонки. Колонки и таблицы не удаляет.
Local SQLite partnumber book cache contract:
local_partnumber_books.partnumbers_jsonstores PN membership for a pulled book.local_partnumber_book_itemsis a deduplicated local catalog bypartnumber.local_partnumber_book_items.lots_jsonmirrors the serverlots_jsonpayload.- SQLite migration
2026_03_07_local_partnumber_book_catalogrebuilds oldbook_id + lot_namerows into the new local cache shape. → Для добавления новой таблицы или колонки достаточно добавить модель/поле и включить модель в AutoMigrate.
2. runLocalMigrations (internal/localdb/migrations.go) — второй уровень, для операций которые AutoMigrate не умеет: backfill данных, пересоздание таблиц, создание индексов.
Каждая функция выполняется один раз — идемпотентность через запись id в local_schema_migrations.
QuoteForge does not use centralized server-driven SQLite migrations. All local SQLite schema/data migrations live in the client codebase.
MariaDB Migrations (server-side)
- Stored in
migrations/(SQL files) - Applied via
-migrateflag 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"