System Overview - Visual Guide

Progressive deep-dive from 30,000ft view down to implementation details.


Level 0: Complete System Architecture

┌─────────────────────────────────────────────────────────────────┐
│                        CLIENT APPLICATION                        │
│              SQL queries, DML operations, scans                  │
└────────────────────────────┬────────────────────────────────────┘
                             │
        ┌────────────────────┼────────────────────┐
        │                    │                    │
        ▼                    ▼                    ▼
┌───────────────┐   ┌────────────────┐   ┌─────────────────┐
│ SQL Executor  │   │ Direct Ops     │   │ Writer          │
│ (SELECT)      │   │ (legacy scans) │   │ (DML mutations) │
└───────┬───────┘   └────────┬───────┘   └────────┬────────┘
        │                    │                     │
        └────────────────────┼─────────────────────┘
                             │
                ┌────────────┼────────────┐
                │            │            │
                ▼            ▼            ▼
        ┌────────────┬────────────┬────────────┐
        │ PageDir    │ PageHandler│ Allocator  │
        │ (metadata) │ (caches)   │ (disk)     │
        └────────────┴────────────┴────────────┘
                             │
                             ▼
                    ┌─────────────────┐
                    │ Persistent      │
                    │ Storage         │
                    │ (data files)    │
                    └─────────────────┘

Level 1: SQL Query Execution Flow

SELECT Path (Columnar Batch Processing)

╔═══════════════════════════════════════════════════════════════╗
║ SQL: "SELECT dept, AVG(salary) FROM employees                ║
║       WHERE age > 25 GROUP BY dept ORDER BY dept"            ║
╚═════════════════════════╤═════════════════════════════════════╝
                          │
                          ▼
╔═══════════════════════════════════════════════════════════════╗
║                    SQL PARSER (sqlparser)                     ║
║  • Tokenize: [SELECT, dept, ',', AVG, '(', salary, ...)      ║
║  • Parse: Statement::Query(...)                              ║
║  • Output: AST (Abstract Syntax Tree)                        ║
╚═════════════════════════╤═════════════════════════════════════╝
                          │ AST
                          ▼
╔═══════════════════════════════════════════════════════════════╗
║              SQL EXECUTOR (executor/mod.rs)                   ║
║                                                               ║
║  Phase 1: Planning                                            ║
║    • Identify required columns: [dept, salary, age]          ║
║    • Detect aggregates: [AVG(salary)]                        ║
║    • Extract WHERE filters: [age > 25]                       ║
║    • Extract GROUP BY keys: [dept]                           ║
║    • Extract ORDER BY: [dept ASC]                            ║
╚═════════════════════════╤═════════════════════════════════════╝
                          │
                          ▼
╔═══════════════════════════════════════════════════════════════╗
║  Phase 2: Data Loading                                        ║
║    ┌──────────────────────────────────────────┐              ║
║    │ PageDirectory::locate_range(table, ...)  │              ║
║    │   → Returns Vec<PageDescriptor>          │              ║
║    └───────────────┬──────────────────────────┘              ║
║                    ▼                                          ║
║    ┌──────────────────────────────────────────┐              ║
║    │ PageHandler::get_pages(descriptors)      │              ║
║    │   → Returns Vec<Arc<Page>>               │              ║
║    └───────────────┬──────────────────────────┘              ║
║                    ▼                                          ║
║    ┌──────────────────────────────────────────┐              ║
║    │ Convert to ColumnarBatch                 │              ║
║    │   Page(Vec<Entry>) → ColumnarBatch       │              ║
║    │     columns: {                            │              ║
║    │       0 → ColumnarPage {                  │              ║
║    │             data: Text(["eng","sales"]),  │              ║
║    │             null_bitmap: [0,0]            │              ║
║    │           }                               │              ║
║    │       1 → ColumnarPage {                  │              ║
║    │             data: Int64([25, 30, ...]),   │              ║
║    │             null_bitmap: [0,0]            │              ║
║    │           }                               │              ║
║    │       2 → ColumnarPage {                  │              ║
║    │             data: Int64([80000, 90000])   │              ║
║    │           }                               │              ║
║    │     }                                     │              ║
║    └──────────────────────────────────────────┘              ║
╚═════════════════════════╤═════════════════════════════════════╝
                          │ ColumnarBatch (1000 rows)
                          ▼
