Skip to content

SymbolStorageManager

Core CRUD operations and database row mapping for the symbols table.

Purpose

SymbolStorageManager is the foundational data access layer for the SymbolRegistry. It owns the symbols table schema, the SymbolRow type definition, and the rowToConfig() mapping function used by all other managers to convert database rows into the SymbolConfig API type.

It handles seeding core symbols from static constants on first boot (idempotent via existence checks or INSERT OR IGNORE), querying symbols with optional filters (enabled status, category), and updating individual symbol properties with validation.

The manager provides both granular operations (enableSymbol, disableSymbol, updateSymbol) and bulk read operations (getSymbols, getSymbolsByCategory). All enable/disable operations are idempotent, returning success if the symbol is already in the desired state.

High-Level Design

Role Within the System

SymbolStorageManager is the canonical owner of the symbols table — it defines the schema, the SymbolRow type, and the rowToConfig() mapping function. However, ownership is by convention, not enforcement: other managers (SymbolAdminManager, RankingsManager) write directly to the same table for their domain-specific operations. SymbolStorageManager covers core CRUD and seeding; it does not handle ranking crawls, admin promote/demote, or downstream propagation.

Static Constants (26 symbols)

        │  [first boot, table empty]

seedCoreSymbolsInternal()


  symbols table ◄── updateSymbol / enable / disable  (this manager)
                ◄── promote / demote / batch updates  (SymbolAdminManager)
                ◄── insertDiscoveredSymbol / rankings  (RankingsManager)
                ──► getEnabledSymbolsWithMetadata()    (PushNotificationManager)


                   PriceCollector + PriceAlert DOs

Seeding: Two Paths

There are two seeding methods because they serve different callers:

  • seedCoreSymbolsInternal() — Called during DO initialize() when the table is empty. Uses INSERT OR IGNORE in a loop over all 26 SymbolIdentifiers from static constants, setting is_core_symbol = 1. Fast, no per-row existence check.
  • seedCoreSymbols() — Public RPC method for manual re-seeding. Checks each symbol with a SELECT before inserting, skipping existing rows. Returns a count of newly seeded symbols so the caller knows what changed.

Both paths are idempotent. The split exists because the internal path optimizes for the cold-start case (no reads needed), while the public path provides feedback. Note: seedCoreSymbolsInternal() explicitly sets is_core_symbol = 1, while seedCoreSymbols() relies on the DB default (0) — so symbols seeded via the public RPC won't be marked as core.

Shared Table, Distributed Writes

The symbols table is the single source of truth for symbol metadata, but multiple managers write directly to it for their domain-specific operations. There are no transactions spanning managers — each SQL statement is independent. This is safe because the data volume is small (26 core + a handful of discovered symbols) and concurrent writes to the same row are unlikely. See each manager's doc for which columns it owns.

rowToConfig() as the Mapping Boundary

The rowToConfig() instance method is the single point where SymbolRow (snake_case DB row) is converted to SymbolConfig (camelCase API type). The parent DO wraps it in a closure and passes it to other managers (e.g., SymbolAdminManager), keeping the type mapping centralized even though multiple managers read the table.

Enable/Disable as Soft State

There is no delete operation. Symbols are never removed from the table. Disabling (is_enabled = 0) is the closest equivalent and is fully reversible. The enabled_at timestamp records the last disable-to-enable transition within this manager — it is set when enabling a disabled symbol but not cleared on disable, giving a "last activated" timestamp. Note that other code paths (e.g., SymbolAdminManager's promoteSymbol) may set enabled_at unconditionally.

Validation Boundaries

SymbolStorageManager validates inputs only for its own update operations:

  • Category: Checked against the SymbolCategory enum
  • maxLeverageOverride: Range 1–100, or null to clear
  • iconUrl: Validated with new URL() constructor

Seeding and discovery inserts (via RankingsManager) skip validation because they use trusted static constants or crawler-verified data.

Edge Cases & Error Handling

  • enableSymbol / disableSymbol are idempotent -- no error if already in desired state.
  • updateSymbol with no valid fields to update returns current state without writing.
  • seedCoreSymbols skips existing symbols via existence check; seedCoreSymbolsInternal uses INSERT OR IGNORE.
  • enabled_at only set on the transition from disabled to enabled (not on repeated enable calls via updateSymbol).
  • iconUrl validated with new URL() constructor; invalid formats rejected.
  • maxLeverageOverride accepts null to clear the override.
  • getSymbolsByCategory always returns all 5 category keys, even if empty.

See Also