Files
QuoteForge/bible-local/03-database.md
2026-03-07 23:18:07 +03:00

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, and local_partnumber_book_items are 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_bom
  • qt_lot_bundles
  • qt_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:

  1. Read active or target book from qt_partnumber_books.
  2. Parse partnumbers_json.
  3. Load payloads from qt_partnumber_book_items WHERE partnumber IN (...).

Pricelist stock enrichment contract:

  1. Sync pulls base pricelist rows from qt_pricelist_items.
  2. Sync reads latest stock quantities from stock_log.
  3. Sync resolves partnumber -> lot through the local mirror of qt_partnumber_book_items (local_partnumber_book_items.lots_json).
  4. Sync stores enriched available_qty and partnumbers into local_pricelist_items.

Runtime rule:

  • pricelist UI and quote logic read only local_pricelist_items;
  • runtime code must not query stock_log, qt_pricelist_items, or qt_partnumber_book_items directly outside sync.

qt_partnumber_book_items no longer contains book_id or lot_name. It stores one row per partnumber with:

  • partnumber
  • lots_json as [{"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:

  1. blocked readiness state from local_sync_guard_state
  2. latest non-empty pending_changes.last_error
  3. NULL when 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_json stores PN membership for a pulled book.
  • local_partnumber_book_items is a deduplicated local catalog by partnumber.
  • local_partnumber_book_items.lots_json mirrors the server lots_json payload.
  • SQLite migration 2026_03_07_local_partnumber_book_catalog rebuilds old book_id + lot_name rows 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 -migrate flag
  • 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"