╔═══════════════════════════════════════════════════════════════╗
║  Phase 3: WHERE Filtering (batch.rs + expressions.rs)         ║
║    ┌──────────────────────────────────────────┐              ║
║    │ evaluate_selection_on_batch(age > 25)    │              ║
║    │   → Bitmap: [1,1,0,1,0,1,1,0,...]        │              ║
║    └───────────────┬──────────────────────────┘              ║
║                    ▼                                          ║
║    ┌──────────────────────────────────────────┐              ║
║    │ selected_indices = bitmap.iter_ones()    │              ║
║    │   → [0, 1, 3, 5, 6, ...]                 │              ║
║    └───────────────┬──────────────────────────┘              ║
║                    ▼                                          ║
║    ┌──────────────────────────────────────────┐              ║
║    │ batch = batch.gather(selected_indices)   │              ║
║    │   → ColumnarBatch with 650 rows          │              ║
║    └──────────────────────────────────────────┘              ║
╚═════════════════════════╤═════════════════════════════════════╝
                          │ Filtered batch (650 rows)
                          ▼
╔═══════════════════════════════════════════════════════════════╗
║  Phase 4: GROUP BY Aggregation (aggregates.rs)                ║
║    ┌──────────────────────────────────────────┐              ║
║    │ Evaluate group keys: [dept]              │              ║
║    │   keys = ["eng", "sales", "eng", ...]    │              ║
║    └───────────────┬──────────────────────────┘              ║
║                    ▼                                          ║
║    ┌──────────────────────────────────────────┐              ║
║    │ Hash aggregation table:                  │              ║
║    │   HashMap<GroupKey, AggregateState>      │              ║
║    │                                           │              ║
║    │   For each row i:                        │              ║
║    │     key = GroupKey(dept[i])              │              ║
║    │     state = table[key]                   │              ║
║    │     state.sum += salary[i]               │              ║
║    │     state.count += 1                     │              ║
║    └───────────────┬──────────────────────────┘              ║
║                    ▼                                          ║
║    ┌──────────────────────────────────────────┐              ║
║    │ Materialize results:                     │              ║
║    │   dept      AVG(salary)                  │              ║
║    │   ----      -----------                  │              ║
║    │   eng       95000                        │              ║
║    │   sales     87500                        │              ║
║    │   → ColumnarBatch with 2 rows            │              ║
║    └──────────────────────────────────────────┘              ║
╚═════════════════════════╤═════════════════════════════════════╝
                          │ Aggregated batch (2 rows)
                          ▼
╔═══════════════════════════════════════════════════════════════╗
║  Phase 5: ORDER BY Sorting (ordering.rs)                      ║
║    ┌──────────────────────────────────────────┐              ║
║    │ Build sort keys for each row:            │              ║
║    │   keys = [                               │              ║
║    │     OrderKey([Text("eng")]),             │              ║
║    │     OrderKey([Text("sales")])            │              ║
║    │   ]                                      │              ║
║    └───────────────┬──────────────────────────┘              ║
║                    ▼                                          ║
║    ┌──────────────────────────────────────────┐              ║
║    │ keys.sort_unstable_by(compare_order_keys)│              ║
║    │   → indices: [0, 1] (eng < sales)        │              ║
║    └───────────────┬──────────────────────────┘              ║
║                    ▼                                          ║
║    ┌──────────────────────────────────────────┐              ║
║    │ batch = batch.gather(sorted_indices)     │              ║
║    └──────────────────────────────────────────┘              ║
╚═════════════════════════╤═════════════════════════════════════╝
                          │ Sorted batch (2 rows)
                          ▼
