| 42 | | | Prefix | Origin (P1 entity / relationship) | Attributes | |
| 43 | | |---|---|---| |
| 44 | | | `U_` | Users | `U_ID, U_USERNAME, U_EMAIL, U_FULL_NAME, U_PASSWORD_HASH, U_AVAILABLE_BALANCE, U_INVESTED_BALANCE, U_CREATED_AT, U_UPDATED_AT` | |
| 45 | | | `C_` | Cryptos | `C_ID, C_SYMBOL, C_NAME, C_CREATED_AT` | |
| 46 | | | `M_` | Markets (+ `QuotedOn`) | `M_ID, M_CRYPTO_ID, M_QUOTE_CURRENCY, M_IS_ACTIVE, M_CREATED_AT` | |
| 47 | | | `H_` | `Holds` (+ surrogate key) | `H_ID, H_USER_ID, H_CRYPTO_ID, H_QUANTITY, H_RESERVED_QUANTITY, H_AVG_PRICE, H_CREATED_AT, H_UPDATED_AT` | |
| 48 | | | `O_` | Orders (+ `PlacedOn`, `Places`) | `O_ID, O_USER_ID, O_MARKET_ID, O_SIDE, O_TYPE, O_STATUS, O_QUANTITY, O_PRICE, O_PLACED_AT, O_EXECUTED_AT` | |
| 49 | | | `T_` | Transactions (+ `Records`, `Settles`) | `T_ID, T_USER_ID, T_TYPE, T_AMOUNT, T_CURRENCY, T_RELATED_ORDER, T_CREATED_AT, T_DESCRIPTION` | |
| 50 | | | `MT_` | MarketTrades (+ `Fills`) | `MT_ID, MT_MARKET_ID, MT_EXECUTED_AT, MT_PRICE, MT_QUANTITY, MT_SIDE, MT_SOURCE` | |
| 51 | | | `MC_` | MarketCandles (+ `Aggregates`) | `MC_ID, MC_MARKET_ID, MC_TIMEFRAME, MC_OPEN, MC_HIGH, MC_LOW, MC_CLOSE, MC_VOLUME, MC_CANDLE_TIME` | |
| 52 | | | `W_` | Watchlists (+ `Owns`) | `W_ID, W_USER_ID, W_NAME, W_CREATED_AT` | |
| 53 | | | `WI_` | `Contains` (+ surrogate key) | `WI_ID, WI_WATCHLIST_ID, WI_CRYPTO_ID, WI_ADDED_AT` | |
| | 35 | ||= Prefix =||= Origin (P1 entity / relationship) =||= Attributes =|| |
| | 36 | || `U_` || Users || `U_ID, U_USERNAME, U_EMAIL, U_FULL_NAME, U_PASSWORD_HASH, U_AVAILABLE_BALANCE, U_INVESTED_BALANCE, U_CREATED_AT, U_UPDATED_AT` || |
| | 37 | || `C_` || Cryptos || `C_ID, C_SYMBOL, C_NAME, C_CREATED_AT` || |
| | 38 | || `M_` || Markets (+ `QuotedOn`) || `M_ID, M_CRYPTO_ID, M_QUOTE_CURRENCY, M_IS_ACTIVE, M_CREATED_AT` || |
| | 39 | || `H_` || `Holds` (+ surrogate key) || `H_ID, H_USER_ID, H_CRYPTO_ID, H_QUANTITY, H_RESERVED_QUANTITY, H_AVG_PRICE, H_CREATED_AT, H_UPDATED_AT` || |
| | 40 | || `O_` || Orders (+ `PlacedOn`, `Places`) || `O_ID, O_USER_ID, O_MARKET_ID, O_SIDE, O_TYPE, O_STATUS, O_QUANTITY, O_PRICE, O_PLACED_AT, O_EXECUTED_AT` || |
| | 41 | || `T_` || Transactions (+ `Records`, `Settles`) || `T_ID, T_USER_ID, T_TYPE, T_AMOUNT, T_CURRENCY, T_RELATED_ORDER, T_CREATED_AT, T_DESCRIPTION` || |
| | 42 | || `MT_` || !MarketTrades (+ `Fills`) || `MT_ID, MT_MARKET_ID, MT_EXECUTED_AT, MT_PRICE, MT_QUANTITY, MT_SIDE, MT_SOURCE` || |
| | 43 | || `MC_` || !MarketCandles (+ `Aggregates`) || `MC_ID, MC_MARKET_ID, MC_TIMEFRAME, MC_OPEN, MC_HIGH, MC_LOW, MC_CLOSE, MC_VOLUME, MC_CANDLE_TIME` || |
| | 44 | || `W_` || Watchlists (+ `Owns`) || `W_ID, W_USER_ID, W_NAME, W_CREATED_AT` || |
| | 45 | || `WI_` || `Contains` (+ surrogate key) || `WI_ID, WI_WATCHLIST_ID, WI_CRYPTO_ID, WI_ADDED_AT` || |
| 96 | | | # | Functional dependency | Source | |
| 97 | | |---|---|---| |
| 98 | | | FD1 | `U_ID → U_USERNAME, U_EMAIL, U_FULL_NAME, U_PASSWORD_HASH, U_AVAILABLE_BALANCE, U_INVESTED_BALANCE, U_CREATED_AT, U_UPDATED_AT` | Users | |
| 99 | | | FD2 | `U_USERNAME → U_ID` | Users (`UNIQUE(username)`) | |
| 100 | | | FD3 | `U_EMAIL → U_ID` | Users (`UNIQUE(email)`) | |
| 101 | | | FD4 | `C_ID → C_SYMBOL, C_NAME, C_CREATED_AT` | Cryptos | |
| 102 | | | FD5 | `C_SYMBOL → C_ID` | Cryptos (`UNIQUE(symbol)`) | |
| 103 | | | FD6 | `M_ID → M_CRYPTO_ID, M_QUOTE_CURRENCY, M_IS_ACTIVE, M_CREATED_AT` | Markets | |
| 104 | | | FD7 | `M_CRYPTO_ID, M_QUOTE_CURRENCY → M_ID` | Markets (`UNIQUE(crypto_id, quote_currency)`) | |
| 105 | | | FD8 | `H_ID → H_USER_ID, H_CRYPTO_ID, H_QUANTITY, H_RESERVED_QUANTITY, H_AVG_PRICE, H_CREATED_AT, H_UPDATED_AT` | Holds | |
| 106 | | | FD9 | `H_USER_ID, H_CRYPTO_ID → H_ID` | Holds (`UNIQUE(user_id, crypto_id)`) | |
| 107 | | | FD10 | `O_ID → O_USER_ID, O_MARKET_ID, O_SIDE, O_TYPE, O_STATUS, O_QUANTITY, O_PRICE, O_PLACED_AT, O_EXECUTED_AT` | Orders | |
| 108 | | | FD11 | `T_ID → T_USER_ID, T_TYPE, T_AMOUNT, T_CURRENCY, T_RELATED_ORDER, T_CREATED_AT, T_DESCRIPTION` | Transactions | |
| 109 | | | FD12 | `MT_ID → MT_MARKET_ID, MT_EXECUTED_AT, MT_PRICE, MT_QUANTITY, MT_SIDE, MT_SOURCE` | MarketTrades | |
| 110 | | | FD13 | `MC_ID → MC_MARKET_ID, MC_TIMEFRAME, MC_OPEN, MC_HIGH, MC_LOW, MC_CLOSE, MC_VOLUME, MC_CANDLE_TIME` | MarketCandles | |
| 111 | | | FD14 | `MC_MARKET_ID, MC_TIMEFRAME, MC_CANDLE_TIME → MC_ID` | MarketCandles (`UNIQUE(market_id, timeframe, candle_time)`) | |
| 112 | | | FD15 | `W_ID → W_USER_ID, W_NAME, W_CREATED_AT` | Watchlists | |
| 113 | | | FD16 | `WI_ID → WI_WATCHLIST_ID, WI_CRYPTO_ID, WI_ADDED_AT` | Contains | |
| 114 | | | FD17 | `WI_WATCHLIST_ID, WI_CRYPTO_ID → WI_ID` | Contains (`UNIQUE(watchlist_id, crypto_id)`) | |
| 115 | | |
| 116 | | **Minimality, checked by example (Markets):** could FD7 drop an attribute from its left side? |
| | 88 | ||= # =||= Functional dependency =||= Source =|| |
| | 89 | || FD1 || `U_ID → U_USERNAME, U_EMAIL, U_FULL_NAME, U_PASSWORD_HASH, U_AVAILABLE_BALANCE, U_INVESTED_BALANCE, U_CREATED_AT, U_UPDATED_AT` || Users || |
| | 90 | || FD2 || `U_USERNAME → U_ID` || Users (`UNIQUE(username)`) || |
| | 91 | || FD3 || `U_EMAIL → U_ID` || Users (`UNIQUE(email)`) || |
| | 92 | || FD4 || `C_ID → C_SYMBOL, C_NAME, C_CREATED_AT` || Cryptos || |
| | 93 | || FD5 || `C_SYMBOL → C_ID` || Cryptos (`UNIQUE(symbol)`) || |
| | 94 | || FD6 || `M_ID → M_CRYPTO_ID, M_QUOTE_CURRENCY, M_IS_ACTIVE, M_CREATED_AT` || Markets || |
| | 95 | || FD7 || `M_CRYPTO_ID, M_QUOTE_CURRENCY → M_ID` || Markets (`UNIQUE(crypto_id, quote_currency)`) || |
| | 96 | || FD8 || `H_ID → H_USER_ID, H_CRYPTO_ID, H_QUANTITY, H_RESERVED_QUANTITY, H_AVG_PRICE, H_CREATED_AT, H_UPDATED_AT` || Holds || |
| | 97 | || FD9 || `H_USER_ID, H_CRYPTO_ID → H_ID` || Holds (`UNIQUE(user_id, crypto_id)`) || |
| | 98 | || FD10 || `O_ID → O_USER_ID, O_MARKET_ID, O_SIDE, O_TYPE, O_STATUS, O_QUANTITY, O_PRICE, O_PLACED_AT, O_EXECUTED_AT` || Orders || |
| | 99 | || FD11 || `T_ID → T_USER_ID, T_TYPE, T_AMOUNT, T_CURRENCY, T_RELATED_ORDER, T_CREATED_AT, T_DESCRIPTION` || Transactions || |
| | 100 | || FD12 || `MT_ID → MT_MARKET_ID, MT_EXECUTED_AT, MT_PRICE, MT_QUANTITY, MT_SIDE, MT_SOURCE` || !MarketTrades || |
| | 101 | || FD13 || `MC_ID → MC_MARKET_ID, MC_TIMEFRAME, MC_OPEN, MC_HIGH, MC_LOW, MC_CLOSE, MC_VOLUME, MC_CANDLE_TIME` || !MarketCandles || |
| | 102 | || FD14 || `MC_MARKET_ID, MC_TIMEFRAME, MC_CANDLE_TIME → MC_ID` || !MarketCandles (`UNIQUE(market_id, timeframe, candle_time)`) || |
| | 103 | || FD15 || `W_ID → W_USER_ID, W_NAME, W_CREATED_AT` || Watchlists || |
| | 104 | || FD16 || `WI_ID → WI_WATCHLIST_ID, WI_CRYPTO_ID, WI_ADDED_AT` || Contains || |
| | 105 | || FD17 || `WI_WATCHLIST_ID, WI_CRYPTO_ID → WI_ID` || Contains (`UNIQUE(watchlist_id, crypto_id)`) || |
| | 106 | |
| | 107 | '''Minimality, checked by example (Markets):''' could FD7 drop an attribute from its left side? |
| 138 | | | Foreign key | References | Therefore also determines | |
| 139 | | |---|---|---| |
| 140 | | | `M_CRYPTO_ID` | `C_ID` | `C_SYMBOL, C_NAME, C_CREATED_AT` | |
| 141 | | | `H_USER_ID` | `U_ID` | all of `U_*` | |
| 142 | | | `H_CRYPTO_ID` | `C_ID` | all of `C_*` | |
| 143 | | | `O_USER_ID` | `U_ID` | all of `U_*` | |
| 144 | | | `O_MARKET_ID` | `M_ID` | all of `M_*`, and transitively all of `C_*` | |
| 145 | | | `T_USER_ID` | `U_ID` | all of `U_*` | |
| 146 | | | `T_RELATED_ORDER` | `O_ID` | all of `O_*`, and transitively `U_*`, `M_*`, `C_*` (when not null) | |
| 147 | | | `MT_MARKET_ID` | `M_ID` | all of `M_*`, transitively `C_*` | |
| 148 | | | `MC_MARKET_ID` | `M_ID` | all of `M_*`, transitively `C_*` | |
| 149 | | | `W_USER_ID` | `U_ID` | all of `U_*` | |
| 150 | | | `WI_WATCHLIST_ID` | `W_ID` | all of `W_*`, transitively `U_*` | |
| 151 | | | `WI_CRYPTO_ID` | `C_ID` | all of `C_*` | |
| 152 | | |
| 153 | | None of these is added to the canonical cover — each is *derivable* from FD1–FD17 by |
| | 129 | ||= Foreign key =||= References =||= Therefore also determines =|| |
| | 130 | || `M_CRYPTO_ID` || `C_ID` || `C_SYMBOL, C_NAME, C_CREATED_AT` || |
| | 131 | || `H_USER_ID` || `U_ID` || all of `U_*` || |
| | 132 | || `H_CRYPTO_ID` || `C_ID` || all of `C_*` || |
| | 133 | || `O_USER_ID` || `U_ID` || all of `U_*` || |
| | 134 | || `O_MARKET_ID` || `M_ID` || all of `M_*`, and transitively all of `C_*` || |
| | 135 | || `T_USER_ID` || `U_ID` || all of `U_*` || |
| | 136 | || `T_RELATED_ORDER` || `O_ID` || all of `O_*`, and transitively `U_*`, `M_*`, `C_*` (when not null) || |
| | 137 | || `MT_MARKET_ID` || `M_ID` || all of `M_*`, transitively `C_*` || |
| | 138 | || `MC_MARKET_ID` || `M_ID` || all of `M_*`, transitively `C_*` || |
| | 139 | || `W_USER_ID` || `U_ID` || all of `U_*` || |
| | 140 | || `WI_WATCHLIST_ID` || `W_ID` || all of `W_*`, transitively `U_*` || |
| | 141 | || `WI_CRYPTO_ID` || `C_ID` || all of `C_*` || |
| | 142 | |
| | 143 | None of these is added to the canonical cover — each is ''derivable'' from FD1–FD17 by |
| 176 | | ``` |
| 177 | | |
| 178 | | **Closure check**, applying FD1–FD17 in turn to this set: |
| 179 | | |
| 180 | | | Step | Attributes added | Dependency used | |
| 181 | | |---|---|---| |
| 182 | | | start | `U_ID, C_ID, M_ID, H_ID, O_ID, T_ID, MT_ID, MC_ID, W_ID, WI_ID` | — | |
| 183 | | | 1 | `U_USERNAME, U_EMAIL, U_FULL_NAME, U_PASSWORD_HASH, U_AVAILABLE_BALANCE, U_INVESTED_BALANCE, U_CREATED_AT, U_UPDATED_AT` | FD1 (`U_ID → …`) | |
| 184 | | | 2 | `C_SYMBOL, C_NAME, C_CREATED_AT` | FD4 | |
| 185 | | | 3 | `M_CRYPTO_ID, M_QUOTE_CURRENCY, M_IS_ACTIVE, M_CREATED_AT` | FD6 | |
| 186 | | | 4 | `H_USER_ID, H_CRYPTO_ID, H_QUANTITY, H_RESERVED_QUANTITY, H_AVG_PRICE, H_CREATED_AT, H_UPDATED_AT` | FD8 | |
| 187 | | | 5 | `O_USER_ID, O_MARKET_ID, O_SIDE, O_TYPE, O_STATUS, O_QUANTITY, O_PRICE, O_PLACED_AT, O_EXECUTED_AT` | FD10 | |
| 188 | | | 6 | `T_USER_ID, T_TYPE, T_AMOUNT, T_CURRENCY, T_RELATED_ORDER, T_CREATED_AT, T_DESCRIPTION` | FD11 | |
| 189 | | | 7 | `MT_MARKET_ID, MT_EXECUTED_AT, MT_PRICE, MT_QUANTITY, MT_SIDE, MT_SOURCE` | FD12 | |
| 190 | | | 8 | `MC_MARKET_ID, MC_TIMEFRAME, MC_OPEN, MC_HIGH, MC_LOW, MC_CLOSE, MC_VOLUME, MC_CANDLE_TIME` | FD13 | |
| 191 | | | 9 | `W_USER_ID, W_NAME, W_CREATED_AT` | FD15 | |
| 192 | | | 10 | `WI_WATCHLIST_ID, WI_CRYPTO_ID, WI_ADDED_AT` | FD16 | |
| | 166 | }}} |
| | 167 | |
| | 168 | '''Closure check''', applying FD1–FD17 in turn to this set: |
| | 169 | |
| | 170 | ||= Step =||= Attributes added =||= Dependency used =|| |
| | 171 | || start || `U_ID, C_ID, M_ID, H_ID, O_ID, T_ID, MT_ID, MC_ID, W_ID, WI_ID` || — || |
| | 172 | || 1 || `U_USERNAME, U_EMAIL, U_FULL_NAME, U_PASSWORD_HASH, U_AVAILABLE_BALANCE, U_INVESTED_BALANCE, U_CREATED_AT, U_UPDATED_AT` || FD1 (`U_ID → …`) || |
| | 173 | || 2 || `C_SYMBOL, C_NAME, C_CREATED_AT` || FD4 || |
| | 174 | || 3 || `M_CRYPTO_ID, M_QUOTE_CURRENCY, M_IS_ACTIVE, M_CREATED_AT` || FD6 || |
| | 175 | || 4 || `H_USER_ID, H_CRYPTO_ID, H_QUANTITY, H_RESERVED_QUANTITY, H_AVG_PRICE, H_CREATED_AT, H_UPDATED_AT` || FD8 || |
| | 176 | || 5 || `O_USER_ID, O_MARKET_ID, O_SIDE, O_TYPE, O_STATUS, O_QUANTITY, O_PRICE, O_PLACED_AT, O_EXECUTED_AT` || FD10 || |
| | 177 | || 6 || `T_USER_ID, T_TYPE, T_AMOUNT, T_CURRENCY, T_RELATED_ORDER, T_CREATED_AT, T_DESCRIPTION` || FD11 || |
| | 178 | || 7 || `MT_MARKET_ID, MT_EXECUTED_AT, MT_PRICE, MT_QUANTITY, MT_SIDE, MT_SOURCE` || FD12 || |
| | 179 | || 8 || `MC_MARKET_ID, MC_TIMEFRAME, MC_OPEN, MC_HIGH, MC_LOW, MC_CLOSE, MC_VOLUME, MC_CANDLE_TIME` || FD13 || |
| | 180 | || 9 || `W_USER_ID, W_NAME, W_CREATED_AT` || FD15 || |
| | 181 | || 10 || `WI_WATCHLIST_ID, WI_CRYPTO_ID, WI_ADDED_AT` || FD16 || |
| 243 | | | New relation | Attributes | Key(s) | Source FDs | |
| 244 | | |---|---|---|---| |
| 245 | | | `R_USERS` | `U_ID, U_USERNAME, U_EMAIL, U_FULL_NAME, U_PASSWORD_HASH, U_AVAILABLE_BALANCE, U_INVESTED_BALANCE, U_CREATED_AT, U_UPDATED_AT` | `U_ID`, `U_USERNAME`, `U_EMAIL` | FD1, FD2, FD3 | |
| 246 | | | `R_CRYPTO` | `C_ID, C_SYMBOL, C_NAME, C_CREATED_AT` | `C_ID`, `C_SYMBOL` | FD4, FD5 | |
| 247 | | | `R_MARKETS` | `M_ID, M_CRYPTO_ID, M_QUOTE_CURRENCY, M_IS_ACTIVE, M_CREATED_AT` | `M_ID`, `{M_CRYPTO_ID, M_QUOTE_CURRENCY}` | FD6, FD7 | |
| 248 | | | `R_HOLDINGS` | `H_ID, H_USER_ID, H_CRYPTO_ID, H_QUANTITY, H_RESERVED_QUANTITY, H_AVG_PRICE, H_CREATED_AT, H_UPDATED_AT` | `H_ID`, `{H_USER_ID, H_CRYPTO_ID}` | FD8, FD9 | |
| 249 | | | `R_ORDERS` | `O_ID, O_USER_ID, O_MARKET_ID, O_SIDE, O_TYPE, O_STATUS, O_QUANTITY, O_PRICE, O_PLACED_AT, O_EXECUTED_AT` | `O_ID` | FD10 | |
| 250 | | | `R_TRANSACTIONS` | `T_ID, T_USER_ID, T_TYPE, T_AMOUNT, T_CURRENCY, T_RELATED_ORDER, T_CREATED_AT, T_DESCRIPTION` | `T_ID` | FD11 | |
| 251 | | | `R_MARKET_TRADES` | `MT_ID, MT_MARKET_ID, MT_EXECUTED_AT, MT_PRICE, MT_QUANTITY, MT_SIDE, MT_SOURCE` | `MT_ID` | FD12 | |
| 252 | | | `R_MARKET_CANDLES` | `MC_ID, MC_MARKET_ID, MC_TIMEFRAME, MC_OPEN, MC_HIGH, MC_LOW, MC_CLOSE, MC_VOLUME, MC_CANDLE_TIME` | `MC_ID`, `{MC_MARKET_ID, MC_TIMEFRAME, MC_CANDLE_TIME}` | FD13, FD14 | |
| 253 | | | `R_WATCHLISTS` | `W_ID, W_USER_ID, W_NAME, W_CREATED_AT` | `W_ID` | FD15 | |
| 254 | | | `R_WATCHLIST_ITEMS` | `WI_ID, WI_WATCHLIST_ID, WI_CRYPTO_ID, WI_ADDED_AT` | `WI_ID`, `{WI_WATCHLIST_ID, WI_CRYPTO_ID}` | FD16, FD17 | |
| 255 | | |
| 256 | | Every one of these ten relations now has **all** of its non-prime attributes depending on its |
| 257 | | **whole** key (in every case there is only one non-composite or one designated key doing the |
| | 231 | ||= New relation =||= Attributes =||= Key(s) =||= Source FDs =|| |
| | 232 | || `R_USERS` || `U_ID, U_USERNAME, U_EMAIL, U_FULL_NAME, U_PASSWORD_HASH, U_AVAILABLE_BALANCE, U_INVESTED_BALANCE, U_CREATED_AT, U_UPDATED_AT` || `U_ID`, `U_USERNAME`, `U_EMAIL` || FD1, FD2, FD3 || |
| | 233 | || `R_CRYPTO` || `C_ID, C_SYMBOL, C_NAME, C_CREATED_AT` || `C_ID`, `C_SYMBOL` || FD4, FD5 || |
| | 234 | || `R_MARKETS` || `M_ID, M_CRYPTO_ID, M_QUOTE_CURRENCY, M_IS_ACTIVE, M_CREATED_AT` || `M_ID`, `{M_CRYPTO_ID, M_QUOTE_CURRENCY}` || FD6, FD7 || |
| | 235 | || `R_HOLDINGS` || `H_ID, H_USER_ID, H_CRYPTO_ID, H_QUANTITY, H_RESERVED_QUANTITY, H_AVG_PRICE, H_CREATED_AT, H_UPDATED_AT` || `H_ID`, `{H_USER_ID, H_CRYPTO_ID}` || FD8, FD9 || |
| | 236 | || `R_ORDERS` || `O_ID, O_USER_ID, O_MARKET_ID, O_SIDE, O_TYPE, O_STATUS, O_QUANTITY, O_PRICE, O_PLACED_AT, O_EXECUTED_AT` || `O_ID` || FD10 || |
| | 237 | || `R_TRANSACTIONS` || `T_ID, T_USER_ID, T_TYPE, T_AMOUNT, T_CURRENCY, T_RELATED_ORDER, T_CREATED_AT, T_DESCRIPTION` || `T_ID` || FD11 || |
| | 238 | || `R_MARKET_TRADES` || `MT_ID, MT_MARKET_ID, MT_EXECUTED_AT, MT_PRICE, MT_QUANTITY, MT_SIDE, MT_SOURCE` || `MT_ID` || FD12 || |
| | 239 | || `R_MARKET_CANDLES` || `MC_ID, MC_MARKET_ID, MC_TIMEFRAME, MC_OPEN, MC_HIGH, MC_LOW, MC_CLOSE, MC_VOLUME, MC_CANDLE_TIME` || `MC_ID`, `{MC_MARKET_ID, MC_TIMEFRAME, MC_CANDLE_TIME}` || FD13, FD14 || |
| | 240 | || `R_WATCHLISTS` || `W_ID, W_USER_ID, W_NAME, W_CREATED_AT` || `W_ID` || FD15 || |
| | 241 | || `R_WATCHLIST_ITEMS` || `WI_ID, WI_WATCHLIST_ID, WI_CRYPTO_ID, WI_ADDED_AT` || `WI_ID`, `{WI_WATCHLIST_ID, WI_CRYPTO_ID}` || FD16, FD17 || |
| | 242 | |
| | 243 | Every one of these ten relations now has '''all''' of its non-prime attributes depending on its |
| | 244 | '''whole''' key (in every case there is only one non-composite or one designated key doing the |
| 319 | | | Relation | Functional dependencies in force | Determinant | Is it a candidate key? | |
| 320 | | |---|---|---|---| |
| 321 | | | `R_USERS` | FD1, FD2, FD3 | `U_ID`, `U_USERNAME`, `U_EMAIL` | Yes — all three are candidate keys | |
| 322 | | | `R_CRYPTO` | FD4, FD5 | `C_ID`, `C_SYMBOL` | Yes — both candidate keys | |
| 323 | | | `R_MARKETS` | FD6, FD7 | `M_ID`, `{M_CRYPTO_ID, M_QUOTE_CURRENCY}` | Yes — both candidate keys | |
| 324 | | | `R_HOLDINGS` | FD8, FD9 | `H_ID`, `{H_USER_ID, H_CRYPTO_ID}` | Yes — both candidate keys | |
| 325 | | | `R_ORDERS` | FD10 | `O_ID` | Yes — the only candidate key | |
| 326 | | | `R_TRANSACTIONS` | FD11 | `T_ID` | Yes — the only candidate key | |
| 327 | | | `R_MARKET_TRADES` | FD12 | `MT_ID` | Yes — the only candidate key | |
| 328 | | | `R_MARKET_CANDLES` | FD13, FD14 | `MC_ID`, `{MC_MARKET_ID, MC_TIMEFRAME, MC_CANDLE_TIME}` | Yes — both candidate keys | |
| 329 | | | `R_WATCHLISTS` | FD15 | `W_ID` | Yes — the only candidate key | |
| 330 | | | `R_WATCHLIST_ITEMS` | FD16, FD17 | `WI_ID`, `{WI_WATCHLIST_ID, WI_CRYPTO_ID}` | Yes — both candidate keys | |
| 331 | | |
| 332 | | Every determinant in every relation is one of that relation's own candidate keys. **All ten |
| 333 | | relations are already in BCNF** — the highest of the four normal forms this phase asks for, |
| | 304 | ||= Relation =||= Functional dependencies in force =||= Determinant =||= Is it a candidate key? =|| |
| | 305 | || `R_USERS` || FD1, FD2, FD3 || `U_ID`, `U_USERNAME`, `U_EMAIL` || Yes — all three are candidate keys || |
| | 306 | || `R_CRYPTO` || FD4, FD5 || `C_ID`, `C_SYMBOL` || Yes — both candidate keys || |
| | 307 | || `R_MARKETS` || FD6, FD7 || `M_ID`, `{M_CRYPTO_ID, M_QUOTE_CURRENCY}` || Yes — both candidate keys || |
| | 308 | || `R_HOLDINGS` || FD8, FD9 || `H_ID`, `{H_USER_ID, H_CRYPTO_ID}` || Yes — both candidate keys || |
| | 309 | || `R_ORDERS` || FD10 || `O_ID` || Yes — the only candidate key || |
| | 310 | || `R_TRANSACTIONS` || FD11 || `T_ID` || Yes — the only candidate key || |
| | 311 | || `R_MARKET_TRADES` || FD12 || `MT_ID` || Yes — the only candidate key || |
| | 312 | || `R_MARKET_CANDLES` || FD13, FD14 || `MC_ID`, `{MC_MARKET_ID, MC_TIMEFRAME, MC_CANDLE_TIME}` || Yes — both candidate keys || |
| | 313 | || `R_WATCHLISTS` || FD15 || `W_ID` || Yes — the only candidate key || |
| | 314 | || `R_WATCHLIST_ITEMS` || FD16, FD17 || `WI_ID`, `{WI_WATCHLIST_ID, WI_CRYPTO_ID}` || Yes — both candidate keys || |
| | 315 | |
| | 316 | Every determinant in every relation is one of that relation's own candidate keys. '''All ten relations are already in BCNF''' — the highest of the four normal forms this phase asks for, |
| 369 | | ### Discussion |
| 370 | | |
| 371 | | **This is the P2 design.** Strip the `U_`/`C_`/`M_`/… prefixes back to plain column names and |
| 372 | | `R_USERS, R_CRYPTO, R_MARKETS, R_HOLDINGS, R_ORDERS, R_TRANSACTIONS, R_MARKET_TRADES, |
| 373 | | R_MARKET_CANDLES, R_WATCHLISTS, R_WATCHLIST_ITEMS` are, attribute for attribute and key for |
| 374 | | key, `users, crypto, markets, holdings, orders, transactions, market_trades, market_candles, |
| 375 | | watchlists, watchlist_items` from |
| 376 | | [RelationalDesign](../P2-RelationalDesign/RelationalDesign.md). Every foreign key matches, |
| 377 | | every candidate key matches (including the less obvious composite ones — `{user_id, |
| 378 | | crypto_id}` on `holdings`, `{crypto_id, quote_currency}` on `markets`, `{market_id, timeframe, |
| 379 | | candle_time}` on `market_candles`), and the normal form matches (P2 already claimed 3NF; this |
| | 352 | === Discussion === |
| | 353 | |
| | 354 | '''This is the P2 design.''' Strip the `U_`/`C_`/`M_`/… prefixes back to plain column names and |
| | 355 | `R_USERS, R_CRYPTO, R_MARKETS, R_HOLDINGS, R_ORDERS, R_TRANSACTIONS, R_MARKET_TRADES, R_MARKET_CANDLES, R_WATCHLISTS, R_WATCHLIST_ITEMS` are, attribute for attribute and key for |
| | 356 | key, `users, crypto, markets, holdings, orders, transactions, market_trades, market_candles, watchlists, watchlist_items` from |
| | 357 | [wiki:RelationalDesign]. Every foreign key matches, |
| | 358 | every candidate key matches (including the less obvious composite ones — `{user_id, crypto_id}` on `holdings`, `{crypto_id, quote_currency}` on `markets`, `{market_id, timeframe, candle_time}` on `market_candles`), and the normal form matches (P2 already claimed 3NF; this |
| | 401 | |
| | 402 | The table definitions in `server/db/schema_creation.sql`: |
| | 403 | |
| | 404 | {{{ |
| | 405 | CREATE TABLE project.users ( |
| | 406 | id uuid PRIMARY KEY DEFAULT gen_random_uuid(), |
| | 407 | username varchar(50) NOT NULL UNIQUE, |
| | 408 | email varchar(255) NOT NULL UNIQUE, |
| | 409 | full_name varchar(200), |
| | 410 | password_hash varchar(255) NOT NULL, |
| | 411 | available_balance numeric(18,4) NOT NULL DEFAULT 0 CHECK (available_balance >= 0), |
| | 412 | invested_balance numeric(18,4) NOT NULL DEFAULT 0 CHECK (invested_balance >= 0), |
| | 413 | created_at timestamptz NOT NULL DEFAULT now(), |
| | 414 | updated_at timestamptz |
| | 415 | ); |
| | 416 | |
| | 417 | -- ============================================================================ |
| | 418 | -- CRYPTO |
| | 419 | -- Catalog of crypto assets available on the platform. |
| | 420 | -- ============================================================================ |
| | 421 | CREATE TABLE project.crypto ( |
| | 422 | id uuid PRIMARY KEY DEFAULT gen_random_uuid(), |
| | 423 | symbol varchar(20) NOT NULL UNIQUE, |
| | 424 | name varchar(255) NOT NULL, |
| | 425 | created_at timestamptz NOT NULL DEFAULT now() |
| | 426 | ); |
| | 427 | |
| | 428 | -- ============================================================================ |
| | 429 | -- MARKETS |
| | 430 | -- A market is a (crypto, quote_currency) pair, e.g. BTC/USD. |
| | 431 | -- ============================================================================ |
| | 432 | CREATE TABLE project.markets ( |
| | 433 | id uuid PRIMARY KEY DEFAULT gen_random_uuid(), |
| | 434 | crypto_id uuid NOT NULL REFERENCES project.crypto(id), |
| | 435 | quote_currency char(3) NOT NULL DEFAULT 'USD', |
| | 436 | is_active boolean NOT NULL DEFAULT true, |
| | 437 | created_at timestamptz NOT NULL DEFAULT now(), |
| | 438 | CONSTRAINT uq_markets UNIQUE (crypto_id, quote_currency) |
| | 439 | ); |
| | 440 | |
| | 441 | -- ============================================================================ |
| | 442 | -- HOLDINGS |
| | 443 | -- Per-user crypto position with running weighted average entry price. |
| | 444 | -- ============================================================================ |
| | 445 | CREATE TABLE project.holdings ( |
| | 446 | id uuid PRIMARY KEY DEFAULT gen_random_uuid(), |
| | 447 | user_id uuid NOT NULL REFERENCES project.users(id) ON DELETE CASCADE, |
| | 448 | crypto_id uuid NOT NULL REFERENCES project.crypto(id), |
| | 449 | quantity numeric(20,4) NOT NULL CHECK (quantity >= 0), |
| | 450 | -- Committed to the user's own open sell orders, not yet removed from the |
| | 451 | -- position. quantity - reserved_quantity is what is actually free to |
| | 452 | -- sell — the crypto-side equivalent of users.available_balance. |
| | 453 | reserved_quantity numeric(20,4) NOT NULL DEFAULT 0 |
| | 454 | CHECK (reserved_quantity >= 0 AND reserved_quantity <= quantity), |
| | 455 | -- Weighted-average entry price. NOT NULL so that the P/L arithmetic in |
| | 456 | -- v_portfolio can never silently produce NULL for an existing position. |
| | 457 | avg_price numeric(18,6) NOT NULL DEFAULT 0 CHECK (avg_price >= 0), |
| | 458 | created_at timestamptz NOT NULL DEFAULT now(), |
| | 459 | updated_at timestamptz, |
| | 460 | CONSTRAINT uq_holdings_user_crypto UNIQUE (user_id, crypto_id) |
| | 461 | ); |
| | 462 | |
| | 463 | -- ============================================================================ |
| | 464 | -- ORDERS |
| | 465 | -- Orders placed by users on a market. |
| | 466 | -- ============================================================================ |
| | 467 | CREATE TABLE project.orders ( |
| | 468 | id uuid PRIMARY KEY DEFAULT gen_random_uuid(), |
| | 469 | user_id uuid NOT NULL REFERENCES project.users(id) ON DELETE CASCADE, |
| | 470 | market_id uuid NOT NULL REFERENCES project.markets(id), |
| | 471 | side varchar(4) NOT NULL CHECK (side IN ('buy', 'sell')), |
| | 472 | type varchar(20) NOT NULL CHECK (type IN ('market', 'limit')), |
| | 473 | status varchar(20) NOT NULL CHECK (status IN ('open', 'executed', 'cancelled')), |
| | 474 | quantity numeric(20,4) NOT NULL CHECK (quantity > 0), |
| | 475 | price numeric(18,6), |
| | 476 | placed_at timestamptz NOT NULL DEFAULT now(), |
| | 477 | executed_at timestamptz |
| | 478 | ); |
| | 479 | |
| | 480 | CREATE INDEX idx_orders_user ON project.orders(user_id); |
| | 481 | CREATE INDEX idx_orders_market ON project.orders(market_id); |
| | 482 | CREATE INDEX idx_orders_status ON project.orders(status); |
| | 483 | |
| | 484 | -- ============================================================================ |
| | 485 | -- TRANSACTIONS |
| | 486 | -- Financial ledger: deposits, buys, sells, fees. |
| | 487 | -- ============================================================================ |
| | 488 | CREATE TABLE project.transactions ( |
| | 489 | id uuid PRIMARY KEY DEFAULT gen_random_uuid(), |
| | 490 | user_id uuid NOT NULL REFERENCES project.users(id) ON DELETE CASCADE, |
| | 491 | type varchar(50) NOT NULL CHECK (type IN ('deposit', 'buy', 'sell', 'fee')), |
| | 492 | amount numeric(18,4) NOT NULL, |
| | 493 | currency char(3) NOT NULL DEFAULT 'USD', |
| | 494 | related_order uuid REFERENCES project.orders(id), |
| | 495 | created_at timestamptz NOT NULL DEFAULT now(), |
| | 496 | description text |
| | 497 | ); |
| | 498 | |
| | 499 | CREATE INDEX idx_transactions_user ON project.transactions(user_id, created_at DESC); |
| | 500 | |
| | 501 | -- ============================================================================ |
| | 502 | -- MARKET TRADES |
| | 503 | -- Raw executed trades on a market. Source of truth for current price. |
| | 504 | -- ============================================================================ |
| | 505 | CREATE TABLE project.market_trades ( |
| | 506 | id bigserial PRIMARY KEY, |
| | 507 | market_id uuid NOT NULL REFERENCES project.markets(id), |
| | 508 | executed_at timestamptz NOT NULL, |
| | 509 | price numeric(18,6) NOT NULL CHECK (price > 0), |
| | 510 | quantity numeric(20,6) NOT NULL CHECK (quantity > 0), |
| | 511 | side varchar(4) CHECK (side IN ('buy', 'sell')), |
| | 512 | source varchar(50) NOT NULL DEFAULT 'simulation' |
| | 513 | ); |
| | 514 | |
| | 515 | CREATE INDEX idx_market_trades_market_time ON project.market_trades(market_id, executed_at DESC); |
| | 516 | |
| | 517 | -- ============================================================================ |
| | 518 | -- MARKET CANDLES |
| | 519 | -- OHLCV aggregates over standard timeframes. |
| | 520 | -- ============================================================================ |
| | 521 | CREATE TABLE project.market_candles ( |
| | 522 | id bigserial PRIMARY KEY, |
| | 523 | market_id uuid NOT NULL REFERENCES project.markets(id), |
| | 524 | timeframe varchar(5) NOT NULL CHECK (timeframe IN ('1m', '5m', '1h', '1d')), |
| | 525 | open numeric(18,6) NOT NULL, |
| | 526 | high numeric(18,6) NOT NULL, |
| | 527 | low numeric(18,6) NOT NULL, |
| | 528 | close numeric(18,6) NOT NULL, |
| | 529 | volume numeric(20,6) NOT NULL, |
| | 530 | candle_time timestamptz NOT NULL, |
| | 531 | CONSTRAINT uq_candle UNIQUE (market_id, timeframe, candle_time) |
| | 532 | ); |
| | 533 | |
| | 534 | CREATE INDEX idx_market_candles_market_tf_time ON project.market_candles(market_id, timeframe, candle_time DESC); |
| | 535 | |
| | 536 | -- ============================================================================ |
| | 537 | -- WATCHLISTS |
| | 538 | -- ============================================================================ |
| | 539 | CREATE TABLE project.watchlists ( |
| | 540 | id uuid PRIMARY KEY DEFAULT gen_random_uuid(), |
| | 541 | user_id uuid NOT NULL REFERENCES project.users(id) ON DELETE CASCADE, |
| | 542 | name varchar(100) NOT NULL, |
| | 543 | created_at timestamptz NOT NULL DEFAULT now() |
| | 544 | ); |
| | 545 | |
| | 546 | CREATE TABLE project.watchlist_items ( |
| | 547 | id uuid PRIMARY KEY DEFAULT gen_random_uuid(), |
| | 548 | watchlist_id uuid NOT NULL REFERENCES project.watchlists(id) ON DELETE CASCADE, |
| | 549 | crypto_id uuid NOT NULL REFERENCES project.crypto(id), |
| | 550 | added_at timestamptz NOT NULL DEFAULT now(), |
| | 551 | CONSTRAINT uq_watchlist_crypto UNIQUE (watchlist_id, crypto_id) |
| | 552 | ); |
| | 553 | }}} |