| [ef1c1c7] | 1 | = Relational Design =
|
|---|
| 2 |
|
|---|
| 3 | == Descriptive representation of the relational schema ==
|
|---|
| 4 |
|
|---|
| 5 | Notation: '''bold''' = primary key, ''italic'' = foreign key.
|
|---|
| 6 |
|
|---|
| 7 | * '''Users'''(__'''id'''__, username, email, full_name, password_hash, available_balance, invested_balance, created_at, updated_at)
|
|---|
| 8 | * Candidate keys: `{id}`, `{username}`, `{email}`. `UNIQUE(username)`, `UNIQUE(email)`.
|
|---|
| 9 | * '''Crypto'''(__'''id'''__, symbol, name, created_at)
|
|---|
| 10 | * Candidate keys: `{id}`, `{symbol}`. `UNIQUE(symbol)`.
|
|---|
| 11 | * '''Markets'''(__'''id'''__, ''crypto_id'', quote_currency, is_active, created_at)
|
|---|
| 12 | * Candidate keys: `{id}`, `{crypto_id, quote_currency}`. `UNIQUE(crypto_id, quote_currency)`.
|
|---|
| 13 | * '''Holdings'''(__'''id'''__, ''user_id'', ''crypto_id'', quantity, reserved_quantity, avg_price, created_at, updated_at)
|
|---|
| 14 | * Transformation of the M:N relationship `Holds`. Candidate keys: `{id}` and
|
|---|
| 15 | `{user_id, crypto_id}` — the latter is the relationship's own key and is
|
|---|
| 16 | enforced with `UNIQUE(user_id, crypto_id)`. `id` was chosen as PK for
|
|---|
| 17 | consistency with the other relations.
|
|---|
| 18 | * `avg_price` is `NOT NULL DEFAULT 0 CHECK (avg_price >= 0)`.
|
|---|
| 19 | * `reserved_quantity` is `NOT NULL DEFAULT 0 CHECK (reserved_quantity >= 0 AND reserved_quantity <= quantity)` — the amount already committed to the
|
|---|
| 20 | user's own open sell orders. `quantity - reserved_quantity` (the amount
|
|---|
| 21 | actually free to sell) is not a stored column; it is computed wherever
|
|---|
| 22 | needed, in `v_portfolio` as `available_quantity` and in the sell path of
|
|---|
| 23 | [wiki:UseCase0005]. See the `Holds` section of [wiki:ERModel]
|
|---|
| 24 | for why this mirrors `available_balance`/`invested_balance` on `Users`.
|
|---|
| 25 | * '''Orders'''(__'''id'''__, ''user_id'', ''market_id'', side, type, status, quantity, price, placed_at, executed_at)
|
|---|
| 26 | * `side ∈ {buy, sell}`, `type ∈ {market, limit}`, `status ∈ {open, executed, cancelled}`.
|
|---|
| 27 | * '''Transactions'''(__'''id'''__, ''user_id'', type, amount, currency, ''related_order'', created_at, description)
|
|---|
| 28 | * `type ∈ {deposit, buy, sell, fee}`.
|
|---|
| 29 | * '''!MarketTrades'''(__'''id'''__, ''market_id'', executed_at, price, quantity, side, source)
|
|---|
| 30 | * '''!MarketCandles'''(__'''id'''__, ''market_id'', timeframe, open, high, low, close, volume, candle_time)
|
|---|
| 31 | * `UNIQUE(market_id, timeframe, candle_time)`.
|
|---|
| 32 | * '''Watchlists'''(__'''id'''__, ''user_id'', name, created_at)
|
|---|
| 33 | * '''!WatchlistItems'''(__'''id'''__, ''watchlist_id'', ''crypto_id'', added_at)
|
|---|
| 34 | * Transformation of the M:N relationship `Contains`. Candidate keys: `{id}`
|
|---|
| 35 | and `{watchlist_id, crypto_id}`, the latter enforced with
|
|---|
| 36 | `UNIQUE(watchlist_id, crypto_id)`.
|
|---|
| 37 |
|
|---|
| 38 | === Transformation method used ===
|
|---|
| 39 |
|
|---|
| 40 | '''Partial transformation.''' Applied as follows:
|
|---|
| 41 |
|
|---|
| 42 | * Each of the 8 entity sets in [wiki:ERModel] becomes one table, keeping
|
|---|
| 43 | its UUID (or serial) primary key.
|
|---|
| 44 | * Each '''1:N relationship without attributes''' is transformed by adding the
|
|---|
| 45 | parent's primary key as a foreign-key column on the child table — the "N"
|
|---|
| 46 | side. This is where every foreign key in the schema comes from, and it is why
|
|---|
| 47 | no foreign keys appear in the ER diagram itself:
|
|---|
| 48 | `QuotedOn` → `markets.crypto_id`, `PlacedOn` → `orders.market_id`,
|
|---|
| 49 | `Places` → `orders.user_id`, `Records` → `transactions.user_id`,
|
|---|
| 50 | `Settles` → `transactions.related_order`, `Fills` → `market_trades.market_id`,
|
|---|
| 51 | `Aggregates` → `market_candles.market_id`, `Owns` → `watchlists.user_id`.
|
|---|
| 52 | * Each '''M:N relationship''' becomes its own table holding the two foreign keys
|
|---|
| 53 | plus the relationship's own attributes: `Holds` → `holdings`,
|
|---|
| 54 | `Contains` → `watchlist_items`. The pair of foreign keys is the relationship's
|
|---|
| 55 | key and is enforced as a `UNIQUE` constraint in both tables.
|
|---|
| 56 | * '''Total participation''' in the ER model becomes `NOT NULL` on the
|
|---|
| 57 | corresponding foreign key; partial participation stays nullable. `Settles` is
|
|---|
| 58 | partial on both sides, which is exactly why `transactions.related_order` is
|
|---|
| 59 | the one nullable foreign key in the schema — a deposit has no originating
|
|---|
| 60 | order.
|
|---|
| 61 |
|
|---|
| 62 | === Normalisation ===
|
|---|
| 63 |
|
|---|
| 64 | > '''Validated in P5.''' Normalization derives this
|
|---|
| 65 | > exact schema independently — starting only from a single de-normalized relation of every
|
|---|
| 66 | > model attribute and its functional dependencies, with no reference to the ER-to-relational
|
|---|
| 67 | > transformation below — and shows it decomposes to '''BCNF''', one normal form stronger than
|
|---|
| 68 | > the 3NF claimed here. The two designs agree relation for relation and key for key, so
|
|---|
| 69 | > nothing here changed as a result; see that page's
|
|---|
| 70 | > discussion section for what the one real
|
|---|
| 71 | > difference is (`avg_price`, a stored derived value, not a normalisation issue) and why this
|
|---|
| 72 | > design is still the one used from P5 onward.
|
|---|
| 73 |
|
|---|
| 74 | All relations are in '''3NF''':
|
|---|
| 75 |
|
|---|
| 76 | * Every attribute is atomic (no repeating groups, no composite fields).
|
|---|
| 77 | * No partial dependency exists because every primary key is a single UUID column.
|
|---|
| 78 | * No transitive dependency exists: every non-key attribute depends directly on the row identifier. For example, `holdings.quantity` depends on `holdings.id`, not on `user_id` via some intermediate.
|
|---|
| 79 | * `avg_price` in `Holdings` is a '''derived value''' cached for performance (it is
|
|---|
| 80 | the weighted-average entry price across all `buy` transactions for that
|
|---|
| 81 | `(user, crypto)` pair) — it is drawn as a derived attribute in the ER diagram.
|
|---|
| 82 | We accept the denormalisation: it is recomputed by the database inside the same
|
|---|
| 83 | transaction as each buy, in the same statement that changes the quantity
|
|---|
| 84 | (`INSERT … ON CONFLICT (user_id, crypto_id) DO UPDATE`), so the stored average
|
|---|
| 85 | and the stored quantity can never disagree.
|
|---|
| 86 | * `avg_price` is declared `NOT NULL DEFAULT 0`. This matters: it is used in the
|
|---|
| 87 | P/L arithmetic of `v_portfolio`, and in SQL any arithmetic involving `NULL`
|
|---|
| 88 | yields `NULL`, so a nullable average would have silently blanked the
|
|---|
| 89 | unrealised-P/L column for an existing position instead of failing loudly.
|
|---|
| 90 | * `holdings.reserved_quantity`, unlike `avg_price`, is '''not''' derived — it is
|
|---|
| 91 | written directly by the application (`trade.go`) as orders are placed and
|
|---|
| 92 | settled, the same way `quantity` itself is. `quantity - reserved_quantity`
|
|---|
| 93 | ("available") is the derived value here, and it is never stored, only
|
|---|
| 94 | computed where it is needed.
|
|---|
| 95 |
|
|---|
| 96 | === Reservation and the order lifecycle ===
|
|---|
| 97 |
|
|---|
| 98 | `holdings.reserved_quantity` exists so that placing a sell order can be
|
|---|
| 99 | checked against what a user actually has ''free'' to sell
|
|---|
| 100 | (`quantity - reserved_quantity`), not against the raw `quantity`, which also
|
|---|
| 101 | counts crypto already promised to another order that has not settled yet.
|
|---|
| 102 | `CHECK (reserved_quantity >= 0 AND reserved_quantity <= quantity)` makes an
|
|---|
| 103 | inconsistent reservation impossible at the database level, regardless of what
|
|---|
| 104 | application code does. The exact statement sequence — lock the row, check the
|
|---|
| 105 | available amount, reserve, then settle — is in
|
|---|
| 106 | [wiki:UseCase0005]; the same
|
|---|
| 107 | `SELECT … FOR UPDATE` locking that already protected `users.available_balance`
|
|---|
| 108 | on the buy path is what makes two concurrent sell orders against the same
|
|---|
| 109 | holding serialize correctly instead of racing.
|
|---|
| 110 |
|
|---|
| 111 | == DDL script ==
|
|---|
| 112 |
|
|---|
| 113 | The script that creates the entire schema is `../server/db/schema_creation.sql` (shown in full below). It is idempotent: it drops and recreates the `project` schema every run, so it works on an empty database and on a database that already has the schema.
|
|---|
| 114 |
|
|---|
| 115 | The script creates:
|
|---|
| 116 | * 10 tables with check constraints, primary keys, foreign keys and unique constraints.
|
|---|
| 117 | * 5 performance indexes.
|
|---|
| 118 | * 2 views: `v_latest_prices` (latest trade price per market) and `v_portfolio` (per-user holdings valuation with unrealised P/L, plus `reserved_quantity` and the derived `available_quantity`).
|
|---|
| 119 |
|
|---|
| 120 | === schema_creation.sql ===
|
|---|
| 121 |
|
|---|
| 122 | The two report functions at the end of the file (`report_top_traders` and `report_market_performance`) belong to Phase 6 ([wiki:AdvancedReports]) and are left out here.
|
|---|
| 123 |
|
|---|
| 124 | {{{
|
|---|
| 125 | -- schema_creation.sql
|
|---|
| 126 | -- EduBerza - crypto exchange simulation database
|
|---|
| 127 | -- Course: Databases 2025/2026 Winter, FINKI UKIM
|
|---|
| 128 | --
|
|---|
| 129 | -- This script is idempotent. It drops the `project` schema and all contained
|
|---|
| 130 | -- objects, then recreates them from scratch. Safe to run on an empty database
|
|---|
| 131 | -- or on a database where the schema already exists.
|
|---|
| 132 |
|
|---|
| 133 | DROP SCHEMA IF EXISTS project CASCADE;
|
|---|
| 134 | CREATE SCHEMA project;
|
|---|
| 135 |
|
|---|
| 136 | CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
|---|
| 137 |
|
|---|
| 138 | SET search_path TO project, public;
|
|---|
| 139 |
|
|---|
| 140 | -- ============================================================================
|
|---|
| 141 | -- USERS
|
|---|
| 142 | -- Platform users. Each user has virtual (prop) balances used for simulation.
|
|---|
| 143 | -- ============================================================================
|
|---|
| 144 | CREATE TABLE project.users (
|
|---|
| 145 | id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|---|
| 146 | username varchar(50) NOT NULL UNIQUE,
|
|---|
| 147 | email varchar(255) NOT NULL UNIQUE,
|
|---|
| 148 | full_name varchar(200),
|
|---|
| 149 | password_hash varchar(255) NOT NULL,
|
|---|
| 150 | available_balance numeric(18,4) NOT NULL DEFAULT 0 CHECK (available_balance >= 0),
|
|---|
| 151 | invested_balance numeric(18,4) NOT NULL DEFAULT 0 CHECK (invested_balance >= 0),
|
|---|
| 152 | created_at timestamptz NOT NULL DEFAULT now(),
|
|---|
| 153 | updated_at timestamptz
|
|---|
| 154 | );
|
|---|
| 155 |
|
|---|
| 156 | -- ============================================================================
|
|---|
| 157 | -- CRYPTO
|
|---|
| 158 | -- Catalog of crypto assets available on the platform.
|
|---|
| 159 | -- ============================================================================
|
|---|
| 160 | CREATE TABLE project.crypto (
|
|---|
| 161 | id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|---|
| 162 | symbol varchar(20) NOT NULL UNIQUE,
|
|---|
| 163 | name varchar(255) NOT NULL,
|
|---|
| 164 | created_at timestamptz NOT NULL DEFAULT now()
|
|---|
| 165 | );
|
|---|
| 166 |
|
|---|
| 167 | -- ============================================================================
|
|---|
| 168 | -- MARKETS
|
|---|
| 169 | -- A market is a (crypto, quote_currency) pair, e.g. BTC/USD.
|
|---|
| 170 | -- ============================================================================
|
|---|
| 171 | CREATE TABLE project.markets (
|
|---|
| 172 | id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|---|
| 173 | crypto_id uuid NOT NULL REFERENCES project.crypto(id),
|
|---|
| 174 | quote_currency char(3) NOT NULL DEFAULT 'USD',
|
|---|
| 175 | is_active boolean NOT NULL DEFAULT true,
|
|---|
| 176 | created_at timestamptz NOT NULL DEFAULT now(),
|
|---|
| 177 | CONSTRAINT uq_markets UNIQUE (crypto_id, quote_currency)
|
|---|
| 178 | );
|
|---|
| 179 |
|
|---|
| 180 | -- ============================================================================
|
|---|
| 181 | -- HOLDINGS
|
|---|
| 182 | -- Per-user crypto position with running weighted average entry price.
|
|---|
| 183 | -- ============================================================================
|
|---|
| 184 | CREATE TABLE project.holdings (
|
|---|
| 185 | id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|---|
| 186 | user_id uuid NOT NULL REFERENCES project.users(id) ON DELETE CASCADE,
|
|---|
| 187 | crypto_id uuid NOT NULL REFERENCES project.crypto(id),
|
|---|
| 188 | quantity numeric(20,4) NOT NULL CHECK (quantity >= 0),
|
|---|
| 189 | -- Committed to the user's own open sell orders, not yet removed from the
|
|---|
| 190 | -- position. quantity - reserved_quantity is what is actually free to
|
|---|
| 191 | -- sell — the crypto-side equivalent of users.available_balance.
|
|---|
| 192 | reserved_quantity numeric(20,4) NOT NULL DEFAULT 0
|
|---|
| 193 | CHECK (reserved_quantity >= 0 AND reserved_quantity <= quantity),
|
|---|
| 194 | -- Weighted-average entry price. NOT NULL so that the P/L arithmetic in
|
|---|
| 195 | -- v_portfolio can never silently produce NULL for an existing position.
|
|---|
| 196 | avg_price numeric(18,6) NOT NULL DEFAULT 0 CHECK (avg_price >= 0),
|
|---|
| 197 | created_at timestamptz NOT NULL DEFAULT now(),
|
|---|
| 198 | updated_at timestamptz,
|
|---|
| 199 | CONSTRAINT uq_holdings_user_crypto UNIQUE (user_id, crypto_id)
|
|---|
| 200 | );
|
|---|
| 201 |
|
|---|
| 202 | -- ============================================================================
|
|---|
| 203 | -- ORDERS
|
|---|
| 204 | -- Orders placed by users on a market.
|
|---|
| 205 | -- ============================================================================
|
|---|
| 206 | CREATE TABLE project.orders (
|
|---|
| 207 | id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|---|
| 208 | user_id uuid NOT NULL REFERENCES project.users(id) ON DELETE CASCADE,
|
|---|
| 209 | market_id uuid NOT NULL REFERENCES project.markets(id),
|
|---|
| 210 | side varchar(4) NOT NULL CHECK (side IN ('buy', 'sell')),
|
|---|
| 211 | type varchar(20) NOT NULL CHECK (type IN ('market', 'limit')),
|
|---|
| 212 | status varchar(20) NOT NULL CHECK (status IN ('open', 'executed', 'cancelled')),
|
|---|
| 213 | quantity numeric(20,4) NOT NULL CHECK (quantity > 0),
|
|---|
| 214 | price numeric(18,6),
|
|---|
| 215 | placed_at timestamptz NOT NULL DEFAULT now(),
|
|---|
| 216 | executed_at timestamptz
|
|---|
| 217 | );
|
|---|
| 218 |
|
|---|
| 219 | CREATE INDEX idx_orders_user ON project.orders(user_id);
|
|---|
| 220 | CREATE INDEX idx_orders_market ON project.orders(market_id);
|
|---|
| 221 | CREATE INDEX idx_orders_status ON project.orders(status);
|
|---|
| 222 |
|
|---|
| 223 | -- ============================================================================
|
|---|
| 224 | -- TRANSACTIONS
|
|---|
| 225 | -- Financial ledger: deposits, buys, sells, fees.
|
|---|
| 226 | -- ============================================================================
|
|---|
| 227 | CREATE TABLE project.transactions (
|
|---|
| 228 | id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|---|
| 229 | user_id uuid NOT NULL REFERENCES project.users(id) ON DELETE CASCADE,
|
|---|
| 230 | type varchar(50) NOT NULL CHECK (type IN ('deposit', 'buy', 'sell', 'fee')),
|
|---|
| 231 | amount numeric(18,4) NOT NULL,
|
|---|
| 232 | currency char(3) NOT NULL DEFAULT 'USD',
|
|---|
| 233 | related_order uuid REFERENCES project.orders(id),
|
|---|
| 234 | created_at timestamptz NOT NULL DEFAULT now(),
|
|---|
| 235 | description text
|
|---|
| 236 | );
|
|---|
| 237 |
|
|---|
| 238 | CREATE INDEX idx_transactions_user ON project.transactions(user_id, created_at DESC);
|
|---|
| 239 |
|
|---|
| 240 | -- ============================================================================
|
|---|
| 241 | -- MARKET TRADES
|
|---|
| 242 | -- Raw executed trades on a market. Source of truth for current price.
|
|---|
| 243 | -- ============================================================================
|
|---|
| 244 | CREATE TABLE project.market_trades (
|
|---|
| 245 | id bigserial PRIMARY KEY,
|
|---|
| 246 | market_id uuid NOT NULL REFERENCES project.markets(id),
|
|---|
| 247 | executed_at timestamptz NOT NULL,
|
|---|
| 248 | price numeric(18,6) NOT NULL CHECK (price > 0),
|
|---|
| 249 | quantity numeric(20,6) NOT NULL CHECK (quantity > 0),
|
|---|
| 250 | side varchar(4) CHECK (side IN ('buy', 'sell')),
|
|---|
| 251 | source varchar(50) NOT NULL DEFAULT 'simulation'
|
|---|
| 252 | );
|
|---|
| 253 |
|
|---|
| 254 | CREATE INDEX idx_market_trades_market_time ON project.market_trades(market_id, executed_at DESC);
|
|---|
| 255 |
|
|---|
| 256 | -- ============================================================================
|
|---|
| 257 | -- MARKET CANDLES
|
|---|
| 258 | -- OHLCV aggregates over standard timeframes.
|
|---|
| 259 | -- ============================================================================
|
|---|
| 260 | CREATE TABLE project.market_candles (
|
|---|
| 261 | id bigserial PRIMARY KEY,
|
|---|
| 262 | market_id uuid NOT NULL REFERENCES project.markets(id),
|
|---|
| 263 | timeframe varchar(5) NOT NULL CHECK (timeframe IN ('1m', '5m', '1h', '1d')),
|
|---|
| 264 | open numeric(18,6) NOT NULL,
|
|---|
| 265 | high numeric(18,6) NOT NULL,
|
|---|
| 266 | low numeric(18,6) NOT NULL,
|
|---|
| 267 | close numeric(18,6) NOT NULL,
|
|---|
| 268 | volume numeric(20,6) NOT NULL,
|
|---|
| 269 | candle_time timestamptz NOT NULL,
|
|---|
| 270 | CONSTRAINT uq_candle UNIQUE (market_id, timeframe, candle_time)
|
|---|
| 271 | );
|
|---|
| 272 |
|
|---|
| 273 | CREATE INDEX idx_market_candles_market_tf_time ON project.market_candles(market_id, timeframe, candle_time DESC);
|
|---|
| 274 |
|
|---|
| 275 | -- ============================================================================
|
|---|
| 276 | -- WATCHLISTS
|
|---|
| 277 | -- ============================================================================
|
|---|
| 278 | CREATE TABLE project.watchlists (
|
|---|
| 279 | id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|---|
| 280 | user_id uuid NOT NULL REFERENCES project.users(id) ON DELETE CASCADE,
|
|---|
| 281 | name varchar(100) NOT NULL,
|
|---|
| 282 | created_at timestamptz NOT NULL DEFAULT now()
|
|---|
| 283 | );
|
|---|
| 284 |
|
|---|
| 285 | CREATE TABLE project.watchlist_items (
|
|---|
| 286 | id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|---|
| 287 | watchlist_id uuid NOT NULL REFERENCES project.watchlists(id) ON DELETE CASCADE,
|
|---|
| 288 | crypto_id uuid NOT NULL REFERENCES project.crypto(id),
|
|---|
| 289 | added_at timestamptz NOT NULL DEFAULT now(),
|
|---|
| 290 | CONSTRAINT uq_watchlist_crypto UNIQUE (watchlist_id, crypto_id)
|
|---|
| 291 | );
|
|---|
| 292 |
|
|---|
| 293 | -- ============================================================================
|
|---|
| 294 | -- VIEWS
|
|---|
| 295 | -- ============================================================================
|
|---|
| 296 |
|
|---|
| 297 | -- Latest trade price per market (current price).
|
|---|
| 298 | CREATE OR REPLACE VIEW project.v_latest_prices AS
|
|---|
| 299 | SELECT DISTINCT ON (t.market_id)
|
|---|
| 300 | t.market_id,
|
|---|
| 301 | c.symbol,
|
|---|
| 302 | m.quote_currency,
|
|---|
| 303 | t.price,
|
|---|
| 304 | t.executed_at
|
|---|
| 305 | FROM project.market_trades t
|
|---|
| 306 | JOIN project.markets m ON m.id = t.market_id
|
|---|
| 307 | JOIN project.crypto c ON c.id = m.crypto_id
|
|---|
| 308 | ORDER BY t.market_id, t.executed_at DESC;
|
|---|
| 309 |
|
|---|
| 310 | -- Portfolio valuation per user (holdings x latest price).
|
|---|
| 311 | CREATE OR REPLACE VIEW project.v_portfolio AS
|
|---|
| 312 | SELECT h.user_id,
|
|---|
| 313 | c.symbol,
|
|---|
| 314 | h.quantity,
|
|---|
| 315 | h.reserved_quantity,
|
|---|
| 316 | (h.quantity - h.reserved_quantity) AS available_quantity,
|
|---|
| 317 | h.avg_price,
|
|---|
| 318 | lp.price AS current_price,
|
|---|
| 319 | (h.quantity * lp.price) AS market_value,
|
|---|
| 320 | (h.quantity * (lp.price - h.avg_price)) AS unrealized_pnl
|
|---|
| 321 | FROM project.holdings h
|
|---|
| 322 | JOIN project.crypto c ON c.id = h.crypto_id
|
|---|
| 323 | LEFT JOIN project.markets m ON m.crypto_id = c.id AND m.quote_currency = 'USD'
|
|---|
| 324 | LEFT JOIN project.v_latest_prices lp ON lp.market_id = m.id;
|
|---|
| 325 | }}}
|
|---|
| 326 |
|
|---|
| 327 | == DML script (sample data) ==
|
|---|
| 328 |
|
|---|
| 329 | The script that loads realistic sample data is `../server/db/data_load.sql` (shown in full below). It is idempotent: it truncates all tables with `CASCADE` then re-inserts. Loaded:
|
|---|
| 330 | * 5 crypto assets (BTC, ETH, ADA, SOL, DOGE) and 5 USD-quoted markets.
|
|---|
| 331 | * 3 sample users (`alice`, `bob`, `charlie`) with password `test123` (sha256 hex).
|
|---|
| 332 | * 18 recent market trades across all markets so `v_latest_prices` is populated.
|
|---|
| 333 | * 10 one-hour candles (BTC and ETH).
|
|---|
| 334 | * One fully-executed market-buy order for Alice, the matching holding, and two ledger entries (deposit + buy), with Alice's balances updated accordingly.
|
|---|
| 335 | * Two watchlists with five watchlist items.
|
|---|
| 336 |
|
|---|
| 337 | === data_load.sql ===
|
|---|
| 338 |
|
|---|
| 339 | {{{
|
|---|
| 340 | -- data_load.sql
|
|---|
| 341 | -- EduBerza - sample data
|
|---|
| 342 | -- Course: Databases 2025/2026 Winter, FINKI UKIM
|
|---|
| 343 | --
|
|---|
| 344 | -- Idempotent. Truncates all tables in the `project` schema and reloads
|
|---|
| 345 | -- deterministic sample data. Run schema_creation.sql first if tables do
|
|---|
| 346 | -- not yet exist.
|
|---|
| 347 | --
|
|---|
| 348 | -- All sample users have the password: test123
|
|---|
| 349 |
|
|---|
| 350 | SET search_path TO project, public;
|
|---|
| 351 |
|
|---|
| 352 | TRUNCATE TABLE
|
|---|
| 353 | project.watchlist_items,
|
|---|
| 354 | project.watchlists,
|
|---|
| 355 | project.market_candles,
|
|---|
| 356 | project.market_trades,
|
|---|
| 357 | project.transactions,
|
|---|
| 358 | project.orders,
|
|---|
| 359 | project.holdings,
|
|---|
| 360 | project.markets,
|
|---|
| 361 | project.crypto,
|
|---|
| 362 | project.users
|
|---|
| 363 | RESTART IDENTITY CASCADE;
|
|---|
| 364 |
|
|---|
| 365 | -- ============================================================================
|
|---|
| 366 | -- CRYPTO
|
|---|
| 367 | -- ============================================================================
|
|---|
| 368 | INSERT INTO project.crypto (id, symbol, name) VALUES
|
|---|
| 369 | ('11111111-1111-1111-1111-111111111111', 'BTC', 'Bitcoin'),
|
|---|
| 370 | ('22222222-2222-2222-2222-222222222222', 'ETH', 'Ethereum'),
|
|---|
| 371 | ('33333333-3333-3333-3333-333333333333', 'ADA', 'Cardano'),
|
|---|
| 372 | ('44444444-4444-4444-4444-444444444444', 'SOL', 'Solana'),
|
|---|
| 373 | ('55555555-5555-5555-5555-555555555555', 'DOGE', 'Dogecoin');
|
|---|
| 374 |
|
|---|
| 375 | -- ============================================================================
|
|---|
| 376 | -- MARKETS (all quoted in USD)
|
|---|
| 377 | -- ============================================================================
|
|---|
| 378 | INSERT INTO project.markets (id, crypto_id, quote_currency, is_active) VALUES
|
|---|
| 379 | ('a1111111-1111-1111-1111-111111111111', '11111111-1111-1111-1111-111111111111', 'USD', true),
|
|---|
| 380 | ('a2222222-2222-2222-2222-222222222222', '22222222-2222-2222-2222-222222222222', 'USD', true),
|
|---|
| 381 | ('a3333333-3333-3333-3333-333333333333', '33333333-3333-3333-3333-333333333333', 'USD', true),
|
|---|
| 382 | ('a4444444-4444-4444-4444-444444444444', '44444444-4444-4444-4444-444444444444', 'USD', true),
|
|---|
| 383 | ('a5555555-5555-5555-5555-555555555555', '55555555-5555-5555-5555-555555555555', 'USD', true);
|
|---|
| 384 |
|
|---|
| 385 | -- ============================================================================
|
|---|
| 386 | -- USERS
|
|---|
| 387 | -- Password for all: test123 (stored as sha256 hex hash)
|
|---|
| 388 | -- ============================================================================
|
|---|
| 389 | INSERT INTO project.users (id, username, email, full_name, password_hash, available_balance, invested_balance) VALUES
|
|---|
| 390 | ('b1111111-1111-1111-1111-111111111111', 'alice', 'alice@example.com', 'Alice Johnson',
|
|---|
| 391 | encode(digest('test123', 'sha256'), 'hex'), 10000.0000, 0),
|
|---|
| 392 | ('b2222222-2222-2222-2222-222222222222', 'bob', 'bob@example.com', 'Bob Smith',
|
|---|
| 393 | encode(digest('test123', 'sha256'), 'hex'), 5000.0000, 0),
|
|---|
| 394 | ('b3333333-3333-3333-3333-333333333333', 'charlie', 'charlie@example.com', 'Charlie Davis',
|
|---|
| 395 | encode(digest('test123', 'sha256'), 'hex'), 2500.0000, 0);
|
|---|
| 396 |
|
|---|
| 397 | -- ============================================================================
|
|---|
| 398 | -- MARKET TRADES
|
|---|
| 399 | -- Recent simulated trades per market, used as price source.
|
|---|
| 400 | -- ============================================================================
|
|---|
| 401 | INSERT INTO project.market_trades (market_id, executed_at, price, quantity, side, source) VALUES
|
|---|
| 402 | -- BTC/USD around $67,000
|
|---|
| 403 | ('a1111111-1111-1111-1111-111111111111', now() - interval '10 min', 66850.250000, 0.120000, 'buy', 'simulation'),
|
|---|
| 404 | ('a1111111-1111-1111-1111-111111111111', now() - interval '8 min', 66910.500000, 0.075000, 'sell', 'simulation'),
|
|---|
| 405 | ('a1111111-1111-1111-1111-111111111111', now() - interval '5 min', 67020.750000, 0.200000, 'buy', 'simulation'),
|
|---|
| 406 | ('a1111111-1111-1111-1111-111111111111', now() - interval '2 min', 67105.100000, 0.050000, 'buy', 'simulation'),
|
|---|
| 407 | ('a1111111-1111-1111-1111-111111111111', now() - interval '30 second', 67140.000000, 0.030000, 'sell', 'simulation'),
|
|---|
| 408 | -- ETH/USD around $3,500
|
|---|
| 409 | ('a2222222-2222-2222-2222-222222222222', now() - interval '10 min', 3490.500000, 1.500000, 'buy', 'simulation'),
|
|---|
| 410 | ('a2222222-2222-2222-2222-222222222222', now() - interval '6 min', 3502.750000, 0.800000, 'sell', 'simulation'),
|
|---|
| 411 | ('a2222222-2222-2222-2222-222222222222', now() - interval '2 min', 3515.250000, 2.100000, 'buy', 'simulation'),
|
|---|
| 412 | ('a2222222-2222-2222-2222-222222222222', now() - interval '30 second', 3520.000000, 0.650000, 'buy', 'simulation'),
|
|---|
| 413 | -- ADA/USD around $0.45
|
|---|
| 414 | ('a3333333-3333-3333-3333-333333333333', now() - interval '10 min', 0.446500, 500.000000, 'buy', 'simulation'),
|
|---|
| 415 | ('a3333333-3333-3333-3333-333333333333', now() - interval '3 min', 0.452000, 1200.000000, 'buy', 'simulation'),
|
|---|
| 416 | ('a3333333-3333-3333-3333-333333333333', now() - interval '30 second', 0.453750, 800.000000, 'sell', 'simulation'),
|
|---|
| 417 | -- SOL/USD around $165
|
|---|
| 418 | ('a4444444-4444-4444-4444-444444444444', now() - interval '10 min', 164.250000, 10.000000, 'buy', 'simulation'),
|
|---|
| 419 | ('a4444444-4444-4444-4444-444444444444', now() - interval '4 min', 165.500000, 5.500000, 'sell', 'simulation'),
|
|---|
| 420 | ('a4444444-4444-4444-4444-444444444444', now() - interval '30 second', 166.100000, 8.000000, 'buy', 'simulation'),
|
|---|
| 421 | -- DOGE/USD around $0.12
|
|---|
| 422 | ('a5555555-5555-5555-5555-555555555555', now() - interval '10 min', 0.118500, 10000.000000, 'buy', 'simulation'),
|
|---|
| 423 | ('a5555555-5555-5555-5555-555555555555', now() - interval '3 min', 0.121250, 7500.000000, 'sell', 'simulation'),
|
|---|
| 424 | ('a5555555-5555-5555-5555-555555555555', now() - interval '30 second', 0.122000, 12000.000000, 'buy', 'simulation');
|
|---|
| 425 |
|
|---|
| 426 | -- ============================================================================
|
|---|
| 427 | -- MARKET CANDLES (1h aggregates, last 5 hours per market)
|
|---|
| 428 | -- ============================================================================
|
|---|
| 429 | INSERT INTO project.market_candles (market_id, timeframe, open, high, low, close, volume, candle_time) VALUES
|
|---|
| 430 | ('a1111111-1111-1111-1111-111111111111', '1h', 66200, 66500, 66050, 66400, 12.50, date_trunc('hour', now() - interval '5 hour')),
|
|---|
| 431 | ('a1111111-1111-1111-1111-111111111111', '1h', 66400, 66800, 66380, 66700, 15.30, date_trunc('hour', now() - interval '4 hour')),
|
|---|
| 432 | ('a1111111-1111-1111-1111-111111111111', '1h', 66700, 66950, 66650, 66900, 11.80, date_trunc('hour', now() - interval '3 hour')),
|
|---|
| 433 | ('a1111111-1111-1111-1111-111111111111', '1h', 66900, 67100, 66800, 67050, 14.20, date_trunc('hour', now() - interval '2 hour')),
|
|---|
| 434 | ('a1111111-1111-1111-1111-111111111111', '1h', 67050, 67200, 66900, 67140, 10.75, date_trunc('hour', now() - interval '1 hour')),
|
|---|
| 435 | ('a2222222-2222-2222-2222-222222222222', '1h', 3460, 3490, 3450, 3485, 120.0, date_trunc('hour', now() - interval '5 hour')),
|
|---|
| 436 | ('a2222222-2222-2222-2222-222222222222', '1h', 3485, 3510, 3480, 3500, 135.0, date_trunc('hour', now() - interval '4 hour')),
|
|---|
| 437 | ('a2222222-2222-2222-2222-222222222222', '1h', 3500, 3520, 3495, 3515, 110.0, date_trunc('hour', now() - interval '3 hour')),
|
|---|
| 438 | ('a2222222-2222-2222-2222-222222222222', '1h', 3515, 3525, 3500, 3520, 125.5, date_trunc('hour', now() - interval '2 hour')),
|
|---|
| 439 | ('a2222222-2222-2222-2222-222222222222', '1h', 3520, 3530, 3510, 3520, 140.0, date_trunc('hour', now() - interval '1 hour'));
|
|---|
| 440 |
|
|---|
| 441 | -- ============================================================================
|
|---|
| 442 | -- EXAMPLE ORDERS, HOLDINGS AND TRANSACTIONS for alice
|
|---|
| 443 | -- Shows a fully-filled market buy and its resulting holding & ledger entry.
|
|---|
| 444 | -- ============================================================================
|
|---|
| 445 | INSERT INTO project.orders (id, user_id, market_id, side, type, status, quantity, price, placed_at, executed_at) VALUES
|
|---|
| 446 | ('c1111111-1111-1111-1111-111111111111',
|
|---|
| 447 | 'b1111111-1111-1111-1111-111111111111',
|
|---|
| 448 | 'a2222222-2222-2222-2222-222222222222',
|
|---|
| 449 | 'buy', 'market', 'executed', 0.5000, 3500.000000,
|
|---|
| 450 | now() - interval '1 hour', now() - interval '1 hour');
|
|---|
| 451 |
|
|---|
| 452 | INSERT INTO project.holdings (user_id, crypto_id, quantity, avg_price, updated_at) VALUES
|
|---|
| 453 | ('b1111111-1111-1111-1111-111111111111',
|
|---|
| 454 | '22222222-2222-2222-2222-222222222222',
|
|---|
| 455 | 0.5000, 3500.000000, now() - interval '1 hour');
|
|---|
| 456 |
|
|---|
| 457 | INSERT INTO project.transactions (user_id, type, amount, currency, related_order, description) VALUES
|
|---|
| 458 | ('b1111111-1111-1111-1111-111111111111', 'deposit', 10000.0000, 'USD', NULL,
|
|---|
| 459 | 'Initial virtual deposit'),
|
|---|
| 460 | ('b1111111-1111-1111-1111-111111111111', 'buy', -1750.0000, 'USD',
|
|---|
| 461 | 'c1111111-1111-1111-1111-111111111111',
|
|---|
| 462 | 'Market buy 0.5 ETH @ 3500.00');
|
|---|
| 463 |
|
|---|
| 464 | -- After the buy, alice's invested_balance reflects the used funds.
|
|---|
| 465 | UPDATE project.users
|
|---|
| 466 | SET available_balance = 10000.0000 - 1750.0000,
|
|---|
| 467 | invested_balance = 1750.0000,
|
|---|
| 468 | updated_at = now()
|
|---|
| 469 | WHERE id = 'b1111111-1111-1111-1111-111111111111';
|
|---|
| 470 |
|
|---|
| 471 | -- ============================================================================
|
|---|
| 472 | -- WATCHLISTS
|
|---|
| 473 | -- ============================================================================
|
|---|
| 474 | INSERT INTO project.watchlists (id, user_id, name) VALUES
|
|---|
| 475 | ('d1111111-1111-1111-1111-111111111111', 'b1111111-1111-1111-1111-111111111111', 'Favorites'),
|
|---|
| 476 | ('d2222222-2222-2222-2222-222222222222', 'b2222222-2222-2222-2222-222222222222', 'Bobs Picks');
|
|---|
| 477 |
|
|---|
| 478 | INSERT INTO project.watchlist_items (watchlist_id, crypto_id) VALUES
|
|---|
| 479 | ('d1111111-1111-1111-1111-111111111111', '11111111-1111-1111-1111-111111111111'),
|
|---|
| 480 | ('d1111111-1111-1111-1111-111111111111', '22222222-2222-2222-2222-222222222222'),
|
|---|
| 481 | ('d1111111-1111-1111-1111-111111111111', '44444444-4444-4444-4444-444444444444'),
|
|---|
| 482 | ('d2222222-2222-2222-2222-222222222222', '11111111-1111-1111-1111-111111111111'),
|
|---|
| 483 | ('d2222222-2222-2222-2222-222222222222', '55555555-5555-5555-5555-555555555555');
|
|---|
| 484 | }}}
|
|---|
| 485 |
|
|---|
| 486 | == Relational diagram ==
|
|---|
| 487 |
|
|---|
| 488 | [[Image(relational_schema.jpg)]]
|
|---|
| 489 |
|
|---|
| 490 | Generated in '''Pgadmin''' from the '''live''' `project` schema, in crow's-foot
|
|---|
| 491 | notation — not drawn by hand, so it is evidence that the deployed database
|
|---|
| 492 | actually matches the design described above. Each box is a table with its
|
|---|
| 493 | columns and declared types; key icons mark primary keys and the arrowed lines
|
|---|
| 494 | are the 12 declared foreign keys.
|
|---|
| 495 |
|
|---|
| 496 | === How to regenerate it ===
|
|---|
| 497 |
|
|---|
| 498 | '''With pgAdmin 4''', if DBeaver is unavailable — it reads the live schema the same
|
|---|
| 499 | way, so the result is equivalent in substance:
|
|---|
| 500 |
|
|---|
| 501 | 1. Connect to the project database.
|
|---|
| 502 | 2. Right-click the database → '''ERD For Database''' (or open a blank ERD and drag
|
|---|
| 503 | the `project` tables in).
|
|---|
| 504 | 3. Arrange the tables to mirror `ERModel_v03.png`.
|
|---|
| 505 | 4. '''Download image''' → PNG, then convert:
|
|---|
| 506 | `convert relational_schema.png relational_schema.jpg`
|
|---|