╔═══════════════════════════════════════════════════════════════╗
║  Phase 6: Projection (projection_helpers.rs)                  ║
║    Convert ColumnarBatch → Vec<Vec<String>>                   ║
║                                                               ║
║    Results:                                                   ║
║    [                                                          ║
║      ["eng", "95000"],                                        ║
║      ["sales", "87500"]                                       ║
║    ]                                                          ║
╚═══════════════════════════════════════════════════════════════╝

Level 2: DML Execution (INSERT/UPDATE/DELETE)

INSERT Path (Page Group Batching)

╔═══════════════════════════════════════════════════════════════╗
║ SQL: "INSERT INTO users (id, name) VALUES (1, 'Alice')"      ║
╚═════════════════════════╤═════════════════════════════════════╝
                          │
                          ▼
╔═══════════════════════════════════════════════════════════════╗
║              SQL EXECUTOR (executor/mod.rs)                   ║
║  • Parse INSERT statement                                     ║
║  • Extract values: [("1", "Alice")]                          ║
║  • Resolve table schema and ORDER BY columns                 ║
╚═════════════════════════╤═════════════════════════════════════╝
                          │
                          ▼
╔═══════════════════════════════════════════════════════════════╗
║                    WRITER (writer/executor.rs)                ║
║                                                               ║
║  ┌──────────────────────────────────────────┐                ║
║  │ Submit UpdateJob {                       │                ║
║  │   table: "users",                        │                ║
║  │   columns: [                             │                ║
║  │     ColumnUpdate {                       │                ║
║  │       column: "id",                      │                ║
║  │       operations: [                      │                ║
║  │         BufferRow { row: ["1"] }         │                ║
║  │       ]                                  │                ║
║  │     },                                   │                ║
║  │     ColumnUpdate {                       │                ║
║  │       column: "name",                    │                ║
║  │       operations: [                      │                ║
║  │         BufferRow { row: ["Alice"] }     │                ║
║  │       ]                                  │                ║
║  │     }                                    │                ║
║  │   ]                                      │                ║
║  │ }                                        │                ║
║  └───────────────┬──────────────────────────┘                ║
║                  │                                            ║
║                  ▼                                            ║
║  ┌──────────────────────────────────────────┐                ║
║  │ Background Worker Thread                 │                ║
║  │ (polls channel every ~1ms)               │                ║
║  └───────────────┬──────────────────────────┘                ║
║                  │                                            ║
║                  ▼                                            ║
║  ┌──────────────────────────────────────────┐                ║
║  │ buffer_row(table, row)                   │                ║
║  │   buffered_rows["users"] += 1            │                ║
║  │   if buffered_rows.len() >= 1000:        │                ║
║  │     flush_page_group()                   │                ║
║  └───────────────┬──────────────────────────┘                ║
╚══════════════════╪═══════════════════════════════════════════╝
                   │ (after 1000 rows buffered)
                   ▼
╔═══════════════════════════════════════════════════════════════╗
║              flush_page_group() - Phase 1: Stage              ║
║  ┌──────────────────────────────────────────┐                ║
║  │ For each column:                         │                ║
║  │   1. Sort buffered rows by ORDER BY keys│                ║
║  │   2. Create new Page with 1000 entries  │                ║
║  │   3. Serialize with bincode              │                ║
║  │      → page_bytes (e.g., 45,678 bytes)   │                ║
║  │   4. Allocate disk space:                │                ║
║  │      allocator.allocate(45678)           │                ║
║  │      → { path: "storage/data.00000",     │                ║
║  │          offset: 262144,                 │                ║
║  │          alloc_len: 49152 } (48 KiB)     │                ║
║  └──────────────────────────────────────────┘                ║
╚══════════════════╪═══════════════════════════════════════════╝
                   │ Vec<StagedColumn>
                   ▼
╔═══════════════════════════════════════════════════════════════╗
║              flush_page_group() - Phase 2: Commit             ║
║  ┌──────────────────────────────────────────┐                ║
║  │ metadata_client.commit(table, updates)   │                ║
║  │   → PageDirectory::register_batch()      │                ║
║  │   → Acquires write lock                  │                ║
║  │   → Generates page IDs: ["p1001", "p1002"]│               ║
║  │   → Updates column metadata atomically   │                ║
║  │   → Returns Vec<PageDescriptor>          │                ║
║  └──────────────────────────────────────────┘                ║
╚══════════════════╪═══════════════════════════════════════════╝
                   │ Committed
                   ▼
