Database Organization and Schema ================================== This guide provides comprehensive documentation of the database structure, relationships, migration system, and data integrity constraints for developers. .. contents:: Contents :local: :depth: 2 --- Database Overview ----------------- Technology Stack ~~~~~~~~~~~~~~~~ **Database**: PostgreSQL 15+ - Chosen for: ACID compliance, JSON support, robust indexing, university standard **ORM**: Drizzle ORM - Type-safe schema definitions - Migration support - SQL-first approach **Schema Location**: ``/shared/schema.ts`` - Single source of truth for database structure - TypeScript types generated from schema - Used by both server and client --- Schema Overview and ERD ----------------------- Interactive ERD Viewer ~~~~~~~~~~~~~~~~~~~~~~ The system includes an **interactive Entity Relationship Diagram** accessible in the application: 1. Navigate to **Settings** → **Database Schema** (admin only) 2. Click **"Entity Relationship Diagram"** tab 3. Interactive SVG diagram shows: - All tables with columns - Primary keys (🔑) - Foreign keys (🔗) - Relationships with crow's foot notation - Color-coded by relationship type **ERD File**: ``/client/src/components/DatabaseERD.tsx`` Core Tables ~~~~~~~~~~~ The database consists of **15 core tables** organized into functional groups: **1. User Management** (3 tables): - ``users`` - User accounts and authentication - ``permissions`` - 45 granular permissions - ``charge_code_assignments`` - User charge code authorization (permission system) **2. Inventory** (4 tables): - ``items`` - Inventory items with location/unit fields - ``categories`` - Hierarchical item organization - ``stock_movements`` - Complete audit trail for all stock changes - ``notes`` - Flexible notes system (linked to multiple entities) **3. Sales & Quotes** (4 tables): - ``quotes`` - Draft and saved quotes - ``quote_items`` - Items in quotes with snapshots - ``sales`` - Completed sales - ``sale_items`` - Items in sales with snapshots **4. Procurement** (4 tables): - ``orders`` - Purchase orders - ``order_items`` - Items in orders - ``suppliers`` - Vendor information - ``sources`` - Item-supplier relationships (many-to-many) **5. Financial Control** (3 tables): - ``chargecodes`` - Budget codes for sales - ``charge_code_exclusions`` - Category restrictions per code - ``charge_code_assignments`` - User authorization per code --- Table Schemas (Detailed) ------------------------- Users Table ~~~~~~~~~~~ **Purpose**: User accounts, authentication, and role-based access control **Schema**: .. code-block:: typescript users { id: varchar (PK) - Unique user ID email: varchar (unique) - Login email / username name: varchar - Full name role: varchar - 'user', 'superuser' (manager), 'admin' hashedPassword: varchar - bcrypt hashed password createdAt: timestamp - Account creation updatedAt: timestamp - Last modification } **Indexes**: - PRIMARY KEY (id) - UNIQUE (email) **Relationships**: - → charge_code_assignments (user_id, assigned_by) - → notes (created_by) - → items (created_by, updated_by) - → sales (processed_by) - → quotes (created_by, processed_by) - → orders (created_by) - → stock_movements (performed_by) - → chargecodes (authorised_by) **Key Constraints**: - Email must be valid format - Role must be one of: 'user', 'superuser', 'admin' - Passwords min 8 characters (enforced in application) Items Table ~~~~~~~~~~~ **Purpose**: Inventory items with location tracking and measurement units **Schema**: .. code-block:: typescript items { id: serial (PK) - Auto-increment item ID name: varchar - Item name/description sku: varchar (unique) - Stock keeping unit category_id: integer (FK) → categories.id - Item category price: decimal(10,2) - Sale price (GBP) vatRate: decimal(5,4) - VAT rate (e.g., 0.2000 for 20%) vatIncluded: boolean - true = price includes VAT currentStock: decimal(10,2) - Current stock level minimumStock: decimal(10,2) - Low-stock threshold location: varchar - Physical storage location **[ADDED 2025]** unit: varchar - Measurement unit (pieces, kg, meters) **[ADDED 2025]** description: text - Detailed description notes_id: integer (FK) → notes.id (nullable) - Linked notes isActive: boolean - Soft delete flag createdAt: timestamp updatedAt: timestamp created_by: varchar (FK) → users.id updated_by: varchar (FK) → users.id (nullable) } **Indexes**: - PRIMARY KEY (id) - UNIQUE (sku) - INDEX (category_id) - INDEX (isActive) - INDEX (currentStock) - For low-stock queries **Critical Fields**: **location** (Physical Tracking): - Format: "Building/Room, Area, Specific Location" - Example: "Lab Store, Shelf 3A, Bin 05" - Shown on picking lists when processing sales - Admin-only visibility in inventory list - Free text for flexibility **unit** (Measurement Type): - Specifies how quantities are measured - Example values: "pieces", "meters", "kg", "liters" - Allows fractional quantities (2.5 meters) - Prevents interpretation errors - Included in sale item snapshots **vatIncluded** (Price Interpretation): - true: Price £120 means customer pays £120 (VAT backwards-calculated) - false: Price £100 + 20% VAT = £120 total (VAT forward-calculated) - Critical for correct pricing Sales and Quotes Tables ~~~~~~~~~~~~~~~~~~~~~~~~ **Purpose**: Two-tier system for quotes (draft/saved) and completed sales **quotes** Table: .. code-block:: typescript quotes { id: serial (PK) quote_name: varchar (nullable) - Name for saved quotes customer_name: varchar (nullable) customer_email: varchar (nullable) customer_department: varchar (nullable) charge_code: varchar (nullable) - For draft quotes status: varchar - 'draft', 'saved', 'completed' session_id: varchar (nullable) - For draft quotes (4hr expiry) created_by: varchar (FK) → users.id processed_by: varchar (FK) → users.id (nullable) created_at: timestamp processed_at: timestamp (nullable) notes_id: integer (FK) → notes.id (nullable) } **quote_items** Table: .. code-block:: typescript quote_items { id: serial (PK) quote_id: integer (FK) → quotes.id (ON DELETE CASCADE) item_id: integer (FK) → items.id item_name: varchar - Snapshot item_sku: varchar - Snapshot quantity: decimal(10,2) unit_price: decimal(10,2) - Snapshot vat_rate: decimal(5,4) - Snapshot subtotal: decimal(10,2) total_with_vat: decimal(10,2) } **sales** Table: .. code-block:: typescript sales { id: varchar (PK) - Generated ID (e.g., S202501291234) charge_code: varchar - REQUIRED, validated customer_name: varchar (nullable) customer_email: varchar (nullable) customer_department: varchar (nullable) status: varchar - 'completed', 'paid' is_paid: boolean - Payment reconciliation flag total_amount: decimal(10,2) processed_by: varchar (FK) → users.id created_at: timestamp notes_id: integer (FK) → notes.id (nullable) } **sale_items** Table: .. code-block:: typescript sale_items { id: serial (PK) sale_id: varchar (FK) → sales.id (ON DELETE CASCADE) item_id: integer (FK) → items.id item_name: varchar - Immutable snapshot item_sku: varchar - Immutable snapshot quantity: decimal(10,2) unit_price: decimal(10,2) - Price at sale time vat_rate: decimal(5,4) - VAT at sale time subtotal: decimal(10,2) total_with_vat: decimal(10,2) } **Key Concepts**: **Snapshots**: - Quote/sale items capture ``item_name``, ``item_sku``, ``unit_price``, ``vat_rate`` - Immutable historical record - Prevents confusion if item details change later **Draft vs Saved Quotes**: - Draft: ``status = 'draft'``, ``session_id`` populated, 4hr expiry - Saved: ``status = 'saved'``, ``quote_name`` populated, persistent **Completed vs Paid Sales**: - Completed: ``status = 'completed'``, stock deducted, ``is_paid = false`` - Paid: ``is_paid = true``, accounting reconciliation complete Charge Code Tables (Permission System) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ **chargecodes** Table: .. code-block:: typescript chargecodes { code: varchar (PK) - Unique code (e.g., "CS-2025-Q1") description: varchar validFrom: date (nullable) - Start of validity validUntil: date (nullable) - End of validity isOnHold: boolean - Temporary freeze holdReason: varchar (nullable) - Why on hold pin: varchar (nullable) - Optional PIN protection authorised_by: varchar (FK) → users.id (nullable) notes_id: integer (FK) → notes.id (nullable) createdAt: timestamp } **charge_code_exclusions** Table: .. code-block:: typescript charge_code_exclusions { id: serial (PK) charge_code: varchar (FK) → chargecodes.code (ON DELETE CASCADE) category_id: integer (FK) → categories.id (ON DELETE CASCADE) item_id: integer (FK) → items.id (ON DELETE CASCADE, nullable) created_by: varchar (FK) → users.id createdAt: timestamp UNIQUE (charge_code, category_id, item_id) } **Purpose**: Prevent certain charge codes from being used with certain item categories **Example**: "Office Supplies" charge code cannot be used to purchase "IT Equipment" **charge_code_assignments** Table **[ADDED 2025]**: .. code-block:: typescript charge_code_assignments { id: serial (PK) user_id: varchar (FK) → users.id (ON DELETE CASCADE) charge_code: varchar (FK) → chargecodes.code (ON DELETE CASCADE) assigned_by: varchar (FK) → users.id (nullable) assigned_at: timestamp - Default NOW() notes: text (nullable) UNIQUE (user_id, charge_code) } **Purpose**: Maps users to authorized charge codes (permission system) **Indexes**: - INDEX (user_id) - Fast lookup of user's codes - INDEX (charge_code) - Fast lookup of code's users **Usage**: - Basic users restricted to assigned codes only - Managers/admins have access to all codes (no restrictions) Stock Movements Table (Audit Trail) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ **Purpose**: Complete audit trail for all inventory movements (compliance) **Schema**: .. code-block:: typescript stock_movements { id: serial (PK) item_id: integer (FK) → items.id (ON DELETE CASCADE) type: varchar - 'in', 'out', 'adjustment' quantity: decimal(10,2) - Change amount (+ or -) previous_stock: decimal(10,2) - Stock before new_stock: decimal(10,2) - Stock after reason: text - Detailed explanation performed_by: varchar (FK) → users.id timestamp: timestamp - Default NOW() } **Indexes**: - PRIMARY KEY (id) - INDEX (item_id) - Fast item history lookup - INDEX (timestamp) - Chronological queries **Movement Types**: - ``in``: Stock received (from purchase orders) - ``out``: Stock sold (from sales) - ``adjustment``: Manual correction (physical count, damage, etc.) **Reason Examples**: - "Received Order #O202501151230" - "Sale #S202501201445" - "Physical count found 2 damaged units" **Compliance**: Every stock change MUST have a stock movement record --- Table Relationships and Dependencies ------------------------------------- Relationship Types ~~~~~~~~~~~~~~~~~~ **One-to-Many** (most common): - One parent → many children - Example: One category → many items - Implemented with foreign key on child table **Many-to-Many**: - Through junction table - Example: Items ↔ Suppliers (via ``sources`` table) **One-to-One** (optional): - Shared notes system - Example: Category → notes (nullable) Foreign Key Cascade Rules ~~~~~~~~~~~~~~~~~~~~~~~~~~ **ON DELETE CASCADE**: - When parent deleted, children automatically deleted - Used for: quote_items, sale_items, order_items, stock_movements - Prevents orphaned records **ON DELETE RESTRICT** (default): - Cannot delete parent if children exist - Used for: categories (can't delete if items reference it) **Nullable Foreign Keys**: - Optional relationships - Example: ``items.notes_id`` (items don't require notes) Migration Dependency Order ~~~~~~~~~~~~~~~~~~~~~~~~~~ When migrating data or creating fresh database, follow this order: **Tier 1 - No Dependencies**: 1. ``users`` - Base user accounts **Tier 2 - Core References**: 2. ``notes`` - Depends on users (created_by) 3. ``categories`` - Depends on notes (notes_id) 4. ``suppliers`` - Depends on notes (notes_id) 5. ``permissions`` - Independent permission list **Tier 3 - Primary Data**: 6. ``chargecodes`` - Depends on users (authorised_by), notes 7. ``items`` - Depends on categories, users, notes **Tier 4 - Junction/Assignment Tables**: 8. ``sources`` - Depends on items, suppliers 9. ``charge_code_exclusions`` - Depends on chargecodes, categories, items 10. ``charge_code_assignments`` - **Depends on users, chargecodes** [ADDED 2025] **Tier 5 - Transactional Data**: 11. ``quotes`` → ``quote_items`` - Depends on users, items 12. ``sales`` → ``sale_items`` - Depends on users, items, chargecodes 13. ``orders`` → ``order_items`` - Depends on users, suppliers, items 14. ``stock_movements`` - Depends on items, users .. important:: **Critical**: Always migrate in this order to avoid foreign key constraint violations! --- Migration System ---------------- Migration File Structure ~~~~~~~~~~~~~~~~~~~~~~~~ **Location**: ``/migrations/`` **Naming Convention**: ``###_description.sql`` **Examples**: - ``001_initial_schema.sql`` - ``012_add_location_unit_to_items.sql`` - ``013_add_charge_code_assignments.sql`` **Sequential Numbering**: Ensures correct application order Migration Tracking ~~~~~~~~~~~~~~~~~~ **applied_migrations Table**: .. code-block:: sql CREATE TABLE IF NOT EXISTS applied_migrations ( id SERIAL PRIMARY KEY, migration_name VARCHAR(255) UNIQUE NOT NULL, applied_at TIMESTAMP DEFAULT NOW() ); **Purpose**: Tracks which migrations have been applied **Usage**: Migration script checks this table before running each migration Migration Script ~~~~~~~~~~~~~~~~ **File**: ``/data_migration_script.py`` **Functions**: 1. **Auto-Detection**: Scans ``/migrations/`` directory 2. **Sequential Application**: Applies migrations in numbered order 3. **Idempotent**: Won't re-apply already applied migrations 4. **Error Handling**: Rolls back on failure **Running Migrations**: .. code-block:: bash # Manual migration run python3 data_migration_script.py # Automatic (Docker startup) # Runs automatically in init.sql during container startup Docker Integration ~~~~~~~~~~~~~~~~~~ **init.sql** (Docker Initialization): - Creates all tables if they don't exist - Runs on fresh Docker container startup - Location: ``/init.sql`` **Production Deployments**: - Use ``data_migration_script.py`` for existing databases - Migration files auto-detected and applied - Safe for production (checks ``applied_migrations`` table) Writing New Migrations ~~~~~~~~~~~~~~~~~~~~~~~ **Best Practices**: 1. **Sequential Numbering**: Find highest number, increment 2. **Descriptive Name**: Clear purpose (e.g., ``014_add_index_to_items_sku.sql``) 3. **Idempotent**: Use ``IF NOT EXISTS`` where possible 4. **Test First**: Run on development database before production 5. **Rollback Plan**: Document how to reverse if needed **Example Migration**: .. code-block:: sql -- File: 014_add_index_to_items_sku.sql -- Description: Add index on items.sku for faster searches -- Create index if it doesn't exist CREATE INDEX IF NOT EXISTS idx_items_sku ON items(sku); -- Record migration INSERT INTO applied_migrations (migration_name) VALUES ('014_add_index_to_items_sku.sql') ON CONFLICT (migration_name) DO NOTHING; --- Indexing Strategy ----------------- Primary Indexes ~~~~~~~~~~~~~~~ **Auto-Created**: - All primary keys (``id``, ``code``, etc.) - All unique constraints (``email``, ``sku``, etc.) **Performance Impact**: O(log n) lookups on indexed columns Foreign Key Indexes ~~~~~~~~~~~~~~~~~~~ **Critical for Performance**: .. code-block:: sql CREATE INDEX idx_items_category_id ON items(category_id); CREATE INDEX idx_sale_items_sale_id ON sale_items(sale_id); CREATE INDEX idx_stock_movements_item_id ON stock_movements(item_id); CREATE INDEX idx_charge_code_assignments_user_id ON charge_code_assignments(user_id); **Why**: Foreign keys used frequently in JOINs Query Optimization Indexes ~~~~~~~~~~~~~~~~~~~~~~~~~~~ **Common Queries**: .. code-block:: sql -- Low stock queries CREATE INDEX idx_items_current_stock ON items(currentStock); -- Active items filter CREATE INDEX idx_items_is_active ON items(isActive); -- Chronological stock movements CREATE INDEX idx_stock_movements_timestamp ON stock_movements(timestamp); --- Data Integrity Constraints --------------------------- NOT NULL Constraints ~~~~~~~~~~~~~~~~~~~~ **Critical Fields**: - User: email, role, hashedPassword - Item: name, sku, price, currentStock - Sale: charge_code (REQUIRED!) - Stock Movement: item_id, type, reason **Purpose**: Prevent incomplete records UNIQUE Constraints ~~~~~~~~~~~~~~~~~~ **Enforced Uniqueness**: - ``users.email`` - One account per email - ``items.sku`` - One item per SKU - ``chargecodes.code`` - One charge code per code string - ``(user_id, charge_code)`` in charge_code_assignments **Purpose**: Data quality and referential integrity CHECK Constraints ~~~~~~~~~~~~~~~~~ **Application-Level** (not database-enforced in current schema): - Role must be: 'user', 'superuser', 'admin' - Stock movement type: 'in', 'out', 'adjustment' - Quote status: 'draft', 'saved', 'completed' **Validated in**: Application code before insert/update Referential Integrity ~~~~~~~~~~~~~~~~~~~~~ **Foreign Keys**: - Enforced by PostgreSQL - CASCADE deletes prevent orphans - Cannot insert child without parent --- Database Performance -------------------- Connection Pooling ~~~~~~~~~~~~~~~~~~ **Configuration**: ``server/storage.ts`` **Pool Settings**: - Min connections: 2 - Max connections: 10 - Idle timeout: 30 seconds **Why**: Reuse connections, reduce overhead Query Optimization ~~~~~~~~~~~~~~~~~~ **Prepared Statements**: - All queries use parameterized statements - Prevents SQL injection - Query plan caching **SELECT Optimization**: - Only select needed columns (avoid ``SELECT *``) - Use appropriate indexes - Limit result sets with ``LIMIT`` Transaction Management ~~~~~~~~~~~~~~~~~~~~~~ **Atomic Operations**: .. code-block:: typescript // Example: Processing quote into sale await db.transaction(async (tx) => { // 1. Create sale const sale = await tx.insert(sales).values({...}); // 2. Create sale items await tx.insert(saleItems).values([...]); // 3. Deduct stock await tx.update(items).set({currentStock: ...}); // 4. Create stock movements await tx.insert(stockMovements).values([...]); // All or nothing! }); **Purpose**: ACID guarantees, prevents partial updates --- Backup and Recovery ------------------- See: :doc:`/operations/system-recovery` for detailed backup procedures **Quick Backup**: .. code-block:: bash docker exec lustores_db pg_dump -U postgres inventory > backup.sql **Restore**: .. code-block:: bash cat backup.sql | docker exec -i lustores_db psql -U postgres inventory --- Additional Resources -------------------- **Related Documentation**: - :doc:`/reference/database-schema` - Full schema reference - :doc:`/explanations/concepts` - Business logic and data model - :doc:`/operations/system-recovery` - Backup and restore procedures - :doc:`/admin/backup-restore` - Detailed backup guide **In-App Tools**: - **Database ERD**: Settings → Database Schema → ERD tab - **Database Schema Viewer**: Settings → Database Schema → Schema Details tab - **Migration Guide**: Settings → Database Schema → Migration Guide tab **External Resources**: - `PostgreSQL Documentation `_ - `Drizzle ORM Documentation `_