╔═══════════════════════════════════════════════════════════════╗
║              flush_page_group() - Phase 3: Persist            ║
║  ┌──────────────────────────────────────────┐                ║
║  │ For each StagedColumn:                   │                ║
║  │   1. Zero-pad buffer to alloc_len        │                ║
║  │      buffer = vec![0; 49152]             │                ║
║  │      buffer[0..45678] = serialized_bytes │                ║
║  │   2. Open file (O_DIRECT on Linux)       │                ║
║  │   3. write_all_at(buffer, offset)        │                ║
║  │      → Writes to storage/data.00000      │                ║
║  └──────────────────────────────────────────┘                ║
╚══════════════════╪═══════════════════════════════════════════╝
                   │ Persisted
                   ▼
╔═══════════════════════════════════════════════════════════════╗
║              flush_page_group() - Phase 4: Cache              ║
║  ┌──────────────────────────────────────────┐                ║
║  │ For each page:                           │                ║
║  │   page_handler.write_back_uncompressed(  │                ║
║  │     descriptor.id, page                  │                ║
║  │   )                                      │                ║
║  │   → Inserts into UPC                     │                ║
║  │   → Next read hits cache immediately     │                ║
║  └──────────────────────────────────────────┘                ║
╚═══════════════════════════════════════════════════════════════╝

Level 3: Storage Architecture Deep-Dive

Cache Hierarchy

╔══════════════════════════════════════════════════════════════════╗
║                   READ REQUEST: get_page("p42")                  ║
╚════════════════════════════╤═════════════════════════════════════╝
                             │
                             ▼
          ╔══════════════════════════════════════╗
          ║ LAYER 1: Uncompressed Page Cache    ║
          ║         (UPC / hot pages)            ║
          ╚══════════════════════════════════════╝
                             │
              ┌──────────────┼──────────────┐
              │              │              │
              ▼              ▼              ▼
        Read lock     HashMap lookup   Found?
              │              │              │
              └──────────────┴──YES─────────┴──▶ return Arc<Page>
                             │
                            NO (miss)
                             │
                             ▼
          ╔══════════════════════════════════════╗
          ║ LAYER 2: Compressed Page Cache       ║
          ║         (CPC / cold blobs)            ║
          ╚══════════════════════════════════════╝
                             │
              ┌──────────────┼──────────────┐
              │              │              │
              ▼              ▼              ▼
        Read lock     HashMap lookup   Found?
              │              │              │
              │              │             YES
              │              │              │
              └──────────────┴──────────────┤
                             │              │
                            NO              ▼
                             │     ┌────────────────────┐
                             │     │ Decompress blob    │
                             │     │ (lz4 + bincode)    │
                             │     └────────┬───────────┘
                             │              │
                             │              ▼
                             │     ┌────────────────────┐
                             │     │ Insert into UPC    │
                             │     └────────┬───────────┘
                             │              │
                             │              └──▶ return Arc<Page>
                             │
                             ▼
          ╔══════════════════════════════════════╗
          ║ LAYER 3: Disk I/O                    ║
          ║         (persistent storage)         ║
          ╚══════════════════════════════════════╝
                             │
              ┌──────────────┼──────────────┐
              │              │              │
              ▼              ▼              ▼
     Lookup descriptor  Open file    Read bytes
              │              │              │
              │              │              ▼
              │              │     ┌────────────────────┐
              │              │     │ PageIO::read()     │
              │              │     │ • seek(offset)     │
              │              │     │ • read(alloc_len)  │
              │              │     └────────┬───────────┘
              │              │              │
              │              │              ▼
              │              │     ┌────────────────────┐
              │              │     │ Insert into CPC    │
              │              │     └────────┬───────────┘
              │              │              │
              │              │              ▼
              │              │     ┌────────────────────┐
              │              │     │ Decompress → UPC   │
              │              │     └────────┬───────────┘
              │              │              │
              │              └──────────────┴──▶ return Arc<Page>
              │
              ▼
        (page not found error)

Cache Eviction Flow

╔══════════════════════════════════════════════════════════════════╗
║              UPC reaches capacity (10 entries)                   ║
╚════════════════════════════╤═════════════════════════════════════╝
                             │
                             ▼
          ┌──────────────────────────────────┐
          │ Identify LRU entry               │
          │   lru_queue.iter().next()        │
          │   → (used_time: 100, id: "p1")   │
          └────────────┬─────────────────────┘
                       │
                       ▼
          ┌──────────────────────────────────┐
          │ Remove from UPC                  │
          │   store.remove("p1")             │
          │   lru_queue.remove((100, "p1"))  │
          │   → Arc<Page>                    │
          └────────────┬─────────────────────┘
                       │
                       ▼
          ┌──────────────────────────────────┐
          │ Lifecycle callback               │
          │   UncompressedToCompressed       │
          │   .on_evict("p1", Arc<Page>)     │
          └────────────┬─────────────────────┘
                       │
                       ▼
          ┌──────────────────────────────────┐
          │ Compress page                    │
          │   bincode::serialize(&page)      │
          │   lz4_flex::compress(&bytes)     │
          │   → compressed_blob: Vec<u8>     │
          └────────────┬─────────────────────┘
                       │
                       ▼
          ┌──────────────────────────────────┐
          │ Insert into CPC                  │
          │   cpc.add("p1", compressed_blob) │
          └──────────────────────────────────┘
                       │
                       ▼
          ┌──────────────────────────────────┐
          │ CPC eviction (if full)           │
          │   → Lifecycle callback           │
          │   → CompressedToDisk             │
          │   → Write to storage file        │
          └──────────────────────────────────┘

Level 4: Metadata Management

PageDirectory Structure

╔══════════════════════════════════════════════════════════════════╗
║                         PageDirectory                            ║
║                         (façade layer)                           ║
╚════════════════════════════╤═════════════════════════════════════╝
                             │
                             ▼
╔══════════════════════════════════════════════════════════════════╗
║                       TableMetaStore                             ║
║                                                                  ║
║  ┌──────────────────────────────────────────────────────────┐   ║
║  │ tables: HashMap<TableName, Arc<RwLock<TableCatalog>>>    │   ║
║  │   "users" → TableCatalog {                               │   ║
║  │     columns: HashMap<ColumnName, ColumnCatalog>          │   ║
║  │       "id" → ColumnCatalog {                             │   ║
║  │         ranges: Vec<TableMetaStoreEntry>                 │   ║
║  │           [                                              │   ║
║  │             Entry { start: 0, end: 1000,                 │   ║
║  │                     versions: [                          │   ║
║  │                       MVCCEntry { page_id: "p1",         │   ║
║  │                                   commit: 100 },         │   ║
║  │                       MVCCEntry { page_id: "p2",         │   ║
║  │                                   commit: 200 }          │   ║
║  │                     ]                                    │   ║
║  │             },                                           │   ║
║  │             Entry { start: 1000, end: 2000,              │   ║
║  │                     versions: [ ... ]                    │   ║
║  │             }                                            │   ║
║  │           ]                                              │   ║
║  │       }                                                  │   ║
║  │   }                                                      │   ║
║  │                                                          │   ║
║  │ page_metadata: HashMap<PageId, Arc<PageMetadata>>        │   ║
║  │   "p1" → PageMetadata {                                  │   ║
║  │     id: "p1",                                            │   ║
║  │     disk_path: "storage/data.00000",                     │   ║
║  │     offset: 0,                                           │   ║
║  │     alloc_len: 49152,                                    │   ║
║  │     actual_len: 45678,                                   │   ║
║  │     entry_count: 1000                                    │   ║
║  │   }                                                      │   ║
║  └──────────────────────────────────────────────────────────┘   ║
╚══════════════════════════════════════════════════════════════════╝

Metadata Lookup: latest_for_column(“users”, “id”)

Step 1: Acquire read lock on TableCatalog
          │
          ▼
Step 2: Get column catalog
          column_catalog = tables["users"].columns["id"]
          │
          ▼
Step 3: Get last range entry
          last_entry = column_catalog.ranges.last()
          → Entry { start: 1000, end: 2000, versions: [...] }
          │
          ▼
Step 4: Get last MVCC version
          last_version = last_entry.versions.last()
          → MVCCEntry { page_id: "p2", commit: 200 }
          │
          ▼
Step 5: Lookup page metadata
          metadata = page_metadata["p2"]
          │
          ▼
Step 6: Build descriptor (lightweight copy)
          PageDescriptor {
            id: "p2",
            disk_path: "storage/data.00000",
            offset: 49152,
            alloc_len: 49152,
            actual_len: 45678,
            entry_count: 1000
          }
          │
          ▼
Step 7: Release read lock, return descriptor

Metadata Update: register_batch()

Input: Vec<PendingPage> for atomic batch insert

Step 1: Acquire WRITE lock on TableCatalog
          (blocks all readers and other writers)
          │
          ▼
Step 2: For each PendingPage:
          a) Generate unique page ID
             page_id = format!("p{}", atomic_counter.fetch_add(1))
          │
          b) Insert into page_metadata map
             page_metadata[page_id] = Arc::new(PageMetadata { ... })
          │
          c) Update column's range list
             If replace_last:
               ranges.last_mut().versions.push(MVCCEntry { page_id, commit: now })
             Else:
               ranges.push(Entry { start, end, versions: [MVCCEntry { ... }] })
          │
          ▼
Step 3: Release write lock
          │
          ▼
Step 4: Return Vec<PageDescriptor>
          (all columns committed atomically)

Level 5: Complete Request Trace

Trace: SELECT with Filtering

╔═══════════════════════════════════════════════════════════════╗
║ SQL: SELECT name, salary FROM employees WHERE age > 30       ║
╚═════════════════════════╤═════════════════════════════════════╝
                          │
                          ▼
[1] Parse SQL → AST
      sqlparser::Parser::parse_sql(sql)
          │
          ▼
[2] Plan query
      executor::execute_select()
      • Required columns: [name, salary, age]
      • WHERE filter: age > 30
          │
          ▼
[3] Locate data
      PageDirectory::locate_range("employees", 0, MAX)
      → Returns: [PageDescriptor{"p100"}, PageDescriptor{"p101"}]
          │
          ▼
[4] Fetch pages
      PageHandler::get_pages([p100, p101])
      ├─ Check UPC (miss both)
      ├─ Check CPC (hit p100, miss p101)
      │   └─ Decompress p100 → UPC
      └─ Fetch p101 from disk → CPC → decompress → UPC
      → Returns: [Arc<Page>, Arc<Page>]
          │
          ▼
[5] Convert to columnar batch
      For each page.entries:
        Parse Entry.data as string
        Try parse as Int64
        If fail, try Float64
        If fail, store as Text
      → ColumnarBatch {
          columns: {
            0 → ColumnarPage { data: Text(["Alice", "Bob", ...]) },
            1 → ColumnarPage { data: Int64([80000, 90000, ...]) },
            2 → ColumnarPage { data: Int64([28, 35, ...]) }
          },
          num_rows: 2000
        }
          │
          ▼
[6] Apply WHERE filter
      evaluate_selection_on_batch(&where_expr, &batch)
      • Evaluate: age > 30
      • For each row i:
          if age_column[i] > 30:
            selection_bitmap.set(i)
      → Bitmap: [0,1,0,1,1,0,...]
          │
          ▼
[7] Gather filtered rows
      selected_indices = selection_bitmap.iter_ones().collect()
      → [1, 3, 4, ...]  (1200 indices)
      batch = batch.gather(&selected_indices)
      → ColumnarBatch with 1200 rows
          │
          ▼
[8] Project columns
      Select only [name, salary] columns
      Convert ColumnarBatch → Vec<Vec<String>>
      → [
          ["Alice", "80000"],
          ["Bob", "90000"],
          ...
        ]
          │
          ▼
[9] Return results

Trace: INSERT with Batching

╔═══════════════════════════════════════════════════════════════╗
║ SQL: INSERT INTO users (id, name) VALUES (1, 'Alice')        ║
╚═════════════════════════╤═════════════════════════════════════╝
                          │
                          ▼
[1] Parse INSERT → Extract values
      executor::execute_insert()
      • Table: "users"
      • Columns: ["id", "name"]
      • Values: [("1", "Alice")]
          │
          ▼
[2] Create UpdateJob
      UpdateJob {
        table: "users",
        columns: [
          ColumnUpdate {
            column: "id",
            operations: [BufferRow { row: ["1"] }]
          },
          ColumnUpdate {
            column: "name",
            operations: [BufferRow { row: ["Alice"] }]
          }
        ]
      }
          │
          ▼
[3] Submit to Writer
      writer.submit(job)
      • Non-blocking, queues job in channel
          │
          ▼
[4] Worker thread receives job
      worker_context.handle_job(job)
          │
          ▼
[5] Buffer row
      buffered_rows["users"].push(["1", "Alice"])
      • buffered_rows.len() = 1 (< 1000 threshold)
      • Return (don't flush yet)
      ... (999 more inserts) ...
          │
          ▼
[6] Threshold reached (1000th insert)
      flush_page_group("users", buffered_rows)
          │
          ├─▶ [6a] Sort rows by ORDER BY columns
          │
          ├─▶ [6b] For each column:
          │     stage_column()
          │     ├─ Create Page with 1000 entries
          │     ├─ Serialize with bincode
          │     └─ Allocate disk space
          │     → StagedColumn { serialized, allocation, ... }
          │
          ├─▶ [6c] Commit metadata (atomic)
          │     metadata_client.commit(table, staged)
          │     ├─ Generate page IDs: ["p1001", "p1002"]
          │     ├─ Update column chains
          │     └─ Return descriptors
          │
          ├─▶ [6d] Persist to disk
          │     For each (staged, descriptor):
          │       ├─ Zero-pad buffer
          │       ├─ write_all_at(buffer, offset)
          │       └─ fsync
          │
          └─▶ [6e] Cache writeback
                For each page:
                  page_handler.write_back_uncompressed(id, page)
                  → Insert into UPC
          │
          ▼
[7] Return success

Performance Characteristics

Cache Hit Rates

Scenario: Random reads with 10-entry UPC
  Working set: 100 unique pages
  Hit rate: ~10% (UPC) + ~10% (CPC) = ~20% total
  → 80% requests hit disk

Scenario: Sequential scan (same pages)
  Working set: 5 pages
  Hit rate: ~100% after initial load
  → Amortized disk cost across many queries

Scenario: Write-heavy workload
  Insertions trigger page group flushes (1000 rows)
  Flushed pages immediately in UPC (100% hit rate)
  Reads of recently written data: ~100% UPC hit rate

Columnar Batch Processing

Operation              Row-by-row    Columnar Batch
──────────────────────────────────────────────────────
WHERE age > 30         1 eval/row    1 eval/word (64 bits)
  (1000 rows)          1000 ops      ~16 ops + bitmap ops

SUM(salary)            1 parse/row   1 parse/row (but vectorized)
  (1000 rows)          1000 parses   Potential SIMD: 4× speedup

Sort by 2 columns      2 key builds  Batch key build
  (1000 rows)          per row       → 1 allocation for all keys

Memory layout          Cache-hostile Cache-friendly
                       (scattered)   (sequential access)

Write Amplification

Scenario: Single row insert (without batching)
  1 row → 1 Page write → ~4 KiB minimum (4K alignment)
  Write amplification: ~4000× (1 byte → 4096 bytes)

Scenario: Page group batching (1000 rows)
  1000 rows → 1 Page write → ~48 KiB (45 KB actual + 3 KB padding)
  Write amplification: ~1.06× (45 KB → 48 KB)
  → 99% reduction in write amplification