- Timestamp:
- 09/29/26 20:55:13 (4 hours ago)
- Branches:
- main
- Parents:
- 0cee8ec
- Location:
- docs
- Files:
-
- 3 added
- 16 edited
-
P1-ConceptualModel/ERModel.md (modified) (14 diffs)
-
P1-ConceptualModel/ERModelAIUsage.md (modified) (2 diffs)
-
P1-ConceptualModel/ERModel_v05.png (added)
-
P1-ConceptualModel/ERModel_v05.xml (added)
-
P1-ConceptualModel/P1.zip (modified) ( previous)
-
P1-ConceptualModel/wiki/ERModel.md (modified) (14 diffs)
-
P1-ConceptualModel/wiki/ERModelAIUsage.md (modified) (2 diffs)
-
P2-RelationalDesign/P2.zip (modified) ( previous)
-
P2-RelationalDesign/RelationalDesign.md (modified) (5 diffs)
-
P2-RelationalDesign/RelationalDesignAIUsage.md (modified) (2 diffs)
-
P2-RelationalDesign/relational_diagram_v4.png (added)
-
P2-RelationalDesign/wiki/RelationalDesign.md (modified) (10 diffs)
-
P2-RelationalDesign/wiki/RelationalDesignAIUsage.md (modified) (2 diffs)
-
P4-Prototype/BuildInstructions.md (modified) (2 diffs)
-
P4-Prototype/wiki/BuildInstructions.md (modified) (2 diffs)
-
P5-Normalization/Normalization.md (modified) (1 diff)
-
P5-Normalization/P5.zip (modified) ( previous)
-
P5-Normalization/wiki/Normalization.md (modified) (11 diffs)
-
README.md (modified) (3 diffs)
Legend:
- Unmodified
- Added
- Removed
-
docs/P1-ConceptualModel/ERModel.md
r0cee8ec r1549dae 1 # Entity-Relationship Model v.0 41 # Entity-Relationship Model v.05 2 2 3 3 ## Diagram 4 4 5 5  6 6 7 7 Notation: Chen. Rectangles are entity sets, diamonds are relationships, ellipses … … 11 11 single line marks partial participation. 12 12 13 T wodeliberate modeling decisions worth stating up front:13 Three deliberate modeling decisions worth stating up front: 14 14 15 15 - **No foreign keys appear in the diagram.** Connections between entity sets are 16 16 expressed as relationships, per the notation. Foreign-key columns appear only 17 17 in the relational model in [RelationalDesign](../P2-RelationalDesign/RelationalDesign.md). 18 - **`Holds` and `Contains` are relationships, not entity sets.** Both are M:N and 19 both carry their own attributes, which is exactly what a Chen relationship is 20 for. They become tables (`holdings`, `watchlist_items`) only in P2. 18 - **A position and a watchlist entry are entity sets, not M:N relationships.** 19 `Holdings` (a user's position in an asset) and `WatchlistItems` (an asset on a 20 watchlist) each have their own identifier `id`, and each is connected by two 21 1:N relationships: `Holds` and `PositionIn` for a holding, `Contains` and 22 `Lists` for a watchlist item. Until v04 they were drawn as the M:N 23 relationships `Holds` and `Contains`, but the database has always given 24 `holdings` and `watchlist_items` their own `id` primary key. That is how an 25 entity set is implemented, not an M:N relationship, whose key would be the 26 pair of participating keys. v05 corrects the model to match; see 27 [history](#entity-relationship-model-history). 28 - **Key and uniqueness rules are stated with entity and relationship names, 29 never with foreign-key columns.** For example: "a crypto is quoted at most 30 once per currency", not "`{crypto_id, quote_currency}` is unique". 21 31 22 32 ## Data requirements … … 46 56 | `id` | UUID | PK, required | 47 57 | `username` | text(50) | required, unique | 48 | `email` | text(255) | required, unique, contains `@` |58 | `email` | text(255) | required, unique, contains `@` (checked by the application at registration, not by a database constraint) | 49 59 | `full_name` | text(200) | optional | 50 60 | `password_hash` | text(255) | required — never the password itself; the prototype stores a SHA-256 hex digest | … … 79 89 trades, candles and orders all reference the pair, not the asset. 80 90 81 **Keys:** candidates `{id}`, `{crypto_id, quote_currency}` — that pair is 82 unique by definition, since a given asset can only be quoted once per 83 currency; primary key **`id`**, so that the many entity sets referencing a 84 market carry one narrow column instead of a composite key. 91 **Keys:** candidate `{id}`; primary key **`id`**, so that the many entity sets 92 related to a market need one narrow identifier instead of a composite one. 93 **Uniqueness rule:** a crypto is quoted at most once per currency, so the crypto 94 a market is `QuotedOn` together with its `quote_currency` identifies the market 95 as well. Chen notation cannot draw this, because half of it comes through a 96 relationship. P2 enforces it as `UNIQUE(crypto_id, quote_currency)`. 85 97 86 98 | Attribute | Type | Constraints | … … 98 110 99 111 Placing an order is what triggers a **reservation** of whatever it commits: 100 the crypto being sold (`Hold s.reserved_quantity`, below) on a sell, and the112 the crypto being sold (`Holdings.reserved_quantity`, below) on a sell, and the 101 113 cash (`Users.reserved_balance`) on a buy. Since v04 (after P7) an order can 102 114 wait in the order book and be filled in parts, so `status` is a real … … 121 133 | `quantity` | numeric(20,4) | required, > 0 | 122 134 | `filled_quantity` | numeric(20,4) | required, default 0, between 0 and `quantity` — how much has been traded; remaining = `quantity − filled_quantity` (added in v04, after P7) | 123 | `price` | numeric(18,6) | the limit price; for a market order, the market price when it was placed |135 | `price` | numeric(18,6) | optional — the limit price; for a market order, the market price when it was placed | 124 136 | `placed_at` | timestamptz | required, defaults to now | 125 137 | `executed_at` | timestamptz | optional, set when the order settles | … … 148 160 writes directly. 149 161 150 **Keys:** candidate `{id}` — `{market_id, executed_at}`looks unique in151 principle, but two trades can share a timestamp, so it is not a safe key;162 **Keys:** candidate `{id}` — "market plus `executed_at`" looks unique in 163 principle, but two trades on a market can share a timestamp, so it is not a safe key; 152 164 primary key **`id`** (a plain auto-incrementing integer here rather than a 153 165 UUID, because this is the highest-volume entity set and it is only ever read … … 156 168 | Attribute | Type | Constraints | 157 169 |---|---|---| 158 | `id` | integer | PK, required, auto-generated|170 | `id` | big integer | PK, required, auto-generated (`bigserial` in P2) | 159 171 | `executed_at` | timestamptz | required | 160 172 | `price` | numeric(18,6) | required, > 0 | … … 177 189 | Attribute | Type | Constraints | 178 190 |---|---|---| 179 | `id` | integer | PK, required, auto-generated|191 | `id` | big integer | PK, required, auto-generated (`bigserial` in P2) | 180 192 | `event_type` | text | required, `placed`, `partially_filled`, `filled` or `cancelled` | 181 193 | `quantity` | numeric(20,4) | required — the ordered quantity for `placed`, the filled amount for a fill, the unfilled rest for `cancelled` | 182 194 | `price` | numeric(18,6) | optional — the order price, or the trade price for a fill | 183 195 | `status_after` | text | required, the order's status after the event | 184 | `created_at` | timestamptz | required, defaults to now|196 | `created_at` | timestamptz | required, set automatically when the event is recorded (`clock_timestamp()`, so events inside one transaction keep their real order) | 185 197 186 198 #### MarketCandles … … 190 202 screen refresh does not scale. 191 203 192 **Keys:** candidates `{id}`, `{market_id, timeframe, candle_time}` — a market 193 has exactly one candle per timeframe per time bucket; primary key **`id`**, the 194 composite is enforced as a uniqueness rule because it is the real-world 195 constraint and it is what prevents duplicate candles. 196 197 | Attribute | Type | Constraints | 198 |---|---|---| 199 | `id` | integer | PK, required, auto-generated | 204 **Keys:** candidate `{id}`; primary key **`id`**. **Uniqueness rule:** a market 205 has exactly one candle per timeframe per time bucket, so the market a candle 206 `Aggregates` together with `timeframe` and `candle_time` also identifies it. 207 This is the real-world constraint that prevents duplicate candles. P2 enforces 208 it as `UNIQUE(market_id, timeframe, candle_time)`. 209 210 | Attribute | Type | Constraints | 211 |---|---|---| 212 | `id` | big integer | PK, required, auto-generated (`bigserial` in P2) | 200 213 | `timeframe` | text | required, `1m`, `5m`, `1h` or `1d` | 201 214 | `open`, `high`, `low`, `close` | numeric(18,6) | all required | … … 208 221 several lists ("long term", "watching today") and each needs its own name. 209 222 210 **Keys:** candidate `{id}` — `{user_id, name}` would also work if list names211 were required to be unique per user, which the model does not impose, so it is212 not listed as a candidate key; primary key **`id`**.223 **Keys:** candidate `{id}`; primary key **`id`**. "Owner plus `name`" would 224 also identify a list if names had to be unique per user, but the model does 225 not require that, so there is no uniqueness rule here. 213 226 214 227 | Attribute | Type | Constraints | … … 218 231 | `created_at` | timestamptz | required, defaults to now | 219 232 220 ### Relationships 221 222 #### QuotedOn — Cryptos (1) : Markets (N), total on Markets 223 Ties a market to the asset it trades. One asset can be quoted in many markets; 224 every market must have exactly one asset, hence total participation on the 225 `Markets` side. No attributes. 226 227 #### PlacedOn — Markets (1) : Orders (N), total on Orders 228 Records which market an order was placed on. Every order must name a market; 229 a market may have no orders yet. No attributes. 230 231 #### Places — Users (1) : Orders (N), total on Orders 232 Records who placed an order. Every order belongs to exactly one user; a new 233 user has no orders. No attributes. 234 235 #### Records — Users (1) : Transactions (N), total on Transactions 236 Attributes each ledger entry to a user. Every entry belongs to exactly one 237 user. No attributes. 238 239 #### Settles — Orders (1) : Transactions (N), partial on both sides 240 Links a ledger entry to the order that caused it. Partial on the 241 `Transactions` side because deposits have no originating order, and partial on 242 the `Orders` side because an order that never executes never produces a 243 ledger entry — which is why the corresponding column is nullable in P2. No 244 attributes. 245 246 #### Fills — Markets (1) : MarketTrades (N), total on MarketTrades 247 Every executed trade happened on exactly one market. No attributes. 248 249 #### FillsBuy — Orders (1) : MarketTrades (N), partial on both sides 250 *Added in v04, after P7.* The buy order a trade filled. An order can be 251 filled by many trades (partial fills); a trade fills at most one buy order, 252 and none when the simulated market was the buyer. No attributes. 253 254 #### FillsSell — Orders (1) : MarketTrades (N), partial on both sides 255 *Added in v04, after P7.* The sell order a trade filled, symmetric to 256 `FillsBuy`. A trade between two users' orders participates in both. No 257 attributes. 258 259 #### Logs — Orders (1) : OrderEvents (N), total on OrderEvents 260 *Added in v04, after P7.* Every event belongs to exactly one order. No 261 attributes. 262 263 #### Aggregates — Markets (1) : MarketCandles (N), total on MarketCandles 264 Every candle summarises trades of exactly one market. No attributes. 265 266 #### Owns — Users (1) : Watchlists (N), total on Watchlists 267 Every watchlist belongs to exactly one user. No attributes. 268 269 #### Holds — Users (M) : Cryptos (N), partial on both sides, **with attributes** 270 A user's position in an asset. M:N because one user holds many assets and one 271 asset is held by many users, and partial on both sides because a user may hold 272 nothing and an asset may be held by nobody. Modeled as a relationship rather 273 than an entity set because a position has no identity of its own — it is 274 entirely described by *which user*, *which asset*, and how much. 233 #### Holdings 234 A user's position in one crypto asset: how much of it the user owns, how much 235 of that is already promised to open sell orders, and at what average price it 236 was accumulated. *An entity set since v05* (until v04 it was the M:N 237 relationship `Holds`). A holding has its own identifier and its own 238 lifecycle: it is created on the first buy, updated on every later fill, and 239 the prototype reads and locks it as a unit (`SELECT … FOR UPDATE` on the sell 240 path). It is linked to its owner through `Holds` and to its asset through 241 `PositionIn`. 242 243 **Keys:** candidate `{id}`; primary key **`id`**. **Uniqueness rule:** a user 244 has at most one holding per crypto, so the user who `Holds` it together with 245 the crypto it is a `PositionIn` also identifies a holding. P2 enforces this as 246 `UNIQUE(user_id, crypto_id)`. 275 247 276 248 `reserved_quantity` mirrors `available_balance`/`invested_balance` on `Users`: … … 284 256 | Attribute | Type | Constraints | 285 257 |---|---|---| 258 | `id` | UUID | PK, required | 286 259 | `quantity` | numeric(20,4) | required, ≥ 0 — total amount owned | 287 260 | `reserved_quantity` | numeric(20,4) | required, default 0, `0 ≤ reserved_quantity ≤ quantity` — committed to the user's own open sell orders, not yet removed from the position | 288 | `avg_price` | numeric(18,6) | required, ≥ 0, **derived** (dashed ellipse) — the weighted average of the prices at which the position was accumulated; derivable from the buy history, stored anyway so unrealised P/L can be shown without replaying the whole ledger |261 | `avg_price` | numeric(18,6) | required, default 0, ≥ 0, **derived** (dashed ellipse) — the weighted average of the prices at which the position was accumulated; derivable from the buy history, stored anyway so unrealised P/L can be shown without replaying the whole ledger | 289 262 | `created_at` | timestamptz | required, defaults to now | 290 263 | `updated_at` | timestamptz | optional | 291 264 292 #### Contains — Watchlists (M) : Cryptos (N), partial on both sides, **with attribute** 293 Which assets are on which watchlist. M:N: a list holds many assets, an asset 294 appears on many lists. Partial on both sides — an empty list is valid and an 295 asset need not be on any list. 296 297 | Attribute | Type | Constraints | 298 |---|---|---| 265 #### WatchlistItems 266 One asset placed on one watchlist. *An entity set since v05* (until v04 it was 267 the M:N relationship `Contains`). It has its own identifier, and it is linked 268 to its list through `Contains` and to its asset through `Lists`. 269 270 **Keys:** candidate `{id}`; primary key **`id`**. **Uniqueness rule:** an asset 271 appears at most once on a given list, so the watchlist that `Contains` an item 272 together with the crypto it `Lists` also identifies the item. P2 enforces this 273 as `UNIQUE(watchlist_id, crypto_id)`. 274 275 | Attribute | Type | Constraints | 276 |---|---|---| 277 | `id` | UUID | PK, required | 299 278 | `added_at` | timestamptz | required, defaults to now — recorded so a list can be shown in the order the user built it | 279 280 ### Relationships 281 282 #### QuotedOn — Cryptos (1) : Markets (N), total on Markets 283 Ties a market to the asset it trades. One asset can be quoted in many markets; 284 every market must have exactly one asset, hence total participation on the 285 `Markets` side. No attributes. 286 287 #### PlacedOn — Markets (1) : Orders (N), total on Orders 288 Records which market an order was placed on. Every order must name a market; 289 a market may have no orders yet. No attributes. 290 291 #### Places — Users (1) : Orders (N), total on Orders 292 Records who placed an order. Every order belongs to exactly one user; a new 293 user has no orders. No attributes. 294 295 #### Records — Users (1) : Transactions (N), total on Transactions 296 Attributes each ledger entry to a user. Every entry belongs to exactly one 297 user. No attributes. 298 299 #### Settles — Orders (1) : Transactions (N), partial on both sides 300 Links a ledger entry to the order that caused it. Partial on the 301 `Transactions` side because deposits have no originating order, and partial on 302 the `Orders` side because an order that never executes never produces a 303 ledger entry — which is why the corresponding column is nullable in P2. No 304 attributes. 305 306 #### Fills — Markets (1) : MarketTrades (N), total on MarketTrades 307 Every executed trade happened on exactly one market. No attributes. 308 309 #### FillsBuy — Orders (1) : MarketTrades (N), partial on both sides 310 *Added in v04, after P7.* The buy order a trade filled. An order can be 311 filled by many trades (partial fills); a trade fills at most one buy order, 312 and none when the simulated market was the buyer. The role of `Orders` in this 313 relationship is *the buy order* of the trade. No attributes. 314 315 #### FillsSell — Orders (1) : MarketTrades (N), partial on both sides 316 *Added in v04, after P7.* The sell order a trade filled, symmetric to 317 `FillsBuy`; the role of `Orders` here is *the sell order* of the trade. A 318 trade between two users' orders participates in both. No attributes. 319 320 #### Logs — Orders (1) : OrderEvents (N), total on OrderEvents 321 *Added in v04, after P7.* Every event belongs to exactly one order. No 322 attributes. 323 324 #### Aggregates — Markets (1) : MarketCandles (N), total on MarketCandles 325 Every candle summarises trades of exactly one market. No attributes. 326 327 #### Owns — Users (1) : Watchlists (N), total on Watchlists 328 Every watchlist belongs to exactly one user. No attributes. 329 330 #### Holds — Users (1) : Holdings (N), total on Holdings 331 *1:N since v05.* Every holding belongs to exactly one user. A user may hold 332 nothing yet, so participation is partial on the `Users` side. No attributes. 333 334 #### PositionIn — Cryptos (1) : Holdings (N), total on Holdings 335 *Added in v05.* Every holding is a position in exactly one crypto asset. An 336 asset may be held by nobody. No attributes. 337 338 Together, `Holds` and `PositionIn` still say what the old M:N `Holds` said: 339 a user can hold many assets and an asset can be held by many users. The 340 difference is that the position is now a thing with its own identity, not 341 just a pair. The rule "at most one holding per user and crypto" is stated 342 under [Holdings](#holdings). 343 344 #### Contains — Watchlists (1) : WatchlistItems (N), total on WatchlistItems 345 *1:N since v05.* Every watchlist item is on exactly one list. An empty list is 346 valid, so participation is partial on the `Watchlists` side. No attributes. 347 348 #### Lists — Cryptos (1) : WatchlistItems (N), total on WatchlistItems 349 *Added in v05.* Every watchlist item names exactly one crypto asset. An asset 350 need not be on any list. No attributes. 300 351 301 352 ## Entity-Relationship Model History … … 341 392 Nothing existing was removed or changed. See 342 393 [AdvancedDatabaseDevelopment](../P7-AdvancedDatabaseDevelopment/AdvancedDatabaseDevelopment.md). 343 The diagram files are `ERModel_v04.xml` / `ERModel_v04.png`; earlier versions 394 The diagram files are `ERModel_v04.xml` / `ERModel_v04.png`. 395 - **v05 — correction after review.** The review of P2 found that two parts of 396 the model were implemented differently in the database: 397 - `Contains` was an M:N relationship in the model, but `watchlist_items` 398 has its own `id` primary key; 399 - `Holds` was an M:N relationship in the model, but `holdings` has its own 400 `id` primary key. 401 402 An M:N relationship has no identifier of its own; its table's key is the pair 403 of participating keys. A table with its own `id` is the implementation of an 404 entity set. Every phase after P2 (the prototype, the reports and the P7 405 logic) already uses the database as it is. So the **model** was corrected to 406 match P2, not the other way round: 407 - `Holds` (M:N, with attributes) became the entity set `Holdings` (its former 408 attributes plus `id`) with two 1:N relationships, `Holds` (Users → Holdings) 409 and `PositionIn` (Cryptos → Holdings), both total on the `Holdings` side; 410 - `Contains` (M:N, with `added_at`) became the entity set `WatchlistItems` 411 (`id`, `added_at`) with `Contains` (Watchlists → WatchlistItems) and 412 `Lists` (Cryptos → WatchlistItems), both total on the `WatchlistItems` 413 side; 414 - the former keys of the two relationships are kept as uniqueness rules 415 ("one holding per user and crypto", "an asset at most once per list"); 416 - the key descriptions of `Markets`, `MarketTrades`, `MarketCandles` and 417 `Watchlists` no longer name foreign-key columns (`crypto_id`, 418 `market_id`, `user_id`), which do not exist in an ER model; 419 - the diagram was redrawn on a grid with no overlapping attributes. In v04, 420 `Watchlists.id` was hidden behind `added_at`, and several attributes of 421 `Orders`, `Transactions`, `MarketTrades` and `MarketCandles` overlapped. 422 The grid also makes it easier to compare the diagram with the P2 423 relational diagram. 424 425 The diagram files are `ERModel_v05.xml` / `ERModel_v05.png`; earlier versions 344 426 are kept. 345 427 -
docs/P1-ConceptualModel/ERModelAIUsage.md
r0cee8ec r1549dae 13 13 ### Diagram 14 14 15 `ERModel_v01.xml` / `ERModel_v01.png`. 15 Current version: `ERModel_v05.xml` / `ERModel_v05.png` (see session 5 below). First 16 version: `ERModel_v01.xml` / `ERModel_v01.png`. 16 17 17 18 **Declaration of how the diagram was produced.** The initial model is the … … 313 314 **What I decided:** to add the P7 structure to the ER model. The new elements are placed 314 315 automatically, so the layout can be tidied by hand in TerraER. 316 317 318 ### Session 5 — 2026-09-29 (Claude Opus 5.5): v05, correction after review 319 320 **Prompt (student, excerpt):** I pasted the reviewer's comments that `Holds` and `Contains` 321 are implemented differently in the database than in the model, with the suggestion to 322 "correct only the Phase 1 model so that it matches Phase 2", and asked: 323 324 > Can you take a look at this stuff? and fix them, but don't push anything just make the 325 > changes requested and try to explain it to me to see if you understand the problem with 326 > the solution 327 328 **Response (AI, summarised):** 329 330 - Explained that an M:N relationship has no identifier of its own, while `holdings` and 331 `watchlist_items` each have an `id` primary key, so the database treats them as entity 332 sets. 333 - Changed the model to v05. `Holdings` (`id`, `quantity`, `reserved_quantity`, derived 334 `avg_price`, `created_at`, `updated_at`) and `WatchlistItems` (`id`, `added_at`) became 335 entity sets, with the 1:N relationships `Holds`, `PositionIn`, `Contains` and `Lists`, 336 each total on the new entity's side. The old relationship keys are now stated as 337 uniqueness rules. The key descriptions no longer name foreign-key columns. 338 - Generated `ERModel_v05.xml` / `ERModel_v05.png` from scratch with TerraER 3.11's own figure 339 classes and writer (adapted from the v01 generator), on a grid, with no overlapping 340 attributes. It was verified by reading the file back with TerraER's reader (184 figures) 341 and by inspecting the rendered PNG. 342 - Updated [ERModel](ERModel.md) (v05 sections and history entry). 343 344 **What I decided:** to follow the reviewer's advice and change the model rather than the 345 database, since every later phase already uses the database as it is. -
docs/P1-ConceptualModel/wiki/ERModel.md
r0cee8ec r1549dae 1 = Entity-Relationship Model v.0 4=1 = Entity-Relationship Model v.05 = 2 2 3 3 == Diagram == 4 4 5 [[Image(ERModel_v0 4.png, 800px)]]5 [[Image(ERModel_v05.png, 800px)]] 6 6 7 7 Notation: Chen. Rectangles are entity sets, diamonds are relationships, ellipses … … 11 11 single line marks partial participation. 12 12 13 T wodeliberate modeling decisions worth stating up front:13 Three deliberate modeling decisions worth stating up front: 14 14 15 15 * '''No foreign keys appear in the diagram.''' Connections between entity sets are expressed as relationships, per the notation. Foreign-key columns appear only in the relational model in [wiki:RelationalDesign]. 16 * '''`Holds` and `Contains` are relationships, not entity sets.''' Both are M:N and both carry their own attributes, which is exactly what a Chen relationship is for. They become tables (`holdings`, `watchlist_items`) only in P2. 16 * '''A position and a watchlist entry are entity sets, not M:N relationships.''' `Holdings` (a user's position in an asset) and `WatchlistItems` (an asset on a watchlist) each have their own identifier `id`, and each is connected by two 1:N relationships: `Holds` and `PositionIn` for a holding, `Contains` and `Lists` for a watchlist item. Until v04 they were drawn as the M:N relationships `Holds` and `Contains`, but the database has always given `holdings` and `watchlist_items` their own `id` primary key. That is how an entity set is implemented, not an M:N relationship, whose key would be the pair of participating keys. v05 corrects the model to match; see history. 17 * '''Key and uniqueness rules are stated with entity and relationship names, never with foreign-key columns.''' For example: "a crypto is quoted at most once per currency", not "`{crypto_id, quote_currency}` is unique". 17 18 18 19 == Data requirements == … … 41 42 || `id` || UUID || PK, required || 42 43 || `username` || text(50) || required, unique || 43 || `email` || text(255) || required, unique, contains `@` ||44 || `email` || text(255) || required, unique, contains `@` (checked by the application at registration, not by a database constraint) || 44 45 || `full_name` || text(200) || optional || 45 46 || `password_hash` || text(255) || required — never the password itself; the prototype stores a SHA-256 hex digest || … … 73 74 trades, candles and orders all reference the pair, not the asset. 74 75 75 '''Keys:''' candidates `{id}`, `{crypto_id, quote_currency}` — that pair is 76 unique by definition, since a given asset can only be quoted once per 77 currency; primary key '''`id`''', so that the many entity sets referencing a 78 market carry one narrow column instead of a composite key. 76 '''Keys:''' candidate `{id}`; primary key '''`id`''', so that the many entity sets 77 related to a market need one narrow identifier instead of a composite one. 78 '''Uniqueness rule:''' a crypto is quoted at most once per currency, so the crypto 79 a market is `QuotedOn` together with its `quote_currency` identifies the market 80 as well. Chen notation cannot draw this, because half of it comes through a 81 relationship. P2 enforces it as `UNIQUE(crypto_id, quote_currency)`. 79 82 80 83 ||= Attribute =||= Type =||= Constraints =|| … … 91 94 92 95 Placing an order is what triggers a '''reservation''' of whatever it commits: 93 the crypto being sold (`Hold s.reserved_quantity`, below) on a sell, and the96 the crypto being sold (`Holdings.reserved_quantity`, below) on a sell, and the 94 97 cash (`Users.reserved_balance`) on a buy. Since v04 (after P7) an order can 95 98 wait in the order book and be filled in parts, so `status` is a real … … 113 116 || `quantity` || numeric(20,4) || required, > 0 || 114 117 || `filled_quantity` || numeric(20,4) || required, default 0, between 0 and `quantity` — how much has been traded; remaining = `quantity − filled_quantity` (added in v04, after P7) || 115 || `price` || numeric(18,6) || the limit price; for a market order, the market price when it was placed ||118 || `price` || numeric(18,6) || optional — the limit price; for a market order, the market price when it was placed || 116 119 || `placed_at` || timestamptz || required, defaults to now || 117 120 || `executed_at` || timestamptz || optional, set when the order settles || … … 139 142 writes directly. 140 143 141 '''Keys:''' candidate `{id}` — `{market_id, executed_at}`looks unique in142 principle, but two trades can share a timestamp, so it is not a safe key;144 '''Keys:''' candidate `{id}` — "market plus `executed_at`" looks unique in 145 principle, but two trades on a market can share a timestamp, so it is not a safe key; 143 146 primary key '''`id`''' (a plain auto-incrementing integer here rather than a 144 147 UUID, because this is the highest-volume entity set and it is only ever read … … 146 149 147 150 ||= Attribute =||= Type =||= Constraints =|| 148 || `id` || integer || PK, required, auto-generated||151 || `id` || big integer || PK, required, auto-generated (`bigserial` in P2) || 149 152 || `executed_at` || timestamptz || required || 150 153 || `price` || numeric(18,6) || required, > 0 || … … 166 169 167 170 ||= Attribute =||= Type =||= Constraints =|| 168 || `id` || integer || PK, required, auto-generated||171 || `id` || big integer || PK, required, auto-generated (`bigserial` in P2) || 169 172 || `event_type` || text || required, `placed`, `partially_filled`, `filled` or `cancelled` || 170 173 || `quantity` || numeric(20,4) || required — the ordered quantity for `placed`, the filled amount for a fill, the unfilled rest for `cancelled` || 171 174 || `price` || numeric(18,6) || optional — the order price, or the trade price for a fill || 172 175 || `status_after` || text || required, the order's status after the event || 173 || `created_at` || timestamptz || required, defaults to now||176 || `created_at` || timestamptz || required, set automatically when the event is recorded (`clock_timestamp()`, so events inside one transaction keep their real order) || 174 177 175 178 ==== !MarketCandles ==== … … 179 182 screen refresh does not scale. 180 183 181 '''Keys:''' candidates `{id}`, `{market_id, timeframe, candle_time}` — a market 182 has exactly one candle per timeframe per time bucket; primary key '''`id`''', the 183 composite is enforced as a uniqueness rule because it is the real-world 184 constraint and it is what prevents duplicate candles. 185 186 ||= Attribute =||= Type =||= Constraints =|| 187 || `id` || integer || PK, required, auto-generated || 184 '''Keys:''' candidate `{id}`; primary key '''`id`'''. '''Uniqueness rule:''' a market 185 has exactly one candle per timeframe per time bucket, so the market a candle 186 `Aggregates` together with `timeframe` and `candle_time` also identifies it. 187 This is the real-world constraint that prevents duplicate candles. P2 enforces 188 it as `UNIQUE(market_id, timeframe, candle_time)`. 189 190 ||= Attribute =||= Type =||= Constraints =|| 191 || `id` || big integer || PK, required, auto-generated (`bigserial` in P2) || 188 192 || `timeframe` || text || required, `1m`, `5m`, `1h` or `1d` || 189 193 || `open`, `high`, `low`, `close` || numeric(18,6) || all required || … … 196 200 several lists ("long term", "watching today") and each needs its own name. 197 201 198 '''Keys:''' candidate `{id}` — `{user_id, name}` would also work if list names199 were required to be unique per user, which the model does not impose, so it is200 not listed as a candidate key; primary key '''`id`'''.202 '''Keys:''' candidate `{id}`; primary key '''`id`'''. "Owner plus `name`" would 203 also identify a list if names had to be unique per user, but the model does 204 not require that, so there is no uniqueness rule here. 201 205 202 206 ||= Attribute =||= Type =||= Constraints =|| … … 205 209 || `created_at` || timestamptz || required, defaults to now || 206 210 207 === Relationships === 208 209 ==== !QuotedOn — Cryptos (1) : Markets (N), total on Markets ==== 210 Ties a market to the asset it trades. One asset can be quoted in many markets; 211 every market must have exactly one asset, hence total participation on the 212 `Markets` side. No attributes. 213 214 ==== !PlacedOn — Markets (1) : Orders (N), total on Orders ==== 215 Records which market an order was placed on. Every order must name a market; 216 a market may have no orders yet. No attributes. 217 218 ==== Places — Users (1) : Orders (N), total on Orders ==== 219 Records who placed an order. Every order belongs to exactly one user; a new 220 user has no orders. No attributes. 221 222 ==== Records — Users (1) : Transactions (N), total on Transactions ==== 223 Attributes each ledger entry to a user. Every entry belongs to exactly one 224 user. No attributes. 225 226 ==== Settles — Orders (1) : Transactions (N), partial on both sides ==== 227 Links a ledger entry to the order that caused it. Partial on the 228 `Transactions` side because deposits have no originating order, and partial on 229 the `Orders` side because an order that never executes never produces a 230 ledger entry — which is why the corresponding column is nullable in P2. No 231 attributes. 232 233 ==== Fills — Markets (1) : !MarketTrades (N), total on !MarketTrades ==== 234 Every executed trade happened on exactly one market. No attributes. 235 236 ==== !FillsBuy — Orders (1) : !MarketTrades (N), partial on both sides ==== 237 ''Added in v04, after P7.'' The buy order a trade filled. An order can be 238 filled by many trades (partial fills); a trade fills at most one buy order, 239 and none when the simulated market was the buyer. No attributes. 240 241 ==== !FillsSell — Orders (1) : !MarketTrades (N), partial on both sides ==== 242 ''Added in v04, after P7.'' The sell order a trade filled, symmetric to 243 `FillsBuy`. A trade between two users' orders participates in both. No 244 attributes. 245 246 ==== Logs — Orders (1) : !OrderEvents (N), total on !OrderEvents ==== 247 ''Added in v04, after P7.'' Every event belongs to exactly one order. No 248 attributes. 249 250 ==== Aggregates — Markets (1) : !MarketCandles (N), total on !MarketCandles ==== 251 Every candle summarises trades of exactly one market. No attributes. 252 253 ==== Owns — Users (1) : Watchlists (N), total on Watchlists ==== 254 Every watchlist belongs to exactly one user. No attributes. 255 256 ==== Holds — Users (M) : Cryptos (N), partial on both sides, '''with attributes''' ==== 257 A user's position in an asset. M:N because one user holds many assets and one 258 asset is held by many users, and partial on both sides because a user may hold 259 nothing and an asset may be held by nobody. Modeled as a relationship rather 260 than an entity set because a position has no identity of its own — it is 261 entirely described by ''which user'', ''which asset'', and how much. 211 ==== Holdings ==== 212 A user's position in one crypto asset: how much of it the user owns, how much 213 of that is already promised to open sell orders, and at what average price it 214 was accumulated. ''An entity set since v05'' (until v04 it was the M:N 215 relationship `Holds`). A holding has its own identifier and its own 216 lifecycle: it is created on the first buy, updated on every later fill, and 217 the prototype reads and locks it as a unit (`SELECT … FOR UPDATE` on the sell 218 path). It is linked to its owner through `Holds` and to its asset through 219 `PositionIn`. 220 221 '''Keys:''' candidate `{id}`; primary key '''`id`'''. '''Uniqueness rule:''' a user 222 has at most one holding per crypto, so the user who `Holds` it together with 223 the crypto it is a `PositionIn` also identifies a holding. P2 enforces this as 224 `UNIQUE(user_id, crypto_id)`. 262 225 263 226 `reserved_quantity` mirrors `available_balance`/`invested_balance` on `Users`: … … 270 233 271 234 ||= Attribute =||= Type =||= Constraints =|| 235 || `id` || UUID || PK, required || 272 236 || `quantity` || numeric(20,4) || required, ≥ 0 — total amount owned || 273 237 || `reserved_quantity` || numeric(20,4) || required, default 0, `0 ≤ reserved_quantity ≤ quantity` — committed to the user's own open sell orders, not yet removed from the position || 274 || `avg_price` || numeric(18,6) || required, ≥ 0, '''derived''' (dashed ellipse) — the weighted average of the prices at which the position was accumulated; derivable from the buy history, stored anyway so unrealised P/L can be shown without replaying the whole ledger ||238 || `avg_price` || numeric(18,6) || required, default 0, ≥ 0, '''derived''' (dashed ellipse) — the weighted average of the prices at which the position was accumulated; derivable from the buy history, stored anyway so unrealised P/L can be shown without replaying the whole ledger || 275 239 || `created_at` || timestamptz || required, defaults to now || 276 240 || `updated_at` || timestamptz || optional || 277 241 278 ==== Contains — Watchlists (M) : Cryptos (N), partial on both sides, '''with attribute''' ==== 279 Which assets are on which watchlist. M:N: a list holds many assets, an asset 280 appears on many lists. Partial on both sides — an empty list is valid and an 281 asset need not be on any list. 282 283 ||= Attribute =||= Type =||= Constraints =|| 242 ==== !WatchlistItems ==== 243 One asset placed on one watchlist. ''An entity set since v05'' (until v04 it was 244 the M:N relationship `Contains`). It has its own identifier, and it is linked 245 to its list through `Contains` and to its asset through `Lists`. 246 247 '''Keys:''' candidate `{id}`; primary key '''`id`'''. '''Uniqueness rule:''' an asset 248 appears at most once on a given list, so the watchlist that `Contains` an item 249 together with the crypto it `Lists` also identifies the item. P2 enforces this 250 as `UNIQUE(watchlist_id, crypto_id)`. 251 252 ||= Attribute =||= Type =||= Constraints =|| 253 || `id` || UUID || PK, required || 284 254 || `added_at` || timestamptz || required, defaults to now — recorded so a list can be shown in the order the user built it || 255 256 === Relationships === 257 258 ==== !QuotedOn — Cryptos (1) : Markets (N), total on Markets ==== 259 Ties a market to the asset it trades. One asset can be quoted in many markets; 260 every market must have exactly one asset, hence total participation on the 261 `Markets` side. No attributes. 262 263 ==== !PlacedOn — Markets (1) : Orders (N), total on Orders ==== 264 Records which market an order was placed on. Every order must name a market; 265 a market may have no orders yet. No attributes. 266 267 ==== Places — Users (1) : Orders (N), total on Orders ==== 268 Records who placed an order. Every order belongs to exactly one user; a new 269 user has no orders. No attributes. 270 271 ==== Records — Users (1) : Transactions (N), total on Transactions ==== 272 Attributes each ledger entry to a user. Every entry belongs to exactly one 273 user. No attributes. 274 275 ==== Settles — Orders (1) : Transactions (N), partial on both sides ==== 276 Links a ledger entry to the order that caused it. Partial on the 277 `Transactions` side because deposits have no originating order, and partial on 278 the `Orders` side because an order that never executes never produces a 279 ledger entry — which is why the corresponding column is nullable in P2. No 280 attributes. 281 282 ==== Fills — Markets (1) : !MarketTrades (N), total on !MarketTrades ==== 283 Every executed trade happened on exactly one market. No attributes. 284 285 ==== !FillsBuy — Orders (1) : !MarketTrades (N), partial on both sides ==== 286 ''Added in v04, after P7.'' The buy order a trade filled. An order can be 287 filled by many trades (partial fills); a trade fills at most one buy order, 288 and none when the simulated market was the buyer. The role of `Orders` in this 289 relationship is ''the buy order'' of the trade. No attributes. 290 291 ==== !FillsSell — Orders (1) : !MarketTrades (N), partial on both sides ==== 292 ''Added in v04, after P7.'' The sell order a trade filled, symmetric to 293 `FillsBuy`; the role of `Orders` here is ''the sell order'' of the trade. A 294 trade between two users' orders participates in both. No attributes. 295 296 ==== Logs — Orders (1) : !OrderEvents (N), total on !OrderEvents ==== 297 ''Added in v04, after P7.'' Every event belongs to exactly one order. No 298 attributes. 299 300 ==== Aggregates — Markets (1) : !MarketCandles (N), total on !MarketCandles ==== 301 Every candle summarises trades of exactly one market. No attributes. 302 303 ==== Owns — Users (1) : Watchlists (N), total on Watchlists ==== 304 Every watchlist belongs to exactly one user. No attributes. 305 306 ==== Holds — Users (1) : Holdings (N), total on Holdings ==== 307 ''1:N since v05.'' Every holding belongs to exactly one user. A user may hold 308 nothing yet, so participation is partial on the `Users` side. No attributes. 309 310 ==== !PositionIn — Cryptos (1) : Holdings (N), total on Holdings ==== 311 ''Added in v05.'' Every holding is a position in exactly one crypto asset. An 312 asset may be held by nobody. No attributes. 313 314 Together, `Holds` and `PositionIn` still say what the old M:N `Holds` said: 315 a user can hold many assets and an asset can be held by many users. The 316 difference is that the position is now a thing with its own identity, not 317 just a pair. The rule "at most one holding per user and crypto" is stated 318 under Holdings. 319 320 ==== Contains — Watchlists (1) : !WatchlistItems (N), total on !WatchlistItems ==== 321 ''1:N since v05.'' Every watchlist item is on exactly one list. An empty list is 322 valid, so participation is partial on the `Watchlists` side. No attributes. 323 324 ==== Lists — Cryptos (1) : !WatchlistItems (N), total on !WatchlistItems ==== 325 ''Added in v05.'' Every watchlist item names exactly one crypto asset. An asset 326 need not be on any list. No attributes. 285 327 286 328 == Entity-Relationship Model History == … … 300 342 Nothing existing was removed or changed. See 301 343 [wiki:AdvancedDatabaseDevelopment]. 302 The diagram files are `ERModel_v04.xml` / `ERModel_v04.png`; earlier versions 344 The diagram files are `ERModel_v04.xml` / `ERModel_v04.png`. 345 * '''v05 — correction after review.''' The review of P2 found that two parts of the model were implemented differently in the database: 346 * `Contains` was an M:N relationship in the model, but `watchlist_items` has its own `id` primary key; 347 * `Holds` was an M:N relationship in the model, but `holdings` has its own `id` primary key. 348 349 An M:N relationship has no identifier of its own; its table's key is the pair 350 of participating keys. A table with its own `id` is the implementation of an 351 entity set. Every phase after P2 (the prototype, the reports and the P7 352 logic) already uses the database as it is. So the '''model''' was corrected to 353 match P2, not the other way round: 354 * `Holds` (M:N, with attributes) became the entity set `Holdings` (its former attributes plus `id`) with two 1:N relationships, `Holds` (Users → Holdings) and `PositionIn` (Cryptos → Holdings), both total on the `Holdings` side; 355 * `Contains` (M:N, with `added_at`) became the entity set `WatchlistItems` (`id`, `added_at`) with `Contains` (Watchlists → !WatchlistItems) and `Lists` (Cryptos → !WatchlistItems), both total on the `WatchlistItems` side; 356 * the former keys of the two relationships are kept as uniqueness rules ("one holding per user and crypto", "an asset at most once per list"); 357 * the key descriptions of `Markets`, `MarketTrades`, `MarketCandles` and `Watchlists` no longer name foreign-key columns (`crypto_id`, `market_id`, `user_id`), which do not exist in an ER model; 358 * the diagram was redrawn on a grid with no overlapping attributes. In v04, `Watchlists.id` was hidden behind `added_at`, and several attributes of `Orders`, `Transactions`, `MarketTrades` and `MarketCandles` overlapped. The grid also makes it easier to compare the diagram with the P2 relational diagram. 359 360 The diagram files are `ERModel_v05.xml` / `ERModel_v05.png`; earlier versions 303 361 are kept. 304 362 -
docs/P1-ConceptualModel/wiki/ERModelAIUsage.md
r0cee8ec r1549dae 13 13 === Diagram === 14 14 15 `ERModel_v01.xml` / `ERModel_v01.png`. 15 Current version: `ERModel_v05.xml` / `ERModel_v05.png` (see session 5 below). First 16 version: `ERModel_v01.xml` / `ERModel_v01.png`. 16 17 17 18 '''Declaration of how the diagram was produced.''' The initial model is the … … 305 306 '''What I decided:''' to add the P7 structure to the ER model. The new elements are placed 306 307 automatically, so the layout can be tidied by hand in TerraER. 308 309 === Session 5 — 2026-09-29 (Claude Opus 5.5): v05, correction after review === 310 311 '''Prompt (student, excerpt):''' I pasted the reviewer's comments that `Holds` and `Contains` 312 are implemented differently in the database than in the model, with the suggestion to 313 "correct only the Phase 1 model so that it matches Phase 2", and asked: 314 315 > Can you take a look at this stuff? and fix them, but don't push anything just make the 316 > changes requested and try to explain it to me to see if you understand the problem with 317 > the solution 318 319 '''Response (AI, summarised):''' 320 321 * Explained that an M:N relationship has no identifier of its own, while `holdings` and `watchlist_items` each have an `id` primary key, so the database treats them as entity sets. 322 * Changed the model to v05. `Holdings` (`id`, `quantity`, `reserved_quantity`, derived `avg_price`, `created_at`, `updated_at`) and `WatchlistItems` (`id`, `added_at`) became entity sets, with the 1:N relationships `Holds`, `PositionIn`, `Contains` and `Lists`, each total on the new entity's side. The old relationship keys are now stated as uniqueness rules. The key descriptions no longer name foreign-key columns. 323 * Generated `ERModel_v05.xml` / `ERModel_v05.png` from scratch with TerraER 3.11's own figure classes and writer (adapted from the v01 generator), on a grid, with no overlapping attributes. It was verified by reading the file back with TerraER's reader (184 figures) and by inspecting the rendered PNG. 324 * Updated [wiki:ERModel] (v05 sections and history entry). 325 326 '''What I decided:''' to follow the reviewer's advice and change the model rather than the 327 database, since every later phase already uses the database as it is. -
docs/P2-RelationalDesign/RelationalDesign.md
r0cee8ec r1549dae 1 1 # Relational Design 2 2 3 This page transforms [ERModel](../P1-ConceptualModel/ERModel.md) **v05** into 4 relations. Every relation below corresponds to exactly one entity set of the 5 model, and every foreign key corresponds to exactly one relationship, so the 6 two diagrams can be compared box for box and line for line (see 7 [Relational diagram](#relational-diagram)). 8 3 9 ## Descriptive representation of the relational schema 4 10 5 Notation: **bold** = primary key, *italic* = foreign key. 6 7 - **Users**(<u>**id**</u>, username, email, full_name, password_hash, available_balance, invested_balance, created_at, updated_at) 8 - Candidate keys: `{id}`, `{username}`, `{email}`. `UNIQUE(username)`, `UNIQUE(email)`. 11 Notation: **bold** = primary key, *italic* = foreign key. After each foreign key 12 comes the ER relationship it implements. 13 14 - **Users**(<u>**id**</u>, username, email, full_name, password_hash, available_balance, invested_balance, reserved_balance, created_at, updated_at) 15 - Entity set `Users`. Candidate keys: `{id}`, `{username}`, `{email}`. `UNIQUE(username)`, `UNIQUE(email)`. 9 16 - **Crypto**(<u>**id**</u>, symbol, name, created_at) 10 - Candidate keys: `{id}`, `{symbol}`. `UNIQUE(symbol)`. 11 - **Markets**(<u>**id**</u>, *crypto_id*, quote_currency, is_active, created_at) 12 - Candidate keys: `{id}`, `{crypto_id, quote_currency}`. `UNIQUE(crypto_id, quote_currency)`. 13 - **Holdings**(<u>**id**</u>, *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. 17 - Entity set `Cryptos`. Candidate keys: `{id}`, `{symbol}`. `UNIQUE(symbol)`. 18 - **Markets**(<u>**id**</u>, *crypto_id* [`QuotedOn`], quote_currency, is_active, created_at) 19 - Entity set `Markets`. Candidate keys: `{id}`, `{crypto_id, quote_currency}` 20 (the model's rule "a crypto is quoted at most once per currency"), 21 enforced with `UNIQUE(crypto_id, quote_currency)`. 22 - **Holdings**(<u>**id**</u>, *user_id* [`Holds`], *crypto_id* [`PositionIn`], quantity, reserved_quantity, avg_price, created_at, updated_at) 23 - Entity set `Holdings`. Candidate keys: `{id}` and `{user_id, crypto_id}` 24 (the model's rule "one holding per user and crypto"), enforced with 25 `UNIQUE(user_id, crypto_id)`. 18 26 - `avg_price` is `NOT NULL DEFAULT 0 CHECK (avg_price >= 0)`. 19 27 - `reserved_quantity` is `NOT NULL DEFAULT 0 CHECK (reserved_quantity >= 0 … … 23 31 needed, in `v_portfolio` as `available_quantity` and in the sell path of 24 32 [UseCase0005](../P3-UseCaseModel/UseCase0005.md). See 25 [ERModel](../P1-ConceptualModel/ERModel.md#hold s--users-m--cryptos-n-partial-on-both-sides-with-attributes)33 [ERModel](../P1-ConceptualModel/ERModel.md#holdings) 26 34 for why this mirrors `available_balance`/`invested_balance` on `Users`. 27 - **Orders**(<u>**id**</u>, *user_id*, *market_id*, side, type, status, quantity, price, placed_at, executed_at) 28 - `side ∈ {buy, sell}`, `type ∈ {market, limit}`, `status ∈ {open, executed, cancelled}`. 29 - **Transactions**(<u>**id**</u>, *user_id*, type, amount, currency, *related_order*, created_at, description) 30 - `type ∈ {deposit, buy, sell, fee}`. 31 - **MarketTrades**(<u>**id**</u>, *market_id*, executed_at, price, quantity, side, source) 32 - **MarketCandles**(<u>**id**</u>, *market_id*, timeframe, open, high, low, close, volume, candle_time) 33 - `UNIQUE(market_id, timeframe, candle_time)`. 34 - **Watchlists**(<u>**id**</u>, *user_id*, name, created_at) 35 - **WatchlistItems**(<u>**id**</u>, *watchlist_id*, *crypto_id*, added_at) 36 - Transformation of the M:N relationship `Contains`. Candidate keys: `{id}` 37 and `{watchlist_id, crypto_id}`, the latter enforced with 38 `UNIQUE(watchlist_id, crypto_id)`. 35 - **Orders**(<u>**id**</u>, *user_id* [`Places`], *market_id* [`PlacedOn`], side, type, status, quantity, filled_quantity, price, placed_at, executed_at) 36 - Entity set `Orders`. `side ∈ {buy, sell}`, `type ∈ {market, limit}`, 37 `status ∈ {open, partially_filled, executed, cancelled}`, 38 `0 ≤ filled_quantity ≤ quantity`. 39 - **Transactions**(<u>**id**</u>, *user_id* [`Records`], type, amount, currency, *related_order* [`Settles`], created_at, description) 40 - Entity set `Transactions`. `type ∈ {deposit, buy, sell, fee}`. 41 `related_order` is nullable (see below). 42 - **MarketTrades**(<u>**id**</u>, *market_id* [`Fills`], executed_at, price, quantity, side, source, *buy_order_id* [`FillsBuy`], *sell_order_id* [`FillsSell`]) 43 - Entity set `MarketTrades`. `buy_order_id` and `sell_order_id` are both 44 nullable (see below). 45 - **OrderEvents**(<u>**id**</u>, *order_id* [`Logs`], event_type, quantity, price, status_after, created_at) 46 - Entity set `OrderEvents`. `event_type ∈ {placed, partially_filled, filled, cancelled}`. 47 - **MarketCandles**(<u>**id**</u>, *market_id* [`Aggregates`], timeframe, open, high, low, close, volume, candle_time) 48 - Entity set `MarketCandles`. Candidate keys: `{id}`, `{market_id, timeframe, 49 candle_time}` (the model's rule "one candle per market, timeframe and 50 bucket"), enforced with `UNIQUE(market_id, timeframe, candle_time)`. 51 - **Watchlists**(<u>**id**</u>, *user_id* [`Owns`], name, created_at) 52 - Entity set `Watchlists`. 53 - **WatchlistItems**(<u>**id**</u>, *watchlist_id* [`Contains`], *crypto_id* [`Lists`], added_at) 54 - Entity set `WatchlistItems`. Candidate keys: `{id}` and `{watchlist_id, 55 crypto_id}` (the model's rule "an asset at most once per list"), enforced 56 with `UNIQUE(watchlist_id, crypto_id)`. 39 57 40 58 ### Transformation method used 41 59 42 **Partial transformation.** Applied as follows: 43 44 - Each of the 8 entity sets in [ERModel](../P1-ConceptualModel/ERModel.md) becomes one table, keeping 45 its UUID (or serial) primary key. 46 - Each **1:N relationship without attributes** is transformed by adding the 47 parent's primary key as a foreign-key column on the child table — the "N" 48 side. This is where every foreign key in the schema comes from, and it is why 49 no foreign keys appear in the ER diagram itself: 50 `QuotedOn` → `markets.crypto_id`, `PlacedOn` → `orders.market_id`, 51 `Places` → `orders.user_id`, `Records` → `transactions.user_id`, 52 `Settles` → `transactions.related_order`, `Fills` → `market_trades.market_id`, 53 `Aggregates` → `market_candles.market_id`, `Owns` → `watchlists.user_id`. 54 - Each **M:N relationship** becomes its own table holding the two foreign keys 55 plus the relationship's own attributes: `Holds` → `holdings`, 56 `Contains` → `watchlist_items`. The pair of foreign keys is the relationship's 57 key and is enforced as a `UNIQUE` constraint in both tables. 58 - **Total participation** in the ER model becomes `NOT NULL` on the 59 corresponding foreign key; partial participation stays nullable. `Settles` is 60 partial on both sides, which is exactly why `transactions.related_order` is 61 the one nullable foreign key in the schema — a deposit has no originating 62 order. 60 **Partial transformation.** The model has 11 entity sets and 15 relationships. 61 Every relationship is binary and 1:N with no attributes of its own (the two M:N 62 relationships of earlier versions, `Holds` and `Contains`, were corrected into 63 the entity sets `Holdings` and `WatchlistItems` in v05). The rules: 64 65 - **Each entity set becomes one relation**, with its own attributes and its 66 own key `id` as primary key. 11 entity sets → 11 relations. 67 - **Each 1:N relationship becomes one foreign key** on the relation of the "N" 68 side, pointing to the primary key of the "1" side. No relationship gets its 69 own table, because none is M:N and none has attributes. 15 relationships → 70 15 foreign keys: 71 72 | ER relationship | 1 side → N side | Foreign key | Participation of the N side | `NULL`? | 73 |---|---|---|---|---| 74 | `QuotedOn` | Cryptos → Markets | `markets.crypto_id` | total | `NOT NULL` | 75 | `PlacedOn` | Markets → Orders | `orders.market_id` | total | `NOT NULL` | 76 | `Places` | Users → Orders | `orders.user_id` | total | `NOT NULL` | 77 | `Records` | Users → Transactions | `transactions.user_id` | total | `NOT NULL` | 78 | `Settles` | Orders → Transactions | `transactions.related_order` | partial | nullable | 79 | `Fills` | Markets → MarketTrades | `market_trades.market_id` | total | `NOT NULL` | 80 | `FillsBuy` | Orders → MarketTrades | `market_trades.buy_order_id` | partial | nullable | 81 | `FillsSell` | Orders → MarketTrades | `market_trades.sell_order_id` | partial | nullable | 82 | `Logs` | Orders → OrderEvents | `order_events.order_id` | total | `NOT NULL` | 83 | `Aggregates` | Markets → MarketCandles | `market_candles.market_id` | total | `NOT NULL` | 84 | `Owns` | Users → Watchlists | `watchlists.user_id` | total | `NOT NULL` | 85 | `Holds` | Users → Holdings | `holdings.user_id` | total | `NOT NULL` | 86 | `PositionIn` | Cryptos → Holdings | `holdings.crypto_id` | total | `NOT NULL` | 87 | `Contains` | Watchlists → WatchlistItems | `watchlist_items.watchlist_id` | total | `NOT NULL` | 88 | `Lists` | Cryptos → WatchlistItems | `watchlist_items.crypto_id` | total | `NOT NULL` | 89 90 - **Participation decides `NULL`.** Total participation of the N side means 91 every row must reference a parent, so the foreign key is `NOT NULL`. Partial 92 participation leaves it nullable. There are exactly three partial ones: 93 `Settles` (a deposit has no originating order), and `FillsBuy` / `FillsSell` 94 (a trade against the simulated market has no user order on that side). 95 Partial participation of the **1** side (for example, a user with no orders) 96 needs no column at all. It simply means no row points at that parent. 97 - **Uniqueness rules of the model become `UNIQUE` constraints.** The four 98 rules the model states in words ("a crypto quoted once per currency", "one 99 candle per market, timeframe and bucket", "one holding per user and crypto", 100 "an asset once per list") involve a relationship, so Chen notation cannot 101 draw them as keys. After transformation, the relationship is a foreign-key 102 column, and each rule becomes an ordinary composite `UNIQUE` constraint, i.e. a 103 second candidate key. 104 105 Nothing in the schema comes from anywhere else. Every column is either an ER 106 attribute or the foreign key of one listed relationship. 63 107 64 108 ### Normalisation 65 109 66 > ** Validated in P5.** [Normalization](../P5-Normalization/Normalization.md) derives this67 > exact schema independently — starting only from a single de-normalized relation of every68 > model attribute and its functional dependencies, with no reference to the ER-to-relational69 > transformation below — and shows it decomposes to **BCNF**, one normal form stronger than70 > the 3NF claimed here. The two designs agree relation for relation and key for key, so71 > nothing here changed as a result; see that page's72 > [discussion](../P5-Normalization/Normalization.md#discussion) for what the one real73 > difference is (`avg_price`, a stored derived value, not a normalisation issue) and why this 74 > design is still the one used from P5 onward. 75 76 All relations are in **3NF**:110 > **Checked in P5.** [Normalization](../P5-Normalization/Normalization.md) 111 > starts from a single de-normalized relation containing only the attributes 112 > of the ER model and the functional dependencies that follow from its rules. 113 > It decomposes that relation step by step to BCNF and arrives at these same 11 114 > relations, with one deliberate difference: `transactions.user_id` (see the last 115 > bullet below). The comparison is in the *Discussion* section at the end of that 116 > page. 117 118 All relations except `transactions` are in **BCNF**, as P5 shows. `transactions` 119 is in 2NF but not in 3NF, because of the deliberately kept `user_id` (last 120 bullet): 77 121 78 122 - Every attribute is atomic (no repeating groups, no composite fields). 79 - No partial dependency exists because every primary key is a single UUID column. 80 - 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. 123 - No partial dependency exists: every candidate key is either the single 124 column `id` or a composite key (`{user_id, crypto_id}`, …) on which no 125 non-key attribute depends only partially. 126 - No transitive dependency exists, except `transactions.user_id` (last bullet): 127 every other non-key attribute depends directly on the row's own entity, never 128 on another entity reached through a foreign key. 129 For example, `holdings.quantity` depends on `holdings.id`, and nothing about 130 the user or the crypto is copied into `holdings`. 81 131 - `avg_price` in `Holdings` is a **derived value** cached for performance (it is 82 132 the weighted-average entry price across all `buy` transactions for that … … 95 145 ("available") is the derived value here, and it is never stored, only 96 146 computed where it is needed. 147 - `transactions.user_id` is kept **deliberately**, although for an entry that 148 settles an order it repeats that order's user (`related_order → user_id`, a 149 transitive dependency). A deposit has no order (`Settles` is partial), so 150 `user_id` is the only way to record whose deposit it is. For entries with an 151 order, the only code that sets `related_order` (the buy and sell inserts in 152 `advanced_db.sql`) writes both from the same order row. No database 153 constraint enforces this. 97 154 98 155 ### Reservation and the order lifecycle … … 109 166 `SELECT … FOR UPDATE` locking that already protected `users.available_balance` 110 167 on the buy path is what makes two concurrent sell orders against the same 111 holding serialize correctly instead of racing. 168 holding serialize correctly instead of racing. The cash side of a buy order 169 (`users.reserved_balance`), `orders.filled_quantity` and `order_events` were 170 added in P7; see 171 [AdvancedDatabaseDevelopment](../P7-AdvancedDatabaseDevelopment/AdvancedDatabaseDevelopment.md). 112 172 113 173 ## DDL script 114 174 115 The script that creates the entireschema is [`../server/db/schema_creation.sql`](../../server/db/schema_creation.sql). 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.175 The script that creates the schema is [`../server/db/schema_creation.sql`](../../server/db/schema_creation.sql). 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. 116 176 117 177 The script creates: 118 - 10 tableswith check constraints, primary keys, foreign keys and unique constraints.119 - 5performance indexes.178 - 10 of the 11 tables, with check constraints, primary keys, foreign keys and unique constraints. 179 - 8 performance indexes. 120 180 - 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`). 181 182 The 11th table, `order_events`, is created by 183 [`../server/db/advanced_db.sql`](../../server/db/advanced_db.sql) together with 184 the P7 triggers that fill it. `./eduberza -init` runs both scripts in that 185 order, so a freshly initialised database always has all 11 tables and all 15 186 foreign keys. 121 187 122 188 ## DML script (sample data) … … 132 198 ## Relational diagram 133 199 134  135 136 Generated in **Pgadmin** from the **live** `project` schema, in crow's-foot 137 notation — not drawn by hand, so it is evidence that the deployed database 138 actually matches the design described above. Each box is a table with its 139 columns and declared types; key icons mark primary keys and the arrowed lines 140 are the 12 declared foreign keys. 200  201 202 Generated in **DBeaver** from the **live** `project` schema (after 203 `./eduberza -init`), not drawn by hand, so it shows what the deployed database 204 actually contains. Each box is a table with its columns; the key icon marks the 205 primary key, and the lines are the 15 declared foreign keys. The two foreign keys from 206 `market_trades` to `orders` (`buy_order_id`, `sell_order_id`) connect the same 207 two boxes, so DBeaver draws them on top of each other as one line. 208 209 The tables are arranged in the **same positions** as the entity sets in 210 `ERModel_v05.png`, so the two can be compared directly: 211 212 - every rectangle of the ER diagram is one table in the same place; 213 - every diamond of the ER diagram is one foreign-key line between the same two 214 boxes. The dot is on the referencing ("N") table, next to the foreign-key 215 column; 216 - a double (total) line in the ER diagram is a `NOT NULL` foreign key, drawn by 217 DBeaver as a solid line. The three single lines on the N side (`Settles`, 218 `FillsBuy`, `FillsSell`) are the three nullable foreign keys, which DBeaver 219 draws dashed, with a hollow diamond on the `orders` side. The table under 220 [Transformation method used](#transformation-method-used) lists all 15. 221 222 Earlier images (`relational_schema.jpg`, `relational_schema_v2.png`, 223 `relational_schema_v3.png`) were exported from pgAdmin, with a different layout 224 and from an older schema. They are kept only as history. 141 225 142 226 ### How to regenerate it 143 227 144 **With pgAdmin 4**, if DBeaver is unavailable — it reads the live schema the same 145 way, so the result is equivalent in substance: 146 147 1. Connect to the project database. 148 2. Right-click the database → **ERD For Database** (or open a blank ERD and drag 149 the `project` tables in). 150 3. Arrange the tables to mirror `ERModel_v03.png`. 151 4. **Download image** → PNG, then convert: 152 `convert relational_schema.png relational_schema.jpg` 153 228 1. Initialise the database: `./eduberza -init` (runs `schema_creation.sql` and 229 `advanced_db.sql`, so `order_events` is included). 230 2. In DBeaver, connect to the project database and expand 231 *Schemas → project → Tables*. 232 3. Select all 11 tables → right-click → **View Diagram** (or create a new ER 233 diagram and drag the tables in). 234 4. Drag each table to the position of its entity set in `ERModel_v05.png`: 235 236 ``` 237 column 1 column 2 column 3 column 4 238 row 1 watchlist_items crypto markets market_candles 239 row 2 watchlists holdings . market_trades 240 row 3 users . orders . 241 row 4 . transactions order_events . 242 ``` 243 244 Leave the empty cells (`.`) empty. They are where the relationship 245 diamonds are in the ER diagram, so the foreign-key lines will run through 246 the same gaps. 247 248 5. Right-click the canvas → **Export diagram** → PNG, saved as 249 `relational_diagram_v4.png` in this folder. -
docs/P2-RelationalDesign/RelationalDesignAIUsage.md
r0cee8ec r1549dae 12 12 ### Diagram 13 13 14 The student produces `relational_schema.jpg` in DBeaver from the live `project` schema; see [RelationalDesign](RelationalDesign.md) for instructions.14 `relational_diagram_v4.png` was exported by the student in DBeaver from the live `project` schema, with the tables in the same positions as the entity sets of `ERModel_v05.png`; see [RelationalDesign](RelationalDesign.md#how-to-regenerate-it) for instructions. 15 15 16 16 ### Results in details / description … … 100 100 `trade.go` alone to keep the reservation consistent — the same reasoning 101 101 already applied to `avg_price NOT NULL` in session 2. 102 103 ### Session 4 — 2026-09-29 (Claude Opus 5.5): correction after review 104 105 **Prompt (student, excerpt):** I pasted the reviewer's comments on P2: 106 107 > the `contains` relationship from the model is implemented differently in the database 108 > the `holds` relationship from the model is implemented differently in the database 109 > there may be other inconsistencies that are difficult to spot; the Phase 2 diagram should 110 > have been created using DBeaver with a layout visually similar to the ERD 111 112 > I suggest correcting only the Phase 1 model so that it matches Phase 2 [...] For Phase 2, 113 > simply update the image according to the requirements. 114 115 **Response (AI, summarised):** 116 117 - Explained the mismatch. In the model, `Holds` and `Contains` were M:N relationships, whose 118 table key would be the pair `{user_id, crypto_id}` / `{watchlist_id, crypto_id}`. But 119 `holdings` and `watchlist_items` have their own `id` primary key, which is how an entity 120 set is implemented. Following the reviewer's advice, P1 was corrected (v05, entity sets 121 `Holdings` and `WatchlistItems`), and the database was not changed. 122 - Found the other inconsistencies between this page and the live schema. The page was 123 missing `users.reserved_balance`, `orders.filled_quantity`, the status 124 `partially_filled`, `market_trades.buy_order_id` / `sell_order_id` and the whole 125 `order_events` table. The "10 tables", "5 indexes" and "the one nullable foreign key" 126 counts were also out of date (really 11 tables, 8 indexes in `schema_creation.sql`, and 3 127 nullable foreign keys). 128 - Rewrote [RelationalDesign](RelationalDesign.md). Each relation is labelled with its entity 129 set and each foreign key with its relationship, and the transformation is a table of all 130 15 relationships → 15 foreign keys, with `NOT NULL` following participation. 131 - Wrote export instructions with a table grid that mirrors `ERModel_v05.png`. 132 133 **What I decided:** to correct P1 instead of the database, as the reviewer suggested. I 134 exported `relational_diagram_v4.png` from DBeaver myself, with the tables arranged like the 135 ER diagram. -
docs/P2-RelationalDesign/wiki/RelationalDesign.md
r0cee8ec r1549dae 1 1 = Relational Design = 2 2 3 This page transforms [wiki:ERModel] '''v05''' into 4 relations. Every relation below corresponds to exactly one entity set of the 5 model, and every foreign key corresponds to exactly one relationship, so the 6 two diagrams can be compared box for box and line for line (see 7 Relational diagram). 8 3 9 == Descriptive representation of the relational schema == 4 10 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)`. 11 Notation: '''bold''' = primary key, ''italic'' = foreign key. After each foreign key 12 comes the ER relationship it implements. 13 14 * '''Users'''(__'''id'''__, username, email, full_name, password_hash, available_balance, invested_balance, reserved_balance, created_at, updated_at) 15 * Entity set `Users`. Candidate keys: `{id}`, `{username}`, `{email}`. `UNIQUE(username)`, `UNIQUE(email)`. 9 16 * '''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. 17 * Entity set `Cryptos`. Candidate keys: `{id}`, `{symbol}`. `UNIQUE(symbol)`. 18 * '''Markets'''(__'''id'''__, ''crypto_id'' [`QuotedOn`], quote_currency, is_active, created_at) 19 * Entity set `Markets`. Candidate keys: `{id}`, `{crypto_id, quote_currency}` (the model's rule "a crypto is quoted at most once per currency"), enforced with `UNIQUE(crypto_id, quote_currency)`. 20 * '''Holdings'''(__'''id'''__, ''user_id'' [`Holds`], ''crypto_id'' [`PositionIn`], quantity, reserved_quantity, avg_price, created_at, updated_at) 21 * Entity set `Holdings`. Candidate keys: `{id}` and `{user_id, crypto_id}` (the model's rule "one holding per user and crypto"), enforced with `UNIQUE(user_id, crypto_id)`. 18 22 * `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)`. 23 * `reserved_quantity` is `NOT NULL DEFAULT 0 CHECK (reserved_quantity >= 0 AND reserved_quantity <= quantity)` — the amount already committed to the user's own open sell orders. `quantity - reserved_quantity` (the amount actually free to sell) is not a stored column; it is computed wherever needed, in `v_portfolio` as `available_quantity` and in the sell path of [wiki:UseCase0005]. See [wiki:ERModel] (section "Holdings") for why this mirrors `available_balance`/`invested_balance` on `Users`. 24 * '''Orders'''(__'''id'''__, ''user_id'' [`Places`], ''market_id'' [`PlacedOn`], side, type, status, quantity, filled_quantity, price, placed_at, executed_at) 25 * Entity set `Orders`. `side ∈ {buy, sell}`, `type ∈ {market, limit}`, `status ∈ {open, partially_filled, executed, cancelled}`, `0 ≤ filled_quantity ≤ quantity`. 26 * '''Transactions'''(__'''id'''__, ''user_id'' [`Records`], type, amount, currency, ''related_order'' [`Settles`], created_at, description) 27 * Entity set `Transactions`. `type ∈ {deposit, buy, sell, fee}`. `related_order` is nullable (see below). 28 * '''!MarketTrades'''(__'''id'''__, ''market_id'' [`Fills`], executed_at, price, quantity, side, source, ''buy_order_id'' [`FillsBuy`], ''sell_order_id'' [`FillsSell`]) 29 * Entity set `MarketTrades`. `buy_order_id` and `sell_order_id` are both nullable (see below). 30 * '''!OrderEvents'''(__'''id'''__, ''order_id'' [`Logs`], event_type, quantity, price, status_after, created_at) 31 * Entity set `OrderEvents`. `event_type ∈ {placed, partially_filled, filled, cancelled}`. 32 * '''!MarketCandles'''(__'''id'''__, ''market_id'' [`Aggregates`], timeframe, open, high, low, close, volume, candle_time) 33 * Entity set `MarketCandles`. Candidate keys: `{id}`, `{market_id, timeframe, candle_time}` (the model's rule "one candle per market, timeframe and bucket"), enforced with `UNIQUE(market_id, timeframe, candle_time)`. 34 * '''Watchlists'''(__'''id'''__, ''user_id'' [`Owns`], name, created_at) 35 * Entity set `Watchlists`. 36 * '''!WatchlistItems'''(__'''id'''__, ''watchlist_id'' [`Contains`], ''crypto_id'' [`Lists`], added_at) 37 * Entity set `WatchlistItems`. Candidate keys: `{id}` and `{watchlist_id, crypto_id}` (the model's rule "an asset at most once per list"), enforced with `UNIQUE(watchlist_id, crypto_id)`. 37 38 38 39 === Transformation method used === 39 40 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. 41 '''Partial transformation.''' The model has 11 entity sets and 15 relationships. 42 Every relationship is binary and 1:N with no attributes of its own (the two M:N 43 relationships of earlier versions, `Holds` and `Contains`, were corrected into 44 the entity sets `Holdings` and `WatchlistItems` in v05). The rules: 45 46 * '''Each entity set becomes one relation''', with its own attributes and its own key `id` as primary key. 11 entity sets → 11 relations. 47 * '''Each 1:N relationship becomes one foreign key''' on the relation of the "N" side, pointing to the primary key of the "1" side. No relationship gets its own table, because none is M:N and none has attributes. 15 relationships → 15 foreign keys: 48 49 ||= ER relationship =||= 1 side → N side =||= Foreign key =||= Participation of the N side =||= `NULL`? =|| 50 || `QuotedOn` || Cryptos → Markets || `markets.crypto_id` || total || `NOT NULL` || 51 || `PlacedOn` || Markets → Orders || `orders.market_id` || total || `NOT NULL` || 52 || `Places` || Users → Orders || `orders.user_id` || total || `NOT NULL` || 53 || `Records` || Users → Transactions || `transactions.user_id` || total || `NOT NULL` || 54 || `Settles` || Orders → Transactions || `transactions.related_order` || partial || nullable || 55 || `Fills` || Markets → !MarketTrades || `market_trades.market_id` || total || `NOT NULL` || 56 || `FillsBuy` || Orders → !MarketTrades || `market_trades.buy_order_id` || partial || nullable || 57 || `FillsSell` || Orders → !MarketTrades || `market_trades.sell_order_id` || partial || nullable || 58 || `Logs` || Orders → !OrderEvents || `order_events.order_id` || total || `NOT NULL` || 59 || `Aggregates` || Markets → !MarketCandles || `market_candles.market_id` || total || `NOT NULL` || 60 || `Owns` || Users → Watchlists || `watchlists.user_id` || total || `NOT NULL` || 61 || `Holds` || Users → Holdings || `holdings.user_id` || total || `NOT NULL` || 62 || `PositionIn` || Cryptos → Holdings || `holdings.crypto_id` || total || `NOT NULL` || 63 || `Contains` || Watchlists → !WatchlistItems || `watchlist_items.watchlist_id` || total || `NOT NULL` || 64 || `Lists` || Cryptos → !WatchlistItems || `watchlist_items.crypto_id` || total || `NOT NULL` || 65 66 * '''Participation decides `NULL`.''' Total participation of the N side means every row must reference a parent, so the foreign key is `NOT NULL`. Partial participation leaves it nullable. There are exactly three partial ones: `Settles` (a deposit has no originating order), and `FillsBuy` / `FillsSell` (a trade against the simulated market has no user order on that side). Partial participation of the '''1''' side (for example, a user with no orders) needs no column at all. It simply means no row points at that parent. 67 * '''Uniqueness rules of the model become `UNIQUE` constraints.''' The four rules the model states in words ("a crypto quoted once per currency", "one candle per market, timeframe and bucket", "one holding per user and crypto", "an asset once per list") involve a relationship, so Chen notation cannot draw them as keys. After transformation, the relationship is a foreign-key column, and each rule becomes an ordinary composite `UNIQUE` constraint, i.e. a second candidate key. 68 69 Nothing in the schema comes from anywhere else. Every column is either an ER 70 attribute or the foreign key of one listed relationship. 61 71 62 72 === Normalisation === 63 73 64 > ''' Validated in P5.''' Normalization derives this65 > exact schema independently — starting only from a single de-normalized relation of every66 > model attribute and its functional dependencies, with no reference to the ER-to-relational67 > transformation below — and shows it decomposes to '''BCNF''', one normal form stronger than68 > the 3NF claimed here. The two designs agree relation for relation and key for key, so69 > nothing here changed as a result; see that page's70 > discussion section for what the one real71 > 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''':74 > '''Checked in P5.''' [wiki:Normalization] 75 > starts from a single de-normalized relation containing only the attributes 76 > of the ER model and the functional dependencies that follow from its rules. 77 > It decomposes that relation step by step to BCNF and arrives at these same 11 78 > relations, with one deliberate difference: `transactions.user_id` (see the last 79 > bullet below). The comparison is in the ''Discussion'' section at the end of that 80 > page. 81 82 All relations except `transactions` are in '''BCNF''', as P5 shows. `transactions` 83 is in 2NF but not in 3NF, because of the deliberately kept `user_id` (last 84 bullet): 75 85 76 86 * 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. 87 * No partial dependency exists: every candidate key is either the single column `id` or a composite key (`{user_id, crypto_id}`, …) on which no non-key attribute depends only partially. 88 * No transitive dependency exists, except `transactions.user_id` (last bullet): every other non-key attribute depends directly on the row's own entity, never on another entity reached through a foreign key. For example, `holdings.quantity` depends on `holdings.id`, and nothing about the user or the crypto is copied into `holdings`. 89 * `avg_price` in `Holdings` is a '''derived value''' cached for performance (it is the weighted-average entry price across all `buy` transactions for that `(user, crypto)` pair) — it is drawn as a derived attribute in the ER diagram. We accept the denormalisation: it is recomputed by the database inside the same transaction as each buy, in the same statement that changes the quantity (`INSERT … ON CONFLICT (user_id, crypto_id) DO UPDATE`), so the stored average and the stored quantity can never disagree. 90 * `avg_price` is declared `NOT NULL DEFAULT 0`. This matters: it is used in the P/L arithmetic of `v_portfolio`, and in SQL any arithmetic involving `NULL` yields `NULL`, so a nullable average would have silently blanked the unrealised-P/L column for an existing position instead of failing loudly. 91 * `holdings.reserved_quantity`, unlike `avg_price`, is '''not''' derived — it is written directly by the application (`trade.go`) as orders are placed and settled, the same way `quantity` itself is. `quantity - reserved_quantity` ("available") is the derived value here, and it is never stored, only computed where it is needed. 92 * `transactions.user_id` is kept '''deliberately''', although for an entry that settles an order it repeats that order's user (`related_order → user_id`, a transitive dependency). A deposit has no order (`Settles` is partial), so `user_id` is the only way to record whose deposit it is. For entries with an order, the only code that sets `related_order` (the buy and sell inserts in `advanced_db.sql`) writes both from the same order row. No database constraint enforces this. 95 93 96 94 === Reservation and the order lifecycle === … … 107 105 `SELECT … FOR UPDATE` locking that already protected `users.available_balance` 108 106 on the buy path is what makes two concurrent sell orders against the same 109 holding serialize correctly instead of racing. 107 holding serialize correctly instead of racing. The cash side of a buy order 108 (`users.reserved_balance`), `orders.filled_quantity` and `order_events` were 109 added in P7; see 110 [wiki:AdvancedDatabaseDevelopment]. 110 111 111 112 == DDL script == 112 113 113 The script that creates the entire schema is `../server/db/schema_creation.sql` (shown in fullbelow). 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 The script that creates the schema is `../server/db/schema_creation.sql` (shown 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 115 116 The script creates: 116 * 10 tableswith check constraints, primary keys, foreign keys and unique constraints.117 * 5performance indexes.117 * 10 of the 11 tables, with check constraints, primary keys, foreign keys and unique constraints. 118 * 8 performance indexes. 118 119 * 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`). 120 121 The 11th table, `order_events`, is created by 122 `../server/db/advanced_db.sql` together with 123 the P7 triggers that fill it. `./eduberza -init` runs both scripts in that 124 order, so a freshly initialised database always has all 11 tables and all 15 125 foreign keys. 119 126 120 127 === schema_creation.sql === … … 150 157 available_balance numeric(18,4) NOT NULL DEFAULT 0 CHECK (available_balance >= 0), 151 158 invested_balance numeric(18,4) NOT NULL DEFAULT 0 CHECK (invested_balance >= 0), 159 -- P7: cash committed to the user's active buy orders, moved out of 160 -- available_balance when the order is placed and consumed as it fills. 161 reserved_balance numeric(18,4) NOT NULL DEFAULT 0 CHECK (reserved_balance >= 0), 152 162 created_at timestamptz NOT NULL DEFAULT now(), 153 163 updated_at timestamptz … … 210 220 side varchar(4) NOT NULL CHECK (side IN ('buy', 'sell')), 211 221 type varchar(20) NOT NULL CHECK (type IN ('market', 'limit')), 212 status varchar(20) NOT NULL CHECK (status IN ('open', ' executed', 'cancelled')),222 status varchar(20) NOT NULL CHECK (status IN ('open', 'partially_filled', 'executed', 'cancelled')), 213 223 quantity numeric(20,4) NOT NULL CHECK (quantity > 0), 224 -- P7: how much of the order has been traded so far; remaining is 225 -- quantity - filled_quantity. Maintained from market_trades. 226 filled_quantity numeric(20,4) NOT NULL DEFAULT 0 227 CHECK (filled_quantity >= 0 AND filled_quantity <= quantity), 214 228 price numeric(18,6), 215 229 placed_at timestamptz NOT NULL DEFAULT now(), … … 249 263 quantity numeric(20,6) NOT NULL CHECK (quantity > 0), 250 264 side varchar(4) CHECK (side IN ('buy', 'sell')), 251 source varchar(50) NOT NULL DEFAULT 'simulation' 265 source varchar(50) NOT NULL DEFAULT 'simulation', 266 -- P7: the orders this trade filled. NULL on a side means the counterparty 267 -- was the simulated market (bot ticks have both NULL). 268 buy_order_id uuid REFERENCES project.orders(id), 269 sell_order_id uuid REFERENCES project.orders(id) 252 270 ); 253 271 254 272 CREATE INDEX idx_market_trades_market_time ON project.market_trades(market_id, executed_at DESC); 273 CREATE INDEX idx_market_trades_buy_order ON project.market_trades(buy_order_id) WHERE buy_order_id IS NOT NULL; 274 CREATE INDEX idx_market_trades_sell_order ON project.market_trades(sell_order_id) WHERE sell_order_id IS NOT NULL; 255 275 256 276 -- ============================================================================ … … 325 345 }}} 326 346 347 === order_events (from advanced_db.sql) === 348 349 {{{ 350 CREATE TABLE project.order_events ( 351 id bigserial PRIMARY KEY, 352 order_id uuid NOT NULL REFERENCES project.orders(id) ON DELETE CASCADE, 353 event_type varchar(20) NOT NULL 354 CHECK (event_type IN ('placed', 'partially_filled', 'filled', 'cancelled')), 355 quantity numeric(20,4) NOT NULL, 356 price numeric(18,6), 357 status_after varchar(20) NOT NULL, 358 created_at timestamptz NOT NULL DEFAULT clock_timestamp() 359 ); 360 361 CREATE INDEX idx_order_events_order ON project.order_events(order_id, id); 362 }}} 363 327 364 == DML script (sample data) == 328 365 329 The script that loads realistic sample data is `../server/db/data_load.sql` (shown in fullbelow). It is idempotent: it truncates all tables with `CASCADE` then re-inserts. Loaded:366 The script that loads realistic sample data is `../server/db/data_load.sql` (shown below). It is idempotent: it truncates all tables with `CASCADE` then re-inserts. Loaded: 330 367 * 5 crypto assets (BTC, ETH, ADA, SOL, DOGE) and 5 USD-quoted markets. 331 368 * 3 sample users (`alice`, `bob`, `charlie`) with password `test123` (sha256 hex). … … 347 384 -- 348 385 -- All sample users have the password: test123 386 -- 387 -- One transaction: the P7 checks in advanced_db.sql compare balances with 388 -- the ledger at COMMIT, and the users are inserted with their balances 389 -- before the deposit rows that back them. In an auto-commit client 390 -- (DBeaver) every statement would otherwise be checked on its own. 391 392 BEGIN; 349 393 350 394 SET search_path TO project, public; … … 443 487 -- Shows a fully-filled market buy and its resulting holding & ledger entry. 444 488 -- ============================================================================ 445 INSERT INTO project.orders (id, user_id, market_id, side, type, status, quantity, price, placed_at, executed_at) VALUES 489 -- Imported as already completely filled (filled_quantity = quantity). 490 INSERT INTO project.orders (id, user_id, market_id, side, type, status, quantity, filled_quantity, price, placed_at, executed_at) VALUES 446 491 ('c1111111-1111-1111-1111-111111111111', 447 492 'b1111111-1111-1111-1111-111111111111', 448 493 'a2222222-2222-2222-2222-222222222222', 449 'buy', 'market', 'executed', 0.5000, 3500.000000,494 'buy', 'market', 'executed', 0.5000, 0.5000, 3500.000000, 450 495 now() - interval '1 hour', now() - interval '1 hour'); 451 496 … … 457 502 INSERT INTO project.transactions (user_id, type, amount, currency, related_order, description) VALUES 458 503 ('b1111111-1111-1111-1111-111111111111', 'deposit', 10000.0000, 'USD', NULL, 504 'Initial virtual deposit'), 505 ('b2222222-2222-2222-2222-222222222222', 'deposit', 5000.0000, 'USD', NULL, 506 'Initial virtual deposit'), 507 ('b3333333-3333-3333-3333-333333333333', 'deposit', 2500.0000, 'USD', NULL, 459 508 'Initial virtual deposit'), 460 509 ('b1111111-1111-1111-1111-111111111111', 'buy', -1750.0000, 'USD', … … 482 531 ('d2222222-2222-2222-2222-222222222222', '11111111-1111-1111-1111-111111111111'), 483 532 ('d2222222-2222-2222-2222-222222222222', '55555555-5555-5555-5555-555555555555'); 533 534 COMMIT; 484 535 }}} 485 536 486 537 == Relational diagram == 487 538 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. 539 [[Image(relational_diagram_v4.png, 800px)]] 540 541 Generated in '''DBeaver''' from the '''live''' `project` schema (after 542 `./eduberza -init`), not drawn by hand, so it shows what the deployed database 543 actually contains. Each box is a table with its columns; the key icon marks the 544 primary key, and the lines are the 15 declared foreign keys. The two foreign keys from 545 `market_trades` to `orders` (`buy_order_id`, `sell_order_id`) connect the same 546 two boxes, so DBeaver draws them on top of each other as one line. 547 548 The tables are arranged in the '''same positions''' as the entity sets in 549 `ERModel_v05.png`, so the two can be compared directly: 550 551 * every rectangle of the ER diagram is one table in the same place; 552 * every diamond of the ER diagram is one foreign-key line between the same two boxes. The dot is on the referencing ("N") table, next to the foreign-key column; 553 * a double (total) line in the ER diagram is a `NOT NULL` foreign key, drawn by DBeaver as a solid line. The three single lines on the N side (`Settles`, `FillsBuy`, `FillsSell`) are the three nullable foreign keys, which DBeaver draws dashed, with a hollow diamond on the `orders` side. The table under Transformation method used lists all 15. 554 555 Earlier images (`relational_schema.jpg`, `relational_schema_v2.png`, 556 `relational_schema_v3.png`) were exported from pgAdmin, with a different layout 557 and from an older schema. They are kept only as history. 495 558 496 559 === How to regenerate it === 497 560 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` 561 1. Initialise the database: `./eduberza -init` (runs `schema_creation.sql` and `advanced_db.sql`, so `order_events` is included). 562 2. In DBeaver, connect to the project database and expand ''Schemas → project → Tables''. 563 3. Select all 11 tables → right-click → '''View Diagram''' (or create a new ER diagram and drag the tables in). 564 4. Drag each table to the position of its entity set in `ERModel_v05.png`: 565 566 {{{ 567 column 1 column 2 column 3 column 4 568 row 1 watchlist_items crypto markets market_candles 569 row 2 watchlists holdings . market_trades 570 row 3 users . orders . 571 row 4 . transactions order_events . 572 }}} 573 574 Leave the empty cells (`.`) empty. They are where the relationship 575 diamonds are in the ER diagram, so the foreign-key lines will run through 576 the same gaps. 577 578 5. Right-click the canvas → '''Export diagram''' → PNG, saved as `relational_diagram_v4.png` in this folder. -
docs/P2-RelationalDesign/wiki/RelationalDesignAIUsage.md
r0cee8ec r1549dae 12 12 === Diagram === 13 13 14 The student produces `relational_schema.jpg` in DBeaver from the live `project` schema; see [wiki:RelationalDesign]for instructions.14 `relational_diagram_v4.png` was exported by the student in DBeaver from the live `project` schema, with the tables in the same positions as the entity sets of `ERModel_v05.png`; see [wiki:RelationalDesign] (section "How to regenerate it") for instructions. 15 15 16 16 === Results in details / description === … … 96 96 `trade.go` alone to keep the reservation consistent — the same reasoning 97 97 already applied to `avg_price NOT NULL` in session 2. 98 99 === Session 4 — 2026-09-29 (Claude Opus 5.5): correction after review === 100 101 '''Prompt (student, excerpt):''' I pasted the reviewer's comments on P2: 102 103 > the `contains` relationship from the model is implemented differently in the database 104 > the `holds` relationship from the model is implemented differently in the database 105 > there may be other inconsistencies that are difficult to spot; the Phase 2 diagram should 106 > have been created using DBeaver with a layout visually similar to the ERD 107 108 > I suggest correcting only the Phase 1 model so that it matches Phase 2 [...] For Phase 2, 109 > simply update the image according to the requirements. 110 111 '''Response (AI, summarised):''' 112 113 * Explained the mismatch. In the model, `Holds` and `Contains` were M:N relationships, whose table key would be the pair `{user_id, crypto_id}` / `{watchlist_id, crypto_id}`. But `holdings` and `watchlist_items` have their own `id` primary key, which is how an entity set is implemented. Following the reviewer's advice, P1 was corrected (v05, entity sets `Holdings` and `WatchlistItems`), and the database was not changed. 114 * Found the other inconsistencies between this page and the live schema. The page was missing `users.reserved_balance`, `orders.filled_quantity`, the status `partially_filled`, `market_trades.buy_order_id` / `sell_order_id` and the whole `order_events` table. The "10 tables", "5 indexes" and "the one nullable foreign key" counts were also out of date (really 11 tables, 8 indexes in `schema_creation.sql`, and 3 nullable foreign keys). 115 * Rewrote [wiki:RelationalDesign]. Each relation is labelled with its entity set and each foreign key with its relationship, and the transformation is a table of all 15 relationships → 15 foreign keys, with `NOT NULL` following participation. 116 * Wrote export instructions with a table grid that mirrors `ERModel_v05.png`. 117 118 '''What I decided:''' to correct P1 instead of the database, as the reviewer suggested. I 119 exported `relational_diagram_v4.png` from DBeaver myself, with the tables arranged like the 120 ER diagram. -
docs/P4-Prototype/BuildInstructions.md
r0cee8ec r1549dae 13 13 | `psql` | 16 | Optional. Only for running the SQL scripts by hand. | 14 14 | Java | 21 (8+ works) | Optional. Only to open or edit the ER diagram in TerraER. | 15 | DBeaver | any recent | Optional. Only to export `relational_ schema.jpg`.|15 | DBeaver | any recent | Optional. Only to export `relational_diagram_v4.png`. | 16 16 17 17 About the PostgreSQL version: `docker-compose.yml` uses the image `postgres` without a version … … 244 244 245 245 ```sh 246 java -jar TerraER3.11.jar # then File → Open → docs/P1-ConceptualModel/ERModel_v0 3.xml247 ``` 248 249 The current version is `ERModel_v0 3.xml`. Save new versions as `ERModel_v04.xml` and so on,246 java -jar TerraER3.11.jar # then File → Open → docs/P1-ConceptualModel/ERModel_v05.xml 247 ``` 248 249 The current version is `ERModel_v05.xml`. Save new versions as `ERModel_v06.xml` and so on, 250 250 and export a matching PNG for each. TerraER does not add the extension itself: type `.xml` 251 251 yourself, or the file will not reopen. -
docs/P4-Prototype/wiki/BuildInstructions.md
r0cee8ec r1549dae 12 12 || `psql` || 16 || Optional. Only for running the SQL scripts by hand. || 13 13 || Java || 21 (8+ works) || Optional. Only to open or edit the ER diagram in TerraER. || 14 || DBeaver || any recent || Optional. Only to export `relational_ schema.jpg`. ||14 || DBeaver || any recent || Optional. Only to export `relational_diagram_v4.png`. || 15 15 16 16 About the PostgreSQL version: `docker-compose.yml` uses the image `postgres` without a version … … 270 270 271 271 {{{ 272 java -jar TerraER3.11.jar # then File → Open → docs/P1-ConceptualModel/ERModel_v0 3.xml273 }}} 274 275 The current version is `ERModel_v0 3.xml`. Save new versions as `ERModel_v04.xml` and so on,272 java -jar TerraER3.11.jar # then File → Open → docs/P1-ConceptualModel/ERModel_v05.xml 273 }}} 274 275 The current version is `ERModel_v05.xml`. Save new versions as `ERModel_v06.xml` and so on, 276 276 and export a matching PNG for each. TerraER does not add the extension itself: type `.xml` 277 277 yourself, or the file will not reopen. -
docs/P5-Normalization/Normalization.md
r0cee8ec r1549dae 1 1 # Normalization 2 2 3 This phase deliberately ignores the design from [ERModel](../P1-ConceptualModel/ERModel.md) 4 (P1) and [RelationalDesign](../P2-RelationalDesign/RelationalDesign.md) (P2) as a starting 5 point. Instead it starts over from a single flat relation containing every attribute of the 6 model, derives the functional dependencies that hold on it, and decomposes it formally, 7 step by step. The 8 [final section](#final-result-and-discussion) compares what falls out of that process with 9 the P2 design. 3 This phase does not use the relations of 4 [RelationalDesign](../P2-RelationalDesign/RelationalDesign.md) (P2) as a starting point. 5 It starts from the attributes of [ERModel](../P1-ConceptualModel/ERModel.md) **v05** (P1), 6 put into one de-normalized relation. It states the functional dependencies that the 7 model's rules impose on those attributes, **computes** the keys of that relation from the 8 dependencies, and then decomposes it step by step through 2NF, 3NF and BCNF. Every step is 9 checked for a lossless join and for dependency preservation. The 10 [final section](#final-result-and-discussion) compares the result with P2. 10 11 11 12 ## De-normalized database form 12 13 13 ### Building one relation out of the whole model 14 15 The ER model has ten entity/relationship sets carrying attributes (see 16 [ERModel](../P1-ConceptualModel/ERModel.md)): `Users`, `Cryptos`, `Markets`, `Orders`, 17 `Transactions`, `MarketTrades`, `MarketCandles`, `Watchlists`, and the two attributed 18 relationships `Holds` and `Contains`. Eight more relationships (`QuotedOn`, `PlacedOn`, 19 `Places`, `Records`, `Settles`, `Fills`, `Aggregates`, `Owns`) carry no attributes of their 20 own — in Chen notation they need none, because the diagram expresses the link itself as a 21 relationship, not a column. A single flat relation has no such device: the only way to keep 22 one entity's rows pointed at another's is a plain attribute holding the referenced key, 23 which is exactly what P2's ER-to-relational transformation already introduces for each of 24 those eight relationships (`markets.crypto_id`, `orders.market_id`, `orders.user_id`, 25 `transactions.user_id`, `transactions.related_order`, `market_trades.market_id`, 26 `market_candles.market_id`, `watchlists.user_id`). Those linking attributes are included 27 below for that reason — not because they were copied from P2's design, but because a "single 28 table with everything in it" cannot represent the model at all without them. 29 30 Every attribute name is prefixed by a two-or-three-letter code for the entity/relationship it 31 came from, because several names repeat across the model (`id`, `created_at`, `quantity`, 32 `type`, `name`, `price`, `side` all appear more than once) and the de-normalized relation may 33 not contain duplicate names. 34 35 | Prefix | Origin (P1 entity / relationship) | Attributes | 14 ### Which attributes go into the relation 15 16 The relation contains **the attributes of the ER model and nothing else**. In v05 all 17 attributes belong to the 11 entity sets. None of the 15 relationships has attributes of its 18 own. 19 20 A relationship adds **no column**. Foreign-key columns such as `crypto_id` or `watchlist_id` 21 belong to the relational model of P2, not to the ER model, so they do not appear here. What a 22 relationship contributes is a **functional dependency** between attributes that are already 23 in the relation. For example, `Contains` (Watchlists 1 : N WatchlistItems) says that every 24 watchlist item is on exactly one watchlist, which is the dependency `WI_ID → W_ID` in the 25 next section. It is not a column `WI_WATCHLIST_ID`. 26 27 Attribute names are prefixed with the entity set they come from, because several names repeat 28 across the model (`id`, `created_at`, `quantity`, `type`, `name`, `price`, `side`), and one 29 relation cannot contain the same name twice. 30 31 | Prefix | Entity set (P1) | Attributes | 36 32 |---|---|---| 37 | `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` | 38 | `C_` | Cryptos | `C_ID, C_SYMBOL, C_NAME, C_CREATED_AT` | 39 | `M_` | Markets (+ `QuotedOn`) | `M_ID, M_CRYPTO_ID, M_QUOTE_CURRENCY, M_IS_ACTIVE, M_CREATED_AT` | 40 | `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` | 41 | `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` | 42 | `T_` | Transactions (+ `Records`, `Settles`) | `T_ID, T_USER_ID, T_TYPE, T_AMOUNT, T_CURRENCY, T_RELATED_ORDER, T_CREATED_AT, T_DESCRIPTION` | 43 | `MT_` | MarketTrades (+ `Fills`) | `MT_ID, MT_MARKET_ID, MT_EXECUTED_AT, MT_PRICE, MT_QUANTITY, MT_SIDE, MT_SOURCE` | 44 | `MC_` | MarketCandles (+ `Aggregates`) | `MC_ID, MC_MARKET_ID, MC_TIMEFRAME, MC_OPEN, MC_HIGH, MC_LOW, MC_CLOSE, MC_VOLUME, MC_CANDLE_TIME` | 45 | `W_` | Watchlists (+ `Owns`) | `W_ID, W_USER_ID, W_NAME, W_CREATED_AT` | 46 | `WI_` | `Contains` (+ surrogate key) | `WI_ID, WI_WATCHLIST_ID, WI_CRYPTO_ID, WI_ADDED_AT` | 47 48 `H_ID` and `WI_ID` exist for the same reason they exist in P2: `Holds` and `Contains` are M:N 49 relationships with their own attributes, and giving each its own surrogate key (rather than 50 relying solely on the `{user,crypto}` / `{watchlist,crypto}` pair) is the same design choice 51 already justified in [RelationalDesign](../P2-RelationalDesign/RelationalDesign.md#descriptive-representation-of-the-relational-schema). 52 53 This gives **one relation, `R_EDUBERZA`, of 68 attributes:** 33 | `U_` | Users | `U_ID, U_USERNAME, U_EMAIL, U_FULL_NAME, U_PASSWORD_HASH, U_AVAILABLE_BALANCE, U_INVESTED_BALANCE, U_RESERVED_BALANCE, U_CREATED_AT, U_UPDATED_AT` | 34 | `C_` | Cryptos | `C_ID, C_SYMBOL, C_NAME, C_CREATED_AT` | 35 | `M_` | Markets | `M_ID, M_QUOTE_CURRENCY, M_IS_ACTIVE, M_CREATED_AT` | 36 | `H_` | Holdings | `H_ID, H_QUANTITY, H_RESERVED_QUANTITY, H_AVG_PRICE, H_CREATED_AT, H_UPDATED_AT` | 37 | `O_` | Orders | `O_ID, O_SIDE, O_TYPE, O_STATUS, O_QUANTITY, O_FILLED_QUANTITY, O_PRICE, O_PLACED_AT, O_EXECUTED_AT` | 38 | `T_` | Transactions | `T_ID, T_TYPE, T_AMOUNT, T_CURRENCY, T_CREATED_AT, T_DESCRIPTION` | 39 | `MT_` | MarketTrades | `MT_ID, MT_EXECUTED_AT, MT_PRICE, MT_QUANTITY, MT_SIDE, MT_SOURCE` | 40 | `OE_` | OrderEvents | `OE_ID, OE_EVENT_TYPE, OE_QUANTITY, OE_PRICE, OE_STATUS_AFTER, OE_CREATED_AT` | 41 | `MC_` | MarketCandles | `MC_ID, MC_TIMEFRAME, MC_OPEN, MC_HIGH, MC_LOW, MC_CLOSE, MC_VOLUME, MC_CANDLE_TIME` | 42 | `W_` | Watchlists | `W_ID, W_NAME, W_CREATED_AT` | 43 | `WI_` | WatchlistItems | `WI_ID, WI_ADDED_AT` | 44 45 That is 64 attributes. **One case needs two more.** `FillsBuy` and `FillsSell` are two 46 different relationships between the same two entity sets, Orders and MarketTrades. A trade 47 can fill one buy order *and* one sell order, which are two different orders. One relation 48 has only one `O_ID` column, and one column cannot hold two different orders in the same 49 tuple. So the order's identifier appears once per **role**, named after the relationship 50 that gives the role: 51 52 | Attribute | Meaning | 53 |---|---| 54 | `O_ID_FILLSBUY` | the `id` of Orders, in its role in `FillsBuy` (the buy order a trade filled) | 55 | `O_ID_FILLSSELL` | the `id` of Orders, in its role in `FillsSell` (the sell order a trade filled) | 56 57 These are not foreign keys copied from P2. They are the ER attribute `Orders.id` itself, once 58 for each of the two relationships. [ERModel](../P1-ConceptualModel/ERModel.md) names these two 59 roles of `Orders` explicitly: *the buy order* of a trade in `FillsBuy`, and *the sell order* in 60 `FillsSell`. This is the only place where the model has two 61 relationships between the same pair of entity sets. Every other relationship is expressed with 62 the attributes above, without renaming. 63 64 This gives **one relation, `R_EDUBERZA`, of 66 attributes:** 54 65 55 66 ``` 56 67 R_EDUBERZA( 57 68 U_ID, U_USERNAME, U_EMAIL, U_FULL_NAME, U_PASSWORD_HASH, U_AVAILABLE_BALANCE, 58 U_INVESTED_BALANCE, U_ CREATED_AT, U_UPDATED_AT,69 U_INVESTED_BALANCE, U_RESERVED_BALANCE, U_CREATED_AT, U_UPDATED_AT, 59 70 C_ID, C_SYMBOL, C_NAME, C_CREATED_AT, 60 M_ID, M_CRYPTO_ID, M_QUOTE_CURRENCY, M_IS_ACTIVE, M_CREATED_AT, 61 H_ID, H_USER_ID, H_CRYPTO_ID, H_QUANTITY, H_RESERVED_QUANTITY, H_AVG_PRICE, 62 H_CREATED_AT, H_UPDATED_AT, 63 O_ID, O_USER_ID, O_MARKET_ID, O_SIDE, O_TYPE, O_STATUS, O_QUANTITY, O_PRICE, 71 M_ID, M_QUOTE_CURRENCY, M_IS_ACTIVE, M_CREATED_AT, 72 H_ID, H_QUANTITY, H_RESERVED_QUANTITY, H_AVG_PRICE, H_CREATED_AT, H_UPDATED_AT, 73 O_ID, O_SIDE, O_TYPE, O_STATUS, O_QUANTITY, O_FILLED_QUANTITY, O_PRICE, 64 74 O_PLACED_AT, O_EXECUTED_AT, 65 T_ID, T_ USER_ID, T_TYPE, T_AMOUNT, T_CURRENCY, T_RELATED_ORDER, T_CREATED_AT,66 T_DESCRIPTION,67 MT_ID, MT_MARKET_ID, MT_EXECUTED_AT, MT_PRICE, MT_QUANTITY, MT_SIDE, MT_SOURCE,68 MC_ID, MC_MARKET_ID, MC_TIMEFRAME, MC_OPEN, MC_HIGH, MC_LOW, MC_CLOSE, MC_VOLUME,69 MC_ CANDLE_TIME,70 W_ID, W_ USER_ID, W_NAME, W_CREATED_AT,71 WI_ID, WI_ WATCHLIST_ID, WI_CRYPTO_ID, WI_ADDED_AT75 T_ID, T_TYPE, T_AMOUNT, T_CURRENCY, T_CREATED_AT, T_DESCRIPTION, 76 MT_ID, MT_EXECUTED_AT, MT_PRICE, MT_QUANTITY, MT_SIDE, MT_SOURCE, 77 O_ID_FILLSBUY, O_ID_FILLSSELL, 78 OE_ID, OE_EVENT_TYPE, OE_QUANTITY, OE_PRICE, OE_STATUS_AFTER, OE_CREATED_AT, 79 MC_ID, MC_TIMEFRAME, MC_OPEN, MC_HIGH, MC_LOW, MC_CLOSE, MC_VOLUME, MC_CANDLE_TIME, 80 W_ID, W_NAME, W_CREATED_AT, 81 WI_ID, WI_ADDED_AT 72 82 ) 73 83 ``` 74 84 85 To keep the tables below readable, **`X_*`** means the non-identifier attributes of prefix 86 `X_`. For example, `U_*` = `U_USERNAME … U_UPDATED_AT` (9 attributes), and `O_*` = 87 `O_SIDE … O_EXECUTED_AT` (8 attributes). `U_ID`, `O_ID`, … are always written out. 88 75 89 Every attribute is single-valued and atomic (a balance, a timestamp, a symbol, an amount — 76 90 nothing here is a list or a nested record), so `R_EDUBERZA` satisfies 1NF as soon as it is 77 written down. Whether it satisfies anything beyond that is exactly what the rest of this page 91 written down. 92 93 ## Functional dependencies 94 95 At this point `R_EDUBERZA` is just a set of attributes. It has **no keys yet**. `U_ID`, 96 `O_ID`, … are ordinary attributes of this relation, and which attribute sets are keys of 97 `R_EDUBERZA` is computed in the [next section](#candidate-keys-and-primary-key), from the 98 dependencies below. Each dependency is justified by a rule of the domain, as described in 99 the data requirements of [ERModel](../P1-ConceptualModel/ERModel.md). The rules are of four 100 kinds: 101 102 - **(I) Identification.** Every value of an identifier (`U_ID`, `C_ID`, …) is given to 103 exactly one real object: one user, one crypto, one order. That object has exactly one 104 username, one balance, one price, and so on. So the identifier's value fixes those values. 105 - **(R) 1:N relationship.** In a 1:N relationship, each object on the N side is linked to 106 exactly one object on the 1 side. So the N side's identifier fixes the 1 side's 107 identifier. Example: an order is placed by exactly one user (`Places`), so `O_ID → U_ID`. 108 The opposite direction does not hold: a user places many orders, so `U_ID ↛ O_ID`. 109 - **(U) Uniqueness rule.** A rule of the form "at most one X per Y and Z" gives 110 `Y, Z → X`. 111 - **(N) Unique natural attribute.** No two users share a username or an email, and no two 112 cryptos share a symbol. 113 114 **Only rules of the ER model are used.** The dependencies below come from the rules stated in 115 [ERModel](../P1-ConceptualModel/ERModel.md) v05 and nothing else. The analysis uses the 116 classical definitions (Armstrong's axioms), with no special treatment of `NULL`. Partial 117 relationships (`Settles`, `FillsBuy`, `FillsSell`) are discussed where they matter: 118 under [Canonical cover](#canonical-cover) and in the [discussion](#discussion). 119 120 | # | Functional dependency | Rule | Why it holds | 121 |---|---|---|---| 122 | FD1 | `U_ID → U_USERNAME, U_EMAIL, U_FULL_NAME, U_PASSWORD_HASH, U_AVAILABLE_BALANCE, U_INVESTED_BALANCE, U_RESERVED_BALANCE, U_CREATED_AT, U_UPDATED_AT` | I | one user, one value of each | 123 | FD2 | `U_USERNAME → U_ID` | N | usernames are unique | 124 | FD3 | `U_EMAIL → U_ID` | N | emails are unique | 125 | FD4 | `C_ID → C_SYMBOL, C_NAME, C_CREATED_AT` | I | one crypto, one value of each | 126 | FD5 | `C_SYMBOL → C_ID` | N | symbols are unique | 127 | FD6 | `M_ID → M_QUOTE_CURRENCY, M_IS_ACTIVE, M_CREATED_AT, C_ID` | I, R | …and a market is `QuotedOn` exactly one crypto | 128 | FD7 | `C_ID, M_QUOTE_CURRENCY → M_ID` | U | a crypto is quoted at most once per currency | 129 | FD8 | `H_ID → H_QUANTITY, H_RESERVED_QUANTITY, H_AVG_PRICE, H_CREATED_AT, H_UPDATED_AT, U_ID, C_ID` | I, R | …and a holding belongs to one user (`Holds`) and is a position in one crypto (`PositionIn`) | 130 | FD9 | `U_ID, C_ID → H_ID` | U | at most one holding per user and crypto | 131 | FD10 | `O_ID → O_SIDE, O_TYPE, O_STATUS, O_QUANTITY, O_FILLED_QUANTITY, O_PRICE, O_PLACED_AT, O_EXECUTED_AT, U_ID, M_ID` | I, R | …and an order is placed by one user (`Places`) on one market (`PlacedOn`) | 132 | FD11 | `T_ID → T_TYPE, T_AMOUNT, T_CURRENCY, T_CREATED_AT, T_DESCRIPTION, U_ID, O_ID` | I, R | …and a ledger entry belongs to one user (`Records`) and to at most one order (`Settles`) | 133 | FD12 | `MT_ID → MT_EXECUTED_AT, MT_PRICE, MT_QUANTITY, MT_SIDE, MT_SOURCE, M_ID, O_ID_FILLSBUY, O_ID_FILLSSELL` | I, R | …and a trade happened on one market (`Fills`) and filled at most one buy order (`FillsBuy`) and at most one sell order (`FillsSell`) | 134 | FD13 | `OE_ID → OE_EVENT_TYPE, OE_QUANTITY, OE_PRICE, OE_STATUS_AFTER, OE_CREATED_AT, O_ID` | I, R | …and an event belongs to one order (`Logs`) | 135 | FD14 | `MC_ID → MC_TIMEFRAME, MC_OPEN, MC_HIGH, MC_LOW, MC_CLOSE, MC_VOLUME, MC_CANDLE_TIME, M_ID` | I, R | …and a candle summarises one market (`Aggregates`) | 136 | FD15 | `M_ID, MC_TIMEFRAME, MC_CANDLE_TIME → MC_ID` | U | one candle per market, timeframe and bucket | 137 | FD16 | `W_ID → W_NAME, W_CREATED_AT, U_ID` | I, R | …and a watchlist is owned by one user (`Owns`) | 138 | FD17 | `WI_ID → WI_ADDED_AT, W_ID, C_ID` | I, R | …and an item is on one watchlist (`Contains`) and names one crypto (`Lists`) | 139 | FD18 | `W_ID, C_ID → WI_ID` | U | an asset appears at most once per watchlist | 140 141 **Dependencies that do *not* hold** are as important, because they are why some attributes 142 must be combined in the key later: 143 144 - The reverse of every (R) dependency, e.g. `U_ID ↛ O_ID`, `M_ID ↛ MT_ID`, `W_ID ↛ WI_ID`. 145 These are 1:N, not 1:1. 146 - `M_ID, MT_EXECUTED_AT ↛ MT_ID`. Two trades on a market can share a timestamp. 147 - `U_ID, W_NAME ↛ W_ID`. The model does not require list names to be unique per user. 148 - `O_ID_FILLSBUY` and `O_ID_FILLSSELL` determine no other attribute of `R_EDUBERZA` **by any 149 rule of the ER model**. The order data (`O_SIDE`, `O_PRICE`, …) describes the order in the 150 `O_ID` column, not the order in a role column. (The database also has a rule that a trade 151 and the orders it fills are on the same market. That rule is a trigger in P7 relating 152 several entity sets, not a rule of the ER model, so it is not used here.) 153 154 ### Canonical cover 155 156 A canonical (minimal) cover is obtained in three steps. 157 158 **Step 1 — single attribute on the right.** Each FD above is read as one dependency per 159 right-side attribute, e.g. FD6 is `M_ID → M_QUOTE_CURRENCY`, `M_ID → M_IS_ACTIVE`, 160 `M_ID → M_CREATED_AT`, `M_ID → C_ID`. 161 162 **Step 2 — no extraneous attribute on the left.** Only FD7, FD9, FD15 and FD18 have more than one 163 attribute on the left. For each one, dropping any attribute makes the rule false: 164 165 | FD | Drop | Counter-example (the smaller left side does not determine the right side) | 166 |---|---|---| 167 | FD7 | `M_QUOTE_CURRENCY` | BTC is quoted in USD *and* in EUR: one `C_ID`, two markets | 168 | | `C_ID` | USD is the quote currency of many markets | 169 | FD9 | `C_ID` | one user holds several cryptos | 170 | | `U_ID` | one crypto is held by several users | 171 | FD15 | `M_ID` | every market has a `1h` candle starting at 10:00 | 172 | | `MC_TIMEFRAME` | a market has a `1m` and a `1h` candle both starting at 10:00 | 173 | | `MC_CANDLE_TIME` | a market has many `1h` candles | 174 | FD18 | `C_ID` | a watchlist has several items | 175 | | `W_ID` | a crypto is on several watchlists | 176 177 **Step 3 — no redundant dependency.** A dependency is redundant if it follows from the others. For 178 almost every dependency, its right-side attribute appears on the right of no other 179 dependency with a different left side (e.g. nothing but `U_ID` determines 180 `U_AVAILABLE_BALANCE`), so it cannot be derived. The candidates worth checking are the 181 identifiers that are reached from several places: 182 183 - **`T_ID → U_ID` is redundant.** It follows by transitivity from `T_ID → O_ID` (FD11) and 184 `O_ID → U_ID` (FD10): a ledger entry's user is the user of the order it settles. It is 185 therefore **removed** from FD11. The derivation is valid only for an entry that has an 186 order. Every tuple of `R_EDUBERZA` does have one (see the 187 [discussion](#discussion)), so in the de-normalized relation the removal is correct. The 188 consequence for deposits, which have no order, is taken up in the discussion. 189 - `H_ID → U_ID`, `H_ID → C_ID`, `WI_ID → W_ID`, `WI_ID → C_ID`, `M_ID → C_ID`, `MT_ID → M_ID`, 190 `MC_ID → M_ID`, `OE_ID → O_ID`, `W_ID → U_ID` and `O_ID → U_ID`, `O_ID → M_ID`: for 191 each, no other dependency with a different left side has that attribute on its right 192 side and a left side reachable from this one, so none can be derived. 193 - The four (U) and three (N) dependencies go "backwards" from a non-identifier to an 194 identifier. Nothing else produces an identifier from those attributes, so they are not 195 derivable either. 196 197 Grouping the single-attribute dependencies back by left side gives FD1–FD18 as listed, 198 except that FD11 loses `U_ID`: 199 200 | # | Functional dependency (canonical cover) | 201 |---|---| 202 | FD11 | `T_ID → T_TYPE, T_AMOUNT, T_CURRENCY, T_CREATED_AT, T_DESCRIPTION, O_ID` | 203 204 **FD1–FD18, with this FD11, is the canonical cover.** From here on, "FD11" means this reduced 205 form. 206 207 ## Candidate keys and primary key 208 209 A candidate key is a minimal set of attributes whose closure under FD1–FD18 is all 66 210 attributes. 211 212 **Attributes that must be in every key.** `T_ID`, `OE_ID` and `MT_ID` appear on the right side 213 of no dependency. Nothing determines them, so every key must contain them. 214 215 **Closure of `{T_ID, OE_ID, MT_ID}`:** 216 217 | Step | Added | Using | 218 |---|---|---| 219 | start | `T_ID, OE_ID, MT_ID` | — | 220 | 1 | `T_*`, `O_ID` | FD11 | 221 | 2 | `OE_*` | FD13 | 222 | 3 | `MT_*`, `M_ID`, `O_ID_FILLSBUY`, `O_ID_FILLSSELL` | FD12 | 223 | 4 | `O_*`, `U_ID` | FD10 | 224 | 5 | `U_*` | FD1 | 225 | 6 | `M_*`, `C_ID` | FD6 | 226 | 7 | `C_*` | FD4 | 227 | 8 | `H_ID` | FD9 (`U_ID` and `C_ID` are both present) | 228 | 9 | `H_*` | FD8 | 229 230 That is 53 attributes. Still missing are all 8 `MC_` attributes, the 3 `W_` attributes and 231 the 2 `WI_` attributes: 232 233 - **`MC_`:** only `MC_ID` determines them (FD14), and `MC_ID` is reached only by FD15, which 234 needs `M_ID` (already present), `MC_TIMEFRAME` and `MC_CANDLE_TIME`. So the key must add 235 either `MC_ID` or both `MC_TIMEFRAME` and `MC_CANDLE_TIME`. Neither of those two alone is 236 enough. 237 - **`W_` and `WI_`:** `WI_ID` gives `W_ID` (FD17), and `W_ID` gives `WI_ID` together with 238 `C_ID`, which is already present (FD18). So adding either `WI_ID` or `W_ID` gives all five. 239 240 **Candidate keys** (each one's closure is all 66 attributes, and removing any member breaks 241 that, by the argument above): 242 243 | Key | Attributes | 244 |---|---| 245 | **K1** | `T_ID, OE_ID, MT_ID, MC_ID, WI_ID` | 246 | K2 | `T_ID, OE_ID, MT_ID, MC_ID, W_ID` | 247 | K3 | `T_ID, OE_ID, MT_ID, MC_TIMEFRAME, MC_CANDLE_TIME, WI_ID` | 248 | K4 | `T_ID, OE_ID, MT_ID, MC_TIMEFRAME, MC_CANDLE_TIME, W_ID` | 249 250 **Primary key: K1.** It consists only of identifiers, and it is the key that remains at the 251 end of the decomposition below. 252 253 **Prime attributes** (in at least one candidate key): `T_ID, OE_ID, MT_ID, MC_ID, 254 MC_TIMEFRAME, MC_CANDLE_TIME, W_ID, WI_ID`. The other 58 attributes are **non-prime**. The 255 difference matters: 2NF and 3NF only restrict dependencies of non-prime attributes, and BCNF 256 restricts all of them. 257 258 In words, a tuple of `R_EDUBERZA` puts together one ledger entry, one order event, one 259 trade, one candle and one watchlist item. Everything else in the tuple (the user, the order, 260 the market, the crypto, the holding, the watchlist) follows from those five. 261 262 **Normal form of `R_EDUBERZA`:** 1NF only. It is not in 2NF, because, for example, `T_AMOUNT` 263 depends on `T_ID` alone, a proper part of K1. 264 265 ## 1NF decomposition 266 267 No decomposition is needed. Every attribute of `R_EDUBERZA` is atomic and single-valued, and 268 the relation has no repeating groups (see 269 [De-normalized database form](#de-normalized-database-form)). 270 271 ## 2NF decomposition 272 273 ### How every step is described and checked 274 275 Each step of 2NF, 3NF and BCNF below lists, in this order: the relation analyzed, its 276 dependencies, its candidate keys and primary key, and its normal form; the dependency that 277 violates the next normal form and is used for the split; the two resulting relations, each 278 with its dependencies, keys and normal form; and the dependency-preservation and lossless-join 78 279 checks. 79 280 80 ## Functional dependencies 81 82 ### Canonical cover 83 84 Read directly off the model: each entity's/relationship's own key determines its own 85 attributes, nothing more. This is already minimal — no functional dependency below has an 86 extraneous attribute on its left side, and no dependent attribute is repeated on the right 87 side of more than one dependency, which is what "canonical cover" requires. 88 89 | # | Functional dependency | Source | 281 Every step splits one relation `R` into two: the **extracted** relation `Ri` and the 282 **residual** relation `R'` (what is left of `R`). The same two checks are made each time: 283 284 - **Lossless join.** The split of `R` into `Ri` and `R'` is lossless if the common attributes 285 determine one of the two sides: `(Ri ∩ R') → Ri` or `(Ri ∩ R') → R'`. Every step below 286 extracts `Ri = X ∪ (what X determines)` for some determinant `X` that stays in `R'`. So 287 `X ⊆ Ri ∩ R'` and `X → Ri`, and the first condition holds. 288 - **Dependency preservation.** Every dependency of the canonical cover must end up with all 289 its attributes inside one relation. So an attribute is removed from the residual only when 290 no dependency still waiting in the residual needs it. Otherwise it is extracted **and** 291 kept. 292 293 **Relation analyzed first:** `R_EDUBERZA` (66 attributes), dependencies FD1–FD18, candidate 294 keys K1–K4, primary key K1. **Normal form:** 1NF. 295 296 **Dependencies that violate 2NF.** 2NF forbids a non-prime attribute from depending on a proper 297 part of a candidate key. There are six such partial dependencies: 298 299 | Part of a key | Non-prime attributes that depend on it | Through | 90 300 |---|---|---| 91 | 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 | 92 | FD2 | `U_USERNAME → U_ID` | Users (`UNIQUE(username)`) | 93 | FD3 | `U_EMAIL → U_ID` | Users (`UNIQUE(email)`) | 94 | FD4 | `C_ID → C_SYMBOL, C_NAME, C_CREATED_AT` | Cryptos | 95 | FD5 | `C_SYMBOL → C_ID` | Cryptos (`UNIQUE(symbol)`) | 96 | FD6 | `M_ID → M_CRYPTO_ID, M_QUOTE_CURRENCY, M_IS_ACTIVE, M_CREATED_AT` | Markets | 97 | FD7 | `M_CRYPTO_ID, M_QUOTE_CURRENCY → M_ID` | Markets (`UNIQUE(crypto_id, quote_currency)`) | 98 | FD8 | `H_ID → H_USER_ID, H_CRYPTO_ID, H_QUANTITY, H_RESERVED_QUANTITY, H_AVG_PRICE, H_CREATED_AT, H_UPDATED_AT` | Holds | 99 | FD9 | `H_USER_ID, H_CRYPTO_ID → H_ID` | Holds (`UNIQUE(user_id, crypto_id)`) | 100 | 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 | 101 | FD11 | `T_ID → T_USER_ID, T_TYPE, T_AMOUNT, T_CURRENCY, T_RELATED_ORDER, T_CREATED_AT, T_DESCRIPTION` | Transactions | 102 | FD12 | `MT_ID → MT_MARKET_ID, MT_EXECUTED_AT, MT_PRICE, MT_QUANTITY, MT_SIDE, MT_SOURCE` | MarketTrades | 103 | FD13 | `MC_ID → MC_MARKET_ID, MC_TIMEFRAME, MC_OPEN, MC_HIGH, MC_LOW, MC_CLOSE, MC_VOLUME, MC_CANDLE_TIME` | MarketCandles | 104 | FD14 | `MC_MARKET_ID, MC_TIMEFRAME, MC_CANDLE_TIME → MC_ID` | MarketCandles (`UNIQUE(market_id, timeframe, candle_time)`) | 105 | FD15 | `W_ID → W_USER_ID, W_NAME, W_CREATED_AT` | Watchlists | 106 | FD16 | `WI_ID → WI_WATCHLIST_ID, WI_CRYPTO_ID, WI_ADDED_AT` | Contains | 107 | FD17 | `WI_WATCHLIST_ID, WI_CRYPTO_ID → WI_ID` | Contains (`UNIQUE(watchlist_id, crypto_id)`) | 108 109 **Minimality, checked by example (Markets):** could FD7 drop an attribute from its left side? 110 `M_CRYPTO_ID` alone does not determine `M_ID` — many markets can reference the same crypto in 111 different quote currencies (that is the entire point of the market entity), so two rows can 112 share `M_CRYPTO_ID` and disagree on `M_ID`. `M_QUOTE_CURRENCY` alone fails the same way in the 113 other direction. Neither attribute is extraneous, so the left side of FD7 cannot shrink. The 114 same check applies to FD9, FD14 and FD17, whose composite left sides come directly from the 115 `UNIQUE` constraints already justified per-relation in 116 [RelationalDesign](../P2-RelationalDesign/RelationalDesign.md); none of those constraints 117 holds on a proper subset of its columns either. 118 119 **No redundant dependency:** each of FD1–FD17 has a right side that is not implied by any 120 other dependency in the set — for instance, nothing outside FD1 mentions `U_AVAILABLE_BALANCE`, 121 so FD1 cannot be derived from the rest and cannot be dropped. This set is the canonical cover. 122 123 ### Dependencies carried by foreign keys 124 125 Six attributes above are foreign keys: `M_CRYPTO_ID`, `H_USER_ID`, `H_CRYPTO_ID`, 126 `O_USER_ID`, `O_MARKET_ID`, `T_USER_ID`, `T_RELATED_ORDER`, `MT_MARKET_ID`, `MC_MARKET_ID`, 127 `W_USER_ID`, `WI_WATCHLIST_ID`, `WI_CRYPTO_ID` — each one draws its values from the same 128 domain as some other attribute's key. Because of that, every dependency that holds on the 129 referenced key also holds, by substitution, on the referencing attribute: 130 131 | Foreign key | References | Therefore also determines | 132 |---|---|---| 133 | `M_CRYPTO_ID` | `C_ID` | `C_SYMBOL, C_NAME, C_CREATED_AT` | 134 | `H_USER_ID` | `U_ID` | all of `U_*` | 135 | `H_CRYPTO_ID` | `C_ID` | all of `C_*` | 136 | `O_USER_ID` | `U_ID` | all of `U_*` | 137 | `O_MARKET_ID` | `M_ID` | all of `M_*`, and transitively all of `C_*` | 138 | `T_USER_ID` | `U_ID` | all of `U_*` | 139 | `T_RELATED_ORDER` | `O_ID` | all of `O_*`, and transitively `U_*`, `M_*`, `C_*` (when not null) | 140 | `MT_MARKET_ID` | `M_ID` | all of `M_*`, transitively `C_*` | 141 | `MC_MARKET_ID` | `M_ID` | all of `M_*`, transitively `C_*` | 142 | `W_USER_ID` | `U_ID` | all of `U_*` | 143 | `WI_WATCHLIST_ID` | `W_ID` | all of `W_*`, transitively `U_*` | 144 | `WI_CRYPTO_ID` | `C_ID` | all of `C_*` | 145 146 None of these is added to the canonical cover — each is *derivable* from FD1–FD17 by 147 transitivity plus the foreign-key identity, which is exactly why a canonical cover excludes 148 them. They matter anyway: they are precisely the transitive dependencies the 3NF check below 149 has to rule out. 150 151 ## Candidate keys and primary key 152 153 `Orders`, `Transactions`, `MarketTrades`, `MarketCandles`, `Holds`, `Watchlists` and 154 `Contains` are, with respect to each other, independent record types: nothing about an 155 order's id says anything about which market-candle row, or which unrelated transaction, or 156 which watchlist item is in the same tuple of `R_EDUBERZA` — a user can exist with zero of any 157 of them, and having one order says nothing about how many holdings, trades or candles exist 158 alongside it. (The one FK that crosses between two of these — `T_RELATED_ORDER` — is 159 nullable, so it cannot be relied on to always connect a transaction row back to an order.) 160 That means no proper subset of attributes can functionally determine all 68 attributes of 161 `R_EDUBERZA`: the only way to pin down a `H_*` value, an `O_*` value, a `T_*` value, an 162 `MT_*` value, an `MC_*` value, a `W_*` value *and* a `WI_*` value at once is to state one 163 identifying attribute from each cluster explicitly. 164 165 **Chosen primary key** (closure shown below): 166 167 ``` 168 { U_ID, C_ID, M_ID, H_ID, O_ID, T_ID, MT_ID, MC_ID, W_ID, WI_ID } 169 ``` 170 171 **Closure check**, applying FD1–FD17 in turn to this set: 172 173 | Step | Attributes added | Dependency used | 174 |---|---|---| 175 | start | `U_ID, C_ID, M_ID, H_ID, O_ID, T_ID, MT_ID, MC_ID, W_ID, WI_ID` | — | 176 | 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 → …`) | 177 | 2 | `C_SYMBOL, C_NAME, C_CREATED_AT` | FD4 | 178 | 3 | `M_CRYPTO_ID, M_QUOTE_CURRENCY, M_IS_ACTIVE, M_CREATED_AT` | FD6 | 179 | 4 | `H_USER_ID, H_CRYPTO_ID, H_QUANTITY, H_RESERVED_QUANTITY, H_AVG_PRICE, H_CREATED_AT, H_UPDATED_AT` | FD8 | 180 | 5 | `O_USER_ID, O_MARKET_ID, O_SIDE, O_TYPE, O_STATUS, O_QUANTITY, O_PRICE, O_PLACED_AT, O_EXECUTED_AT` | FD10 | 181 | 6 | `T_USER_ID, T_TYPE, T_AMOUNT, T_CURRENCY, T_RELATED_ORDER, T_CREATED_AT, T_DESCRIPTION` | FD11 | 182 | 7 | `MT_MARKET_ID, MT_EXECUTED_AT, MT_PRICE, MT_QUANTITY, MT_SIDE, MT_SOURCE` | FD12 | 183 | 8 | `MC_MARKET_ID, MC_TIMEFRAME, MC_OPEN, MC_HIGH, MC_LOW, MC_CLOSE, MC_VOLUME, MC_CANDLE_TIME` | FD13 | 184 | 9 | `W_USER_ID, W_NAME, W_CREATED_AT` | FD15 | 185 | 10 | `WI_WATCHLIST_ID, WI_CRYPTO_ID, WI_ADDED_AT` | FD16 | 186 187 The closure now contains all 68 attributes, so the set is a superkey; removing any one of its 188 ten attributes drops an entire cluster that nothing else in the set can reach (e.g. drop 189 `T_ID` and no remaining attribute determines any `T_*` value), so it is minimal — a candidate 190 key. 191 192 **It is not the only one.** Any attribute that is itself a determinant of a whole cluster can 193 stand in for that cluster's id — `U_USERNAME` or `U_EMAIL` for `U_ID` (FD2/FD3), `C_SYMBOL` 194 for `C_ID` (FD5), `{M_CRYPTO_ID, M_QUOTE_CURRENCY}` for `M_ID` (FD7), `{H_USER_ID, 195 H_CRYPTO_ID}` for `H_ID` (FD9), `{MC_MARKET_ID, MC_TIMEFRAME, MC_CANDLE_TIME}` for `MC_ID` 196 (FD14), `{WI_WATCHLIST_ID, WI_CRYPTO_ID}` for `WI_ID` (FD17) — giving 3 × 2 × 2 × 2 × 1 × 1 × 197 1 × 2 × 1 × 2 = 96 candidate keys in total. The all-surrogate-id combination above is chosen 198 as **primary key** for the same reason `id` was chosen over `username`/`email`/`symbol`/etc. 199 per entity in [ERModel](../P1-ConceptualModel/ERModel.md): it is opaque, and none of its parts 200 are things a user would ever legitimately change. 201 202 **Normal form of `R_EDUBERZA` before decomposition:** 1NF only, and barely that — see 2NF 203 below. It cannot be in 2NF, 3NF or BCNF, since each of those requires 2NF as a precondition. 204 205 ## 1NF decomposition 206 207 No decomposition happens at this step. 1NF requires atomic, single-valued attributes and no 208 repeating groups; `R_EDUBERZA` was built that way from the start (every column above is a 209 single scalar), so the relation already satisfies 1NF as written in 210 [De-normalized database form](#de-normalized-database-form). The real work starts at 2NF. 211 212 ## 2NF decomposition 213 214 **Relation analyzed:** `R_EDUBERZA`, all 68 attributes, primary key 215 `{U_ID, C_ID, M_ID, H_ID, O_ID, T_ID, MT_ID, MC_ID, W_ID, WI_ID}` (10 attributes), FD1–FD17 216 in force. 217 218 **Current normal form:** 1NF only (previous section). 219 220 **Violations:** 2NF forbids a non-prime attribute from depending on *part* of a candidate 221 key. Every single functional dependency in the canonical cover (FD1–FD17) has a left side 222 that is a **proper subset** of the ten-attribute primary key — `U_ID` alone, `C_ID` alone, …, 223 down to the two-attribute `{WI_WATCHLIST_ID, WI_CRYPTO_ID}`. There is no non-prime attribute 224 in `R_EDUBERZA` that depends on the whole ten-attribute key and nothing smaller. In other 225 words, *every* non-prime attribute violates 2NF at once — the violation is not a handful of 226 stray columns to peel off, it is the entire relation, because gluing ten independent record 227 types together under one artificial composite key was never going to satisfy 2NF to begin 228 with. 229 230 **Decomposition.** This uses 3NF/BCNF **synthesis** (Bernstein's algorithm) rather than the 231 binary decomposition algorithm: since the canonical cover is already in hand (as the phase 232 instructions recommend building first), synthesis creates one relation per left-hand side in 233 the cover directly, instead of hunting for one offending dependency at a time and splitting 234 in two repeatedly. Grouping FD1–FD17 by determinant produces ten relations: 235 236 | New relation | Attributes | Key(s) | Source FDs | 301 | `T_ID` (K1–K4) | `T_*`, `O_ID`, and through them `O_*`, `U_ID`, `U_*`, `M_ID`, `M_*`, `C_ID`, `C_*`, `H_ID`, `H_*` | FD11, then FD10, FD1, FD6, FD4, FD9, FD8 | 302 | `OE_ID` (K1–K4) | `OE_*`, `O_ID` | FD13 | 303 | `MT_ID` (K1–K4) | `MT_*`, `M_ID`, `O_ID_FILLSBUY`, `O_ID_FILLSSELL` | FD12 | 304 | `MC_ID` (K1, K2) | `MC_OPEN, MC_HIGH, MC_LOW, MC_CLOSE, MC_VOLUME`, `M_ID` | FD14 | 305 | `W_ID` (K2, K4) | `W_*`, `U_ID` | FD16 | 306 | `WI_ID` (K1, K3) | `WI_ADDED_AT`, `C_ID` | FD17 | 307 308 The table lists the part of a key that each group depends on most directly. It is not the 309 only one: under K3/K4, for example, `MC_OPEN … MC_VOLUME` also depend on 310 `{MT_ID, MC_TIMEFRAME, MC_CANDLE_TIME}`, and under K1/K3 `W_*` depend on `WI_ID` through 311 `W_ID`. These lead to the same relations, so they need no extra steps. `MC_TIMEFRAME`, 312 `MC_CANDLE_TIME` and `W_ID` also depend on parts of keys, but they are prime, so 2NF does not 313 restrict them. They are handled under BCNF. 314 315 Each step below removes one row of this table, splitting the current relation into two. The 316 **order** is chosen so that no dependency is lost. `T_ID` goes first, because its group is the 317 largest and carries FD1–FD11 with it. Each later step handles a group whose determinant is 318 still in the residual relation. 319 320 ### Step 2NF-1 — partial dependency on `T_ID` 321 322 - **Relation analyzed:** `R_EDUBERZA` (66 attributes). 323 - **Dependencies:** FD1–FD18. **Candidate keys:** K1–K4. **Primary key:** K1. 324 **Normal form:** 1NF. 325 - **2NF violations:** all six rows of the table above. **Split first on `T_ID`**, the 326 largest group (see the order explained above). 327 - **Decomposition dependency:** `T_ID → T_*, O_ID` (FD11), together with everything it 328 determines transitively (FD10, FD1, FD6, FD4, FD9, FD8). `T_ID` is a proper part of K1, and 329 `T_AMOUNT`, for example, is non-prime, so this violates 2NF. 330 - **New relation `R_A`** = `{ T_ID, T_*, O_ID, O_*, U_ID, U_*, M_ID, M_*, C_ID, C_*, H_ID, H_* }` 331 (39 attributes). Dependencies: FD1–FD11. Candidate key and primary key: `T_ID`. Normal form: 2NF (it has a 332 one-attribute key), but not 3NF (see 3NF). 333 - **Residual relation `S1`** = `R_EDUBERZA − { T_*, O_*, U_*, M_*, C_*, H_ID, H_* }` = 334 `{ T_ID, O_ID, U_ID, M_ID, C_ID, OE_ID, OE_*, MT_ID, MT_*, O_ID_FILLSBUY, O_ID_FILLSSELL, 335 MC_ID, MC_*, W_ID, W_*, WI_ID, WI_ADDED_AT }` (32 attributes). `O_ID`, `U_ID`, `M_ID` and 336 `C_ID` stay, because FD13, FD16, FD12/FD14/FD15 and FD17/FD18 still need them. Dependencies: FD12–FD18, plus the projected dependencies between the identifiers kept here: 337 `T_ID → O_ID, U_ID, M_ID, C_ID`, `O_ID → U_ID, M_ID, C_ID`, `M_ID → C_ID`, 338 `OE_ID → U_ID, M_ID, C_ID`, `MT_ID → C_ID`, `MC_ID → C_ID`, `WI_ID → U_ID`. 339 Candidate keys: K1–K4 (all 340 their attributes are still here). Normal form: 1NF. 341 - **Dependency preservation:** FD1–FD11 lie entirely in `R_A`, and FD12–FD18 entirely in `S1`. ✓ 342 - **Lossless join:** `R_A ∩ S1 = { T_ID, O_ID, U_ID, M_ID, C_ID }` contains `T_ID`, and 343 `T_ID → R_A`, so `(R_A ∩ S1) → R_A`. ✓ 344 345 ### Step 2NF-2 — partial dependency on `OE_ID` 346 347 - **Relation analyzed:** `S1` (32 attributes). Dependencies: as listed for `S1` in the 348 previous step. Candidate keys: K1–K4. Primary key: K1. Normal form: 1NF. 349 - **Remaining 2NF violations:** the partial dependencies on `OE_ID`, `MT_ID`, `MC_ID`, `W_ID` 350 and `WI_ID` (table above), and the partial dependencies of the kept identifiers 351 `O_ID`, `U_ID`, `M_ID`, `C_ID` on `T_ID`. The kept identifiers cannot leave yet, because 352 other groups still need them. Each one leaves with the last group that needs it (`O_ID` in 353 2NF-2, `M_ID` in 2NF-4, `U_ID` in 2NF-5, `C_ID` in 2NF-6). **Split first on `OE_ID`**, 354 because after it no group needs `O_ID` any more. 355 - **Decomposition dependency:** `OE_ID → OE_*, O_ID` (FD13). `OE_ID` is a proper part of K1 356 and `OE_*` are non-prime. 357 - **New relation `R_B`** = `{ OE_ID, OE_*, O_ID }` (7 attributes). Dependencies: FD13. 358 Candidate key: `OE_ID`. Normal form: BCNF. 359 - **Residual relation `S2`** = `S1 − { OE_*, O_ID }` (26 attributes). No dependency still 360 needed in the residual uses `O_ID`. Dependencies: FD12, FD14–FD18, plus the projected `T_ID → U_ID, M_ID, C_ID`, 361 `OE_ID → U_ID, M_ID, C_ID`, `M_ID → C_ID`, `MT_ID → C_ID`, `MC_ID → C_ID`, `WI_ID → U_ID`. 362 Candidate keys: K1–K4. Normal form: 1NF. 363 - **Dependency preservation:** FD13 is in `R_B`, and the others are in `S2`. 364 `T_ID → O_ID` is already kept in `R_A`. ✓ 365 - **Lossless join:** `R_B ∩ S2 = { OE_ID }`, and `OE_ID → R_B` (FD13). ✓ 366 367 ### Step 2NF-3 — partial dependency on `MT_ID` 368 369 - **Relation analyzed:** `S2` (26 attributes). Dependencies: as listed for `S2` in the 370 previous step. Candidate keys: K1–K4. Primary key: K1. Normal form: 1NF. 371 - **Remaining 2NF violations:** the groups of `MT_ID`, `MC_ID`, `W_ID`, `WI_ID`, and the kept 372 identifiers `U_ID`, `M_ID`, `C_ID`. **Split first on `MT_ID`**, the next group. `M_ID` must 373 still stay for `MC_ID`. 374 - **Decomposition dependency:** `MT_ID → MT_*, M_ID, O_ID_FILLSBUY, O_ID_FILLSSELL` (FD12). 375 - **New relation `R_C`** = `{ MT_ID, MT_*, M_ID, O_ID_FILLSBUY, O_ID_FILLSSELL }` 376 (9 attributes). Dependencies: FD12. Candidate key: `MT_ID`. Normal form: BCNF. 377 - **Residual relation `S3`** = `S2 − { MT_*, O_ID_FILLSBUY, O_ID_FILLSSELL }` (19 attributes). 378 `M_ID` stays, because FD14/FD15 need it. Dependencies: FD14–FD18, plus the projected `T_ID → U_ID, M_ID, C_ID`, 379 `OE_ID → U_ID, M_ID, C_ID`, `MT_ID → M_ID, C_ID`, `M_ID → C_ID`, `MC_ID → C_ID`, 380 `WI_ID → U_ID`. 381 Candidate keys: K1–K4. Normal form: 1NF. 382 - **Dependency preservation:** FD12 is in `R_C`, and FD14–FD18 are in `S3`. ✓ 383 - **Lossless join:** `R_C ∩ S3 = { MT_ID, M_ID }` contains `MT_ID`, and `MT_ID → R_C` 384 (FD12). ✓ 385 386 ### Step 2NF-4 — partial dependency on `MC_ID` 387 388 - **Relation analyzed:** `S3` (19 attributes). Dependencies: as listed for `S3` in the 389 previous step. Candidate keys: K1–K4. Primary key: K1. Normal form: 1NF. 390 - **Remaining 2NF violations:** the groups of `MC_ID`, `W_ID`, `WI_ID`, and the kept 391 identifiers `U_ID`, `M_ID`, `C_ID`. **Split first on `MC_ID`**, the last group that needs 392 `M_ID`, so `M_ID` can leave with it. 393 - **Decomposition dependency:** `MC_ID → MC_OPEN, MC_HIGH, MC_LOW, MC_CLOSE, MC_VOLUME, M_ID` 394 (FD14). `MC_ID` is a proper part of K1. The prime `MC_TIMEFRAME` and `MC_CANDLE_TIME` also go 395 into the new relation, so that FD15, which needs them with `M_ID` and `MC_ID`, is preserved. 396 - **New relation `R_D`** = `{ MC_ID, MC_TIMEFRAME, MC_OPEN, MC_HIGH, MC_LOW, MC_CLOSE, 397 MC_VOLUME, MC_CANDLE_TIME, M_ID }` (9 attributes). Dependencies: FD14, FD15. Candidate keys: 398 `MC_ID` and `{M_ID, MC_TIMEFRAME, MC_CANDLE_TIME}`. Normal form: BCNF. 399 - **Residual relation `S4`** = `S3 − { MC_OPEN, MC_HIGH, MC_LOW, MC_CLOSE, MC_VOLUME, M_ID }` 400 (13 attributes). `MC_TIMEFRAME` and `MC_CANDLE_TIME` are prime and stay. Dependencies: FD16–FD18, plus the projected `T_ID → U_ID, C_ID`, `OE_ID → U_ID, C_ID`, 401 `MT_ID → C_ID`, `MC_ID → MC_TIMEFRAME, MC_CANDLE_TIME, C_ID`, `WI_ID → U_ID`. 402 Candidate keys: K1–K4. Normal form: 1NF. 403 - **Dependency preservation:** FD14 and FD15 are in `R_D`, and FD16–FD18 are in `S4`. ✓ 404 - **Lossless join:** `R_D ∩ S4 = { MC_ID, MC_TIMEFRAME, MC_CANDLE_TIME }` contains `MC_ID`, 405 and `MC_ID → R_D` (FD14). ✓ 406 407 ### Step 2NF-5 — partial dependency on `W_ID` 408 409 - **Relation analyzed:** `S4` (13 attributes). Dependencies: as listed for `S4` in the 410 previous step. Candidate keys: K1–K4. Primary key: K1. Normal form: 1NF. 411 - **Remaining 2NF violations:** the groups of `W_ID` and `WI_ID`, and the kept identifiers 412 `U_ID`, `C_ID`. **Split first on `W_ID`**, the last group that needs `U_ID`. 413 - **Decomposition dependency:** `W_ID → W_NAME, W_CREATED_AT, U_ID` (FD16). `W_ID` is a proper 414 part of K2. 415 - **New relation `R_E`** = `{ W_ID, W_NAME, W_CREATED_AT, U_ID }` (4 attributes). Dependencies: 416 FD16. Candidate key: `W_ID`. Normal form: BCNF. 417 - **Residual relation `S5`** = `S4 − { W_NAME, W_CREATED_AT, U_ID }` (10 attributes). 418 Dependencies: FD17, FD18, plus the projected `T_ID → C_ID`, `OE_ID → C_ID`, `MT_ID → C_ID`, 419 `MC_ID → MC_TIMEFRAME, MC_CANDLE_TIME, C_ID`. 420 Candidate keys: K1–K4. Normal form: 1NF. 421 - **Dependency preservation:** FD16 is in `R_E`, and FD17 and FD18 are in `S5`. ✓ 422 - **Lossless join:** `R_E ∩ S5 = { W_ID }`, and `W_ID → R_E` (FD16). ✓ 423 424 ### Step 2NF-6 — partial dependency on `WI_ID` 425 426 - **Relation analyzed:** `S5` (10 attributes). Dependencies: as listed for `S5` in the 427 previous step. Candidate keys: K1–K4. Primary key: K1. Normal form: 1NF. 428 - **Remaining 2NF violations:** the group of `WI_ID`, and the kept identifier `C_ID`. 429 **Split on `WI_ID`**, the last group that needs `C_ID`. 430 - **Decomposition dependency:** `WI_ID → WI_ADDED_AT, W_ID, C_ID` (FD17). `WI_ID` is a proper 431 part of K1, and `WI_ADDED_AT` and `C_ID` are non-prime. 432 - **New relation `R_F`** = `{ WI_ID, WI_ADDED_AT, W_ID, C_ID }` (4 attributes). Dependencies: 433 FD17, FD18. Candidate keys: `WI_ID` and `{W_ID, C_ID}`. Normal form: BCNF. 434 - **Residual relation `S6`** = `S5 − { WI_ADDED_AT, C_ID }` = 435 `{ T_ID, OE_ID, MT_ID, MC_ID, MC_TIMEFRAME, MC_CANDLE_TIME, W_ID, WI_ID }` (8 attributes). 436 `W_ID` is prime and stays. Dependencies: no dependency of the cover lies entirely inside 437 `S6`. The projected ones are `MC_ID → MC_TIMEFRAME, MC_CANDLE_TIME` and `WI_ID → W_ID`, plus 438 derived ones such as `MT_ID, MC_TIMEFRAME, MC_CANDLE_TIME → MC_ID` and `MT_ID, W_ID → WI_ID`. 439 Candidate keys: K1–K4. Normal form: 3NF, because every attribute is prime (and so 2NF). 440 - **Dependency preservation:** FD17 and FD18 are in `R_F`. ✓ 441 - **Lossless join:** `R_F ∩ S6 = { WI_ID, W_ID }` contains `WI_ID`, and `WI_ID → R_F` 442 (FD17). ✓ 443 444 **Result of 2NF:** `R_A`, `R_B`, `R_C`, `R_D`, `R_E`, `R_F`, `S6`. All seven are in 2NF (`R_A` 445 only 2NF, `S6` 3NF, the rest BCNF). All 18 dependencies are preserved: FD1–FD11 in `R_A`, 446 FD13 in `R_B`, FD12 in `R_C`, FD14–FD15 in `R_D`, FD16 in `R_E`, FD17–FD18 in `R_F`. 447 448 ## 3NF decomposition 449 450 Only `R_A` is not in 3NF. `R_B`–`R_F` are already in BCNF, and `S6` is in 3NF (all its 451 attributes are prime). 452 453 **Dependencies that violate 3NF in `R_A`.** 3NF forbids a non-prime attribute from depending on 454 a key only **transitively**, through a determinant that is not a superkey. The only key of 455 `R_A` is `T_ID`, but inside `R_A`: 456 457 - `U_ID → U_*` (FD1), `U_USERNAME → U_ID` (FD2), `U_EMAIL → U_ID` (FD3) 458 - `C_ID → C_*` (FD4), `C_SYMBOL → C_ID` (FD5) 459 - `U_ID, C_ID → H_ID` (FD9), `H_ID → H_*, U_ID, C_ID` (FD8) 460 - `M_ID → M_*, C_ID` (FD6), `C_ID, M_QUOTE_CURRENCY → M_ID` (FD7) 461 - `O_ID → O_*, U_ID, M_ID` (FD10) 462 463 None of these determinants is a superkey of `R_A`. For example, `T_ID → O_ID → O_PRICE` is a 464 transitive dependency of the non-prime `O_PRICE` on the key. 465 466 **Order of the steps.** An attribute can leave the residual only after every dependency that 467 needs it has been extracted. FD9 needs `U_ID` and `C_ID` together, and extracting `Markets` 468 takes `C_ID` out of the residual, so `Holdings` must come before `Markets`. Extracting 469 `Orders` takes `M_ID` and `U_ID` out, so `Orders` comes last. The dependencies are therefore 470 taken from the "leaves" of the chain `T_ID → O_ID → {U_ID, M_ID → C_ID}` inward. 471 472 ### Step 3NF-1 — transitive dependency through `U_ID` 473 474 - **Relation analyzed:** `R_A` (39 attributes), dependencies FD1–FD11, candidate key and 475 primary candidate key and primary key `T_ID`, normal form 2NF. 476 - **3NF violations:** all five groups listed above. **Split first on `U_ID`**. It is a leaf 477 of the chain: its dependents determine nothing outside its own group. 478 - **Decomposition dependency:** `U_ID → U_*` (FD1). `U_ID` is not a superkey of `R_A`. 479 - **New relation `R_USERS`** = `{ U_ID, U_* }` (10 attributes). Dependencies: FD1, FD2, FD3. 480 Candidate keys: `U_ID`, `U_USERNAME`, `U_EMAIL`. Primary key: `U_ID`. Normal form: BCNF. 481 - **Residual relation `R_A1`** = `R_A − U_*` (30 attributes). Dependencies: FD4–FD11, which 482 also imply `T_ID → H_ID` and `O_ID → H_ID` (through `U_ID, C_ID`). 483 Candidate key and primary key: `T_ID`. Normal form: 2NF. 484 - **Dependency preservation:** FD1–FD3 are in `R_USERS`, and FD4–FD11 are in `R_A1`. ✓ 485 - **Lossless join:** `R_USERS ∩ R_A1 = { U_ID }`, and `U_ID → R_USERS` (FD1). ✓ 486 487 ### Step 3NF-2 — transitive dependency through `C_ID` 488 489 - **Relation analyzed:** `R_A1` (30 attributes), dependencies FD4–FD11, candidate key and primary key `T_ID`, normal form 490 2NF. 491 - **3NF violations:** `C_ID → C_*`, `U_ID, C_ID → H_ID → H_*`, `M_ID → M_*, C_ID`, 492 `O_ID → O_*, U_ID, M_ID`. **Split first on `C_ID`**, the next leaf. 493 - **Decomposition dependency:** `C_ID → C_*` (FD4). 494 - **New relation `R_CRYPTO`** = `{ C_ID, C_* }` (4 attributes). Dependencies: FD4, FD5. 495 Candidate keys: `C_ID`, `C_SYMBOL`. Primary key: `C_ID`. Normal form: BCNF. 496 - **Residual relation `R_A2`** = `R_A1 − C_*` (27 attributes). Dependencies: FD6–FD11. Candidate 497 key and primary key: `T_ID`. Normal form: 2NF. 498 - **Dependency preservation:** FD4 and FD5 are in `R_CRYPTO`, and FD6–FD11 are in `R_A2`. ✓ 499 - **Lossless join:** `R_CRYPTO ∩ R_A2 = { C_ID }`, and `C_ID → R_CRYPTO` (FD4). ✓ 500 501 ### Step 3NF-3 — transitive dependency through `{U_ID, C_ID}` 502 503 - **Relation analyzed:** `R_A2` (27 attributes), dependencies FD6–FD11, candidate key and primary key `T_ID`, normal form 504 2NF. 505 - **3NF violations:** `U_ID, C_ID → H_ID → H_*`, `M_ID → M_*, C_ID`, `O_ID → O_*, U_ID, M_ID`. 506 **Split first on `{U_ID, C_ID}`**, because it must come before `Markets` takes `C_ID` away. 507 - **Decomposition dependency:** `U_ID, C_ID → H_ID` (FD9), together with `H_ID → H_*` 508 (FD8). 509 - **New relation `R_HOLDINGS`** = `{ H_ID, H_*, U_ID, C_ID }` (8 attributes). Dependencies: 510 FD8, FD9. Candidate keys: `H_ID`, `{U_ID, C_ID}`. Primary key: `H_ID`. Normal form: BCNF. 511 - **Residual relation `R_A3`** = `R_A2 − { H_ID, H_* }` (21 attributes). Dependencies: FD6, 512 FD7, FD10, FD11. Candidate key and primary key: `T_ID`. Normal form: 2NF. 513 - **Dependency preservation:** FD8 and FD9 are in `R_HOLDINGS`, and the others are in `R_A3`. ✓ 514 - **Lossless join:** `R_HOLDINGS ∩ R_A3 = { U_ID, C_ID }`, and `U_ID, C_ID → H_ID → H_*`, so 515 `{U_ID, C_ID} → R_HOLDINGS`. ✓ 516 517 ### Step 3NF-4 — transitive dependency through `M_ID` 518 519 - **Relation analyzed:** `R_A3` (21 attributes), dependencies FD6, FD7, FD10, FD11, key 520 `T_ID`, normal form 2NF. 521 - **3NF violations:** `M_ID → M_*, C_ID` and `O_ID → O_*, U_ID, M_ID`. **Split first on 522 `M_ID`**, because `Orders` still needs `M_ID`. 523 - **Decomposition dependency:** `M_ID → M_*, C_ID` (FD6). 524 - **New relation `R_MARKETS`** = `{ M_ID, M_*, C_ID }` (5 attributes). Dependencies: FD6, FD7. 525 Candidate keys: `M_ID`, `{C_ID, M_QUOTE_CURRENCY}`. Primary key: `M_ID`. Normal form: BCNF. 526 - **Residual relation `R_A4`** = `R_A3 − { M_*, C_ID }` (17 attributes). No dependency left 527 needs `C_ID`. Dependencies: FD10, FD11. Candidate key and primary key: `T_ID`. Normal form: 2NF. 528 - **Dependency preservation:** FD6 and FD7 are in `R_MARKETS`, and FD10 and FD11 are in 529 `R_A4`. ✓ 530 - **Lossless join:** `R_MARKETS ∩ R_A4 = { M_ID }`, and `M_ID → R_MARKETS` (FD6). ✓ 531 532 ### Step 3NF-5 — transitive dependency through `O_ID` 533 534 - **Relation analyzed:** `R_A4` = `{ T_ID, T_*, O_ID, O_*, U_ID, M_ID }` (17 attributes), 535 dependencies FD10, FD11, candidate key and primary key `T_ID`, normal form 2NF. 536 - **3NF violations:** only `O_ID → O_*, U_ID, M_ID`. **Split on `O_ID`**. 537 - **Decomposition dependency:** `O_ID → O_*, U_ID, M_ID` (FD10). 538 - **New relation `R_ORDERS`** = `{ O_ID, O_*, U_ID, M_ID }` (11 attributes). Dependencies: 539 FD10. Candidate key: `O_ID`. Normal form: BCNF. 540 - **Residual relation `R_TRANSACTIONS`** = `R_A4 − { O_*, U_ID, M_ID }` = `{ T_ID, T_*, O_ID }` 541 (7 attributes). Dependencies: FD11. Candidate key: `T_ID`. Normal form: BCNF. Keeping `U_ID` 542 here would have left the transitive dependency `T_ID → O_ID → U_ID` inside the relation. 543 `T_ID → U_ID` was removed from the cover as redundant, so nothing is lost. 544 - **Dependency preservation:** FD10 is in `R_ORDERS`, and FD11 is in `R_TRANSACTIONS`. ✓ 545 - **Lossless join:** `R_ORDERS ∩ R_TRANSACTIONS = { O_ID }`, and `O_ID → R_ORDERS` 546 (FD10). ✓ 547 548 **Result of 3NF:** `R_USERS`, `R_CRYPTO`, `R_HOLDINGS`, `R_MARKETS`, `R_ORDERS`, 549 `R_TRANSACTIONS` (from `R_A`), and `R_B`, `R_C`, `R_D`, `R_E`, `R_F`, `S6` unchanged. 12 550 relations, all in 3NF, and all except `S6` in BCNF. All 18 dependencies are preserved. 551 552 ## BCNF if possible 553 554 BCNF requires **every** determinant of a non-trivial dependency to be a superkey, even when 555 the dependent attribute is prime. 556 557 | Relation | Dependencies in force | Determinants | All superkeys? | 237 558 |---|---|---|---| 238 | `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 | 239 | `R_CRYPTO` | `C_ID, C_SYMBOL, C_NAME, C_CREATED_AT` | `C_ID`, `C_SYMBOL` | FD4, FD5 | 240 | `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 | 241 | `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 | 242 | `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 | 243 | `R_TRANSACTIONS` | `T_ID, T_USER_ID, T_TYPE, T_AMOUNT, T_CURRENCY, T_RELATED_ORDER, T_CREATED_AT, T_DESCRIPTION` | `T_ID` | FD11 | 244 | `R_MARKET_TRADES` | `MT_ID, MT_MARKET_ID, MT_EXECUTED_AT, MT_PRICE, MT_QUANTITY, MT_SIDE, MT_SOURCE` | `MT_ID` | FD12 | 245 | `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 | 246 | `R_WATCHLISTS` | `W_ID, W_USER_ID, W_NAME, W_CREATED_AT` | `W_ID` | FD15 | 247 | `R_WATCHLIST_ITEMS` | `WI_ID, WI_WATCHLIST_ID, WI_CRYPTO_ID, WI_ADDED_AT` | `WI_ID`, `{WI_WATCHLIST_ID, WI_CRYPTO_ID}` | FD16, FD17 | 248 249 Every one of these ten relations now has **all** of its non-prime attributes depending on its 250 **whole** key (in every case there is only one non-composite or one designated key doing the 251 determining, so 2NF holds trivially in each). 252 253 **Dependency preservation.** FD1–FD17 is the canonical cover of `R_EDUBERZA`. Each FD's 254 determinant and every one of its dependent attributes land inside exactly one of the ten new 255 relations (see the "Source FDs" column above — no FD is split across two relations). The 256 union of the FDs that hold on `R_USERS, …, R_WATCHLIST_ITEMS` is therefore exactly FD1–FD17 257 again: nothing was lost. 258 259 **Lossless join — chase test.** 260 261 > *Note: the chase algorithm is not part of the course material. I was curious about a 262 > stricter way to test lossless join than the usual "the common attributes are a key of one 263 > side" argument, so I applied it here.* 264 265 The chase decides whether a decomposition `R = R1 ∪ … ∪ Rn` is lossless under a set of 266 functional dependencies. Build a tableau with one column per attribute of `R` and one row per 267 relation `Ri`. In row `i`, put a distinguished symbol `a` in every column of `Ri` and a unique 268 symbol `b_i` in every other column. Then repeat, until nothing changes: for each FD `X → Y`, 269 whenever two rows agree on all of `X`, make them agree on `Y`. If they disagree, an `a` wins, 270 otherwise one `b` replaces the other. **The decomposition is lossless exactly when some row 271 ends up with `a` in every column.** 272 273 All attributes of one cluster (`U_*`, `C_*`, `M_*`, …) always appear together, and FD1–FD17 274 never mix clusters. So each cluster is one column group below: `a` means every column of the 275 group holds a distinguished symbol, and `b` means none of them does. The foreign-key 276 attributes (`H_USER_ID`, `O_MARKET_ID`, …) belong to their own cluster (`H_*`, `O_*`, …), not 277 to the cluster they reference. 278 279 **Step 1 — the ten relations from the table above.** 280 281 ``` 282 U* C* M* H* O* T* MT* MC* W* WI* 283 R_USERS a b b b b b b b b b 284 R_CRYPTO b a b b b b b b b b 285 R_MARKETS b b a b b b b b b b 286 R_HOLDINGS b b b a b b b b b b 287 R_ORDERS b b b b a b b b b b 288 R_TRANSACTIONS b b b b b a b b b b 289 R_MARKET_TR. b b b b b b a b b b 290 R_MARKET_CA. b b b b b b b a b b 291 R_WATCHLISTS b b b b b b b b a b 292 R_WATCHLIST_I. b b b b b b b b b a 293 ``` 294 295 Every FD has its left side inside one cluster, for example `U_ID → U_*` or 296 `H_USER_ID, H_CRYPTO_ID → H_ID`. For such an FD to fire, two rows would have to agree on that 297 left side. But only one row has `a`s in that cluster, and the `b`s of different rows are 298 all different, so no two rows ever agree on any left side. **The chase changes nothing, and 299 no row becomes all `a`.** Under FD1–FD17 alone, the ten relations are *not* guaranteed to 300 join back to `R_EDUBERZA`. This is not an accident of this model. It is exactly why 301 Bernstein's synthesis algorithm has a final step: *if no synthesised relation contains a 302 candidate key of `R`, add one that does.* None of the ten contains the ten-attribute key. 303 304 **Step 2 — add the key relation** `R_KEY(U_ID, C_ID, M_ID, H_ID, O_ID, T_ID, MT_ID, MC_ID, 305 W_ID, WI_ID)`. Its row has `a` only in the ten ID columns, written `a·` for "`a` in the ID, 306 `b` in the rest of the group": 307 308 ``` 309 U* C* M* H* O* T* MT* MC* W* WI* 310 R_KEY a· a· a· a· a· a· a· a· a· a· 311 (the ten rows of step 1 unchanged) 312 ``` 313 314 Now FD1 `U_ID → U_*` fires: row `R_KEY` and row `R_USERS` both have `a` in `U_ID`, so they 315 must agree on the rest of `U_*`, and `R_USERS` has `a` there. `R_KEY` becomes `a` in the whole 316 `U*` group. The same happens with FD4 (`C*`), FD6 (`M*`), FD8 (`H*`), FD10 (`O*`), FD11 (`T*`), 317 FD12 (`MT*`), FD13 (`MC*`), FD15 (`W*`) and FD16 (`WI*`): 318 319 ``` 320 U* C* M* H* O* T* MT* MC* W* WI* 321 R_KEY a a a a a a a a a a <- all distinguished 322 ``` 323 324 **Row `R_KEY` is all `a`, so the decomposition into the ten relations plus `R_KEY` is 325 lossless.** 326 327 **Why `R_KEY` is not kept in the final schema.** An instance of `R_KEY` would only record 328 which ID of one cluster appears together with which ID of every other cluster. As shown under 329 *Candidate keys and primary key*, the ten clusters are independent record types, and 330 `R_EDUBERZA` pairs every row of one with every row of the others. So `R_KEY` would be just the 331 cross product of the ten ID sets and would carry no information. The same independence means 332 the join dependency `⋈[R_USERS, …, R_WATCHLIST_ITEMS]` holds on `R_EDUBERZA` by construction. 333 Under that dependency the ten relations alone already reconstruct it: their natural join, with 334 no common attributes, is exactly that cross product. The chase makes this reasoning explicit. 335 FDs by themselves cannot prove the join lossless; you need either the key relation or the 336 independence of the clusters. That was hidden in the earlier "foreign key equals primary key" 337 argument, which described the equi-joins the application runs, not the natural join the 338 lossless-join property is about. 339 340 ## 3NF decomposition 341 342 **Relations analyzed:** each of the ten relations produced above, individually. 343 344 For each relation, 3NF asks whether any non-prime attribute is *transitively* dependent on a 345 key — i.e. determined by another non-prime attribute rather than directly by the key. This is 346 exactly where the foreign-key-carried dependencies from 347 [Dependencies carried by foreign keys](#dependencies-carried-by-foreign-keys) have to be 348 checked, because that table is precisely the list of "dependency that would cause a problem at 349 the next higher normal form" the phase template asks for. 350 351 **Worked example — `R_MARKETS`.** Its key `M_ID` determines `M_CRYPTO_ID`, and 352 `M_CRYPTO_ID → C_SYMBOL, C_NAME, C_CREATED_AT` also holds (`M_CRYPTO_ID` draws its values from 353 `C_ID`'s domain). If `C_SYMBOL`, `C_NAME` and `C_CREATED_AT` were still columns of 354 `R_MARKETS`, this would be exactly the transitive dependency `M_ID → M_CRYPTO_ID → C_SYMBOL` 355 that violates 3NF. They are not: the 2NF step above already put them in `R_CRYPTO`, keyed 356 directly by `C_ID` (FD4), because FD4 — not the derived `M_CRYPTO_ID → C_SYMBOL` — is what the 357 canonical cover actually contains. `R_MARKETS` itself has no attribute that determines another 358 non-prime attribute of `R_MARKETS`; the transitive dependency is real, but it points *out* of 359 the relation, not within it. 360 361 The same reasoning applies to every other foreign key in the list: `H_USER_ID`/`H_CRYPTO_ID`, 362 `O_USER_ID`/`O_MARKET_ID`, `T_USER_ID`/`T_RELATED_ORDER`, `MT_MARKET_ID`, `MC_MARKET_ID`, 363 `W_USER_ID`, `WI_WATCHLIST_ID`/`WI_CRYPTO_ID` are all foreign keys sitting *alongside* a 364 non-key attribute set that depends only on their own relation's key, never on the foreign key 365 itself. None of `R_USERS`, `R_CRYPTO`, `R_HOLDINGS`, `R_ORDERS`, `R_TRANSACTIONS`, 366 `R_MARKET_TRADES`, `R_MARKET_CANDLES`, `R_WATCHLISTS`, `R_WATCHLIST_ITEMS` has a non-prime 367 attribute that another non-prime attribute of the *same* relation determines. 368 369 **Conclusion:** synthesising directly from the canonical cover in the 2NF step already 370 avoided every transitive dependency — there is nothing left to decompose for 3NF. All ten 371 relations from the previous section satisfy 3NF unchanged. 372 373 ## BCNF if possible 374 375 **Relations analyzed:** the same ten relations, checked against the stricter BCNF rule: every 376 determinant of every functional dependency that holds on the relation must be a candidate key 377 of that relation (3NF allows an exception when the dependent side is prime; BCNF does not). 378 379 | Relation | Functional dependencies in force | Determinant | Is it a candidate key? | 380 |---|---|---|---| 381 | `R_USERS` | FD1, FD2, FD3 | `U_ID`, `U_USERNAME`, `U_EMAIL` | Yes — all three are candidate keys | 382 | `R_CRYPTO` | FD4, FD5 | `C_ID`, `C_SYMBOL` | Yes — both candidate keys | 383 | `R_MARKETS` | FD6, FD7 | `M_ID`, `{M_CRYPTO_ID, M_QUOTE_CURRENCY}` | Yes — both candidate keys | 384 | `R_HOLDINGS` | FD8, FD9 | `H_ID`, `{H_USER_ID, H_CRYPTO_ID}` | Yes — both candidate keys | 385 | `R_ORDERS` | FD10 | `O_ID` | Yes — the only candidate key | 386 | `R_TRANSACTIONS` | FD11 | `T_ID` | Yes — the only candidate key | 387 | `R_MARKET_TRADES` | FD12 | `MT_ID` | Yes — the only candidate key | 388 | `R_MARKET_CANDLES` | FD13, FD14 | `MC_ID`, `{MC_MARKET_ID, MC_TIMEFRAME, MC_CANDLE_TIME}` | Yes — both candidate keys | 389 | `R_WATCHLISTS` | FD15 | `W_ID` | Yes — the only candidate key | 390 | `R_WATCHLIST_ITEMS` | FD16, FD17 | `WI_ID`, `{WI_WATCHLIST_ID, WI_CRYPTO_ID}` | Yes — both candidate keys | 391 392 Every determinant in every relation is one of that relation's own candidate keys. **All ten 393 relations are already in BCNF** — the highest of the four normal forms this phase asks for, 394 reached in the same step that fixed 2NF. This is not a coincidence: it happens because the 395 canonical cover already grouped each relation's own key directly against its own attributes 396 with no attribute appearing on the right side of two different relations' dependencies, which 397 is exactly what synthesis from a canonical cover guarantees when, as here, none of the 398 per-cluster functional dependencies overlap. 399 400 No further decomposition is possible or necessary; splitting any of the ten relations further 401 would only separate attributes that already depend on the *whole* key of a BCNF relation, 402 which cannot fix anything and only costs a join. 559 | `R_USERS` | FD1, FD2, FD3 | `U_ID`, `U_USERNAME`, `U_EMAIL` | yes | 560 | `R_CRYPTO` | FD4, FD5 | `C_ID`, `C_SYMBOL` | yes | 561 | `R_MARKETS` | FD6, FD7 | `M_ID`, `{C_ID, M_QUOTE_CURRENCY}` | yes | 562 | `R_HOLDINGS` | FD8, FD9 | `H_ID`, `{U_ID, C_ID}` | yes | 563 | `R_ORDERS` | FD10 | `O_ID` | yes | 564 | `R_TRANSACTIONS` | FD11 | `T_ID` | yes | 565 | `R_B` | FD13 | `OE_ID` | yes | 566 | `R_C` | FD12 | `MT_ID` | yes | 567 | `R_D` | FD14, FD15 | `MC_ID`, `{M_ID, MC_TIMEFRAME, MC_CANDLE_TIME}` | yes | 568 | `R_E` | FD16 | `W_ID` | yes | 569 | `R_F` | FD17, FD18 | `WI_ID`, `{W_ID, C_ID}` | yes | 570 | `S6` | `MC_ID → MC_TIMEFRAME, MC_CANDLE_TIME`; `WI_ID → W_ID`; derived ones such as `MT_ID, MC_TIMEFRAME, MC_CANDLE_TIME → MC_ID` and `MT_ID, W_ID → WI_ID` | `MC_ID`, `WI_ID`, `{MT_ID, MC_TIMEFRAME, MC_CANDLE_TIME}`, `{MT_ID, W_ID}`, … | **no** | 571 572 **Dependencies that violate BCNF — only in `S6`.** `MC_ID` determines `MC_TIMEFRAME` and 573 `MC_CANDLE_TIME`, and `WI_ID` determines `W_ID`, but neither `MC_ID` nor `WI_ID` is a superkey 574 of `S6`. 3NF allowed this because the dependent attributes are prime. BCNF does not. The derived 575 dependencies all involve `W_ID` or `MC_TIMEFRAME`/`MC_CANDLE_TIME`, so they disappear once the 576 two steps below remove those attributes. 577 578 ### Step BCNF-1 — `MC_ID → MC_TIMEFRAME, MC_CANDLE_TIME` 579 580 - **Relation analyzed:** `S6` (8 attributes), dependencies as in the table above, candidate 581 keys K1–K4, primary key K1, normal form 3NF. 582 - **BCNF violations:** `MC_ID → MC_TIMEFRAME, MC_CANDLE_TIME` and `WI_ID → W_ID`, and the 583 derived ones that depend on them. **Split first on `MC_ID`**. The order does not matter 584 here, because the two violations share no attribute. 585 - **Decomposition dependency:** `MC_ID → MC_TIMEFRAME, MC_CANDLE_TIME`. `MC_ID` is not a 586 superkey of `S6`. 587 - **New relation** `{ MC_ID, MC_TIMEFRAME, MC_CANDLE_TIME }`. Dependencies: 588 `MC_ID → MC_TIMEFRAME, MC_CANDLE_TIME`. Key: `MC_ID`. Normal form: BCNF. It is a 589 projection of `R_D`, which already contains these attributes with the same key, so it adds no 590 information and is merged into `R_D`. 591 - **Residual relation `S7`** = `{ T_ID, OE_ID, MT_ID, MC_ID, W_ID, WI_ID }` (6 attributes). 592 Dependencies: `WI_ID → W_ID`, and derived ones such as `MT_ID, W_ID → WI_ID`. Candidate 593 keys: `{T_ID, OE_ID, MT_ID, MC_ID, WI_ID}` (K1) and `{T_ID, OE_ID, MT_ID, MC_ID, W_ID}` (K2). 594 Normal form: 3NF. 595 - **Dependency preservation:** no dependency of the cover is affected. FD14 and FD15 are in 596 `R_D`. ✓ 597 - **Lossless join:** the intersection is `{ MC_ID }`, and `MC_ID → { MC_ID, MC_TIMEFRAME, 598 MC_CANDLE_TIME }`. ✓ 599 600 ### Step BCNF-2 — `WI_ID → W_ID` 601 602 - **Relation analyzed:** `S7` (6 attributes), dependencies `WI_ID → W_ID` and derived ones, 603 candidate keys K1, K2, primary key K1, normal form 3NF. 604 - **BCNF violations:** only `WI_ID → W_ID` (and the derived `MT_ID, W_ID → WI_ID`). **Split on 605 `WI_ID`**. 606 - **Decomposition dependency:** `WI_ID → W_ID`. `WI_ID` is not a superkey of `S7`. 607 - **New relation** `{ WI_ID, W_ID }`. Dependencies: `WI_ID → W_ID`. Key: `WI_ID`. Normal 608 form: BCNF. For the same reason as in BCNF-1, it is merged into `R_F`. 609 - **Residual relation `R_KEY`** = `{ T_ID, OE_ID, MT_ID, MC_ID, WI_ID }` (5 attributes). No 610 non-trivial dependency holds among these attributes. Candidate key: all five (= K1). 611 Normal form: BCNF. 612 - **Dependency preservation:** no dependency of the cover is affected. FD17 and FD18 are in 613 `R_F`. The derived dependencies of `S6`/`S7` follow from FD12, FD15, FD17 and FD18, which 614 are all preserved. ✓ 615 - **Lossless join:** the intersection is `{ WI_ID }`, and `WI_ID → { WI_ID, W_ID }`. ✓ 616 617 **Result: every relation is in BCNF.** The decomposition into these 12 relations is lossless 618 (each of the 13 binary steps passed the test) and preserves all 18 dependencies of the 619 canonical cover. 403 620 404 621 ## Final result and discussion 405 622 406 623 ### Normalized relational model 624 625 Each relation is followed by its keys (primary key first). An attribute that is the 626 identifier of another relation is marked `→` with that relation. 407 627 408 628 ``` 409 629 R_USERS (U_ID, U_USERNAME, U_EMAIL, U_FULL_NAME, U_PASSWORD_HASH, 410 U_AVAILABLE_BALANCE, U_INVESTED_BALANCE, U_CREATED_AT, U_UPDATED_AT) 630 U_AVAILABLE_BALANCE, U_INVESTED_BALANCE, U_RESERVED_BALANCE, 631 U_CREATED_AT, U_UPDATED_AT) 632 keys: U_ID; U_USERNAME; U_EMAIL 411 633 R_CRYPTO (C_ID, C_SYMBOL, C_NAME, C_CREATED_AT) 412 R_MARKETS (M_ID, M_CRYPTO_ID → R_CRYPTO, M_QUOTE_CURRENCY, M_IS_ACTIVE, M_CREATED_AT) 413 R_HOLDINGS (H_ID, H_USER_ID → R_USERS, H_CRYPTO_ID → R_CRYPTO, H_QUANTITY, 414 H_RESERVED_QUANTITY, H_AVG_PRICE, H_CREATED_AT, H_UPDATED_AT) 415 R_ORDERS (O_ID, O_USER_ID → R_USERS, O_MARKET_ID → R_MARKETS, O_SIDE, O_TYPE, 416 O_STATUS, O_QUANTITY, O_PRICE, O_PLACED_AT, O_EXECUTED_AT) 417 R_TRANSACTIONS (T_ID, T_USER_ID → R_USERS, T_TYPE, T_AMOUNT, T_CURRENCY, 418 T_RELATED_ORDER → R_ORDERS, T_CREATED_AT, T_DESCRIPTION) 419 R_MARKET_TRADES (MT_ID, MT_MARKET_ID → R_MARKETS, MT_EXECUTED_AT, MT_PRICE, MT_QUANTITY, 420 MT_SIDE, MT_SOURCE) 421 R_MARKET_CANDLES (MC_ID, MC_MARKET_ID → R_MARKETS, MC_TIMEFRAME, MC_OPEN, MC_HIGH, MC_LOW, 422 MC_CLOSE, MC_VOLUME, MC_CANDLE_TIME) 423 R_WATCHLISTS (W_ID, W_USER_ID → R_USERS, W_NAME, W_CREATED_AT) 424 R_WATCHLIST_ITEMS(WI_ID, WI_WATCHLIST_ID → R_WATCHLISTS, WI_CRYPTO_ID → R_CRYPTO, WI_ADDED_AT) 634 keys: C_ID; C_SYMBOL 635 R_MARKETS (M_ID, C_ID → R_CRYPTO, M_QUOTE_CURRENCY, M_IS_ACTIVE, M_CREATED_AT) 636 keys: M_ID; {C_ID, M_QUOTE_CURRENCY} 637 R_HOLDINGS (H_ID, U_ID → R_USERS, C_ID → R_CRYPTO, H_QUANTITY, 638 H_RESERVED_QUANTITY, H_AVG_PRICE, H_CREATED_AT, H_UPDATED_AT) 639 keys: H_ID; {U_ID, C_ID} 640 R_ORDERS (O_ID, U_ID → R_USERS, M_ID → R_MARKETS, O_SIDE, O_TYPE, O_STATUS, 641 O_QUANTITY, O_FILLED_QUANTITY, O_PRICE, O_PLACED_AT, O_EXECUTED_AT) 642 key: O_ID 643 R_TRANSACTIONS (T_ID, O_ID → R_ORDERS, T_TYPE, T_AMOUNT, T_CURRENCY, T_CREATED_AT, 644 T_DESCRIPTION) 645 key: T_ID 646 R_MARKET_TRADES (MT_ID, M_ID → R_MARKETS, MT_EXECUTED_AT, MT_PRICE, MT_QUANTITY, 647 MT_SIDE, MT_SOURCE, O_ID_FILLSBUY → R_ORDERS (nullable), 648 O_ID_FILLSSELL → R_ORDERS (nullable)) [= R_C] 649 key: MT_ID 650 R_ORDER_EVENTS (OE_ID, O_ID → R_ORDERS, OE_EVENT_TYPE, OE_QUANTITY, OE_PRICE, 651 OE_STATUS_AFTER, OE_CREATED_AT) [= R_B] 652 key: OE_ID 653 R_MARKET_CANDLES (MC_ID, M_ID → R_MARKETS, MC_TIMEFRAME, MC_OPEN, MC_HIGH, MC_LOW, 654 MC_CLOSE, MC_VOLUME, MC_CANDLE_TIME) [= R_D] 655 keys: MC_ID; {M_ID, MC_TIMEFRAME, MC_CANDLE_TIME} 656 R_WATCHLISTS (W_ID, U_ID → R_USERS, W_NAME, W_CREATED_AT) [= R_E] 657 key: W_ID 658 R_WATCHLIST_ITEMS(WI_ID, W_ID → R_WATCHLISTS, C_ID → R_CRYPTO, WI_ADDED_AT) [= R_F] 659 keys: WI_ID; {W_ID, C_ID} 660 R_KEY (T_ID, OE_ID, MT_ID, MC_ID, WI_ID) [= R_KEY] 661 key: all five 425 662 ``` 426 663 427 Ten relations, every one in BCNF, connected by the eleven foreign keys spelled out above.428 429 664 ### Discussion 430 665 431 **This is the P2 design.** Strip the `U_`/`C_`/`M_`/… prefixes back to plain column names and 432 `R_USERS, R_CRYPTO, R_MARKETS, R_HOLDINGS, R_ORDERS, R_TRANSACTIONS, R_MARKET_TRADES, 433 R_MARKET_CANDLES, R_WATCHLISTS, R_WATCHLIST_ITEMS` are, attribute for attribute and key for 434 key, `users, crypto, markets, holdings, orders, transactions, market_trades, market_candles, 435 watchlists, watchlist_items` from 436 [RelationalDesign](../P2-RelationalDesign/RelationalDesign.md). Every foreign key matches, 437 every candidate key matches (including the less obvious composite ones — `{user_id, 438 crypto_id}` on `holdings`, `{crypto_id, quote_currency}` on `markets`, `{market_id, timeframe, 439 candle_time}` on `market_candles`), and the normal form matches (P2 already claimed 3NF; this 440 phase shows the stronger result that the design is actually in BCNF). 441 442 That is not a coincidence of two people happening to agree — it is what should happen when a 443 design is derived correctly twice by two different methods from the same underlying model: 444 P2 got here by applying the standard ER-to-relational transformation rules (each entity 445 becomes a table on its own key, each attributed M:N relationship becomes a table on the 446 combined key, each attributeless 1:N relationship becomes a foreign key on the "many" side). 447 This phase got here by ignoring that transformation entirely, writing down only the 448 attributes and the functional dependencies they obey, and mechanically applying 2NF/3NF/BCNF 449 synthesis. Landing on the same ten relations either means the P2 transformation rules are 450 sound for this particular model (which they are, for exactly the reason [RelationalDesign](../P2-RelationalDesign/RelationalDesign.md#normalisation) 451 already argued: single-column UUID primary keys everywhere rule out partial dependencies by 452 construction, and no non-key attribute references another non-key attribute anywhere in the 453 model, which rules out transitive dependencies too), or it is a coincidence spanning ten 454 independently-checked relations and dozens of functional dependencies — the first explanation 455 is the only credible one. 456 457 **The one substantive difference** is `holdings.avg_price`, which P2 documents as a 458 *derived* attribute — the running weighted-average buy price, recomputable from the `buy` rows 459 in `transactions` — kept as a stored column anyway for read performance 460 ([RelationalDesign](../P2-RelationalDesign/RelationalDesign.md#normalisation) calls this out 461 explicitly as an accepted denormalisation). Nothing in this phase's functional-dependency 462 analysis can see that `H_AVG_PRICE` is derivable from `T_*` rows rather than stored 463 independently — FD8 (`H_ID → H_AVG_PRICE`) is a perfectly ordinary functional dependency 464 either way, because *derivability from a different relation's rows* is a property of the data 465 and the application logic that maintains it (see 466 [UseCase0004](../P3-UseCaseModel/UseCase0004.md)'s `ON CONFLICT … DO UPDATE`), not something 467 that shows up as a violation of any single-relation normal form. Formal normalization and "no 468 column is a cached computation of other columns" are related but different concerns; this 469 phase only checked the first one. 470 471 **Which design is used going forward:** P2's, unchanged. Since the two designs coincide 472 exactly, "restructuring the database objects" means confirming there is nothing to change 473 rather than writing new DDL. [`server/db/schema_creation.sql`](../../server/db/schema_creation.sql) 474 already matches `R_USERS`…`R_WATCHLIST_ITEMS` column-for-column (including 475 `holdings.reserved_quantity`, added between P2 and this phase — see 476 [RelationalDesignAIUsage](../P2-RelationalDesign/RelationalDesignAIUsage.md#session-3--2026-09-16) 477 — which is `H_RESERVED_QUANTITY` above, correctly grouped under `R_HOLDINGS`'s key alongside 478 `H_QUANTITY` and not treated as needing a relation of its own). P4's prototype 479 (`server/trade.go`, `server/portfolio.go`) keeps working against the same schema without 480 change. [RelationalDesign](../P2-RelationalDesign/RelationalDesign.md) has been updated with a 481 short note pointing here as the formal validation of its normal-form claim. 666 **The eleven data relations are the P2 design, with one difference** (`transactions.user_id`, 667 explained below). Each relation is one entity set of the ER model: 668 669 | P5 relation | P2 table | How the relationships appear | 670 |---|---|---| 671 | `R_USERS` | `users` | — | 672 | `R_CRYPTO` | `crypto` | — | 673 | `R_MARKETS` | `markets` | `C_ID` = `crypto_id` (`QuotedOn`) | 674 | `R_HOLDINGS` | `holdings` | `U_ID` = `user_id` (`Holds`), `C_ID` = `crypto_id` (`PositionIn`) | 675 | `R_ORDERS` | `orders` | `U_ID` = `user_id` (`Places`), `M_ID` = `market_id` (`PlacedOn`) | 676 | `R_TRANSACTIONS` | `transactions` | `O_ID` = `related_order` (`Settles`); P2 also stores `user_id` (`Records`), see below | 677 | `R_MARKET_TRADES` | `market_trades` | `M_ID` = `market_id` (`Fills`), `O_ID_FILLSBUY` = `buy_order_id`, `O_ID_FILLSSELL` = `sell_order_id` | 678 | `R_ORDER_EVENTS` | `order_events` | `O_ID` = `order_id` (`Logs`) | 679 | `R_MARKET_CANDLES` | `market_candles` | `M_ID` = `market_id` (`Aggregates`) | 680 | `R_WATCHLISTS` | `watchlists` | `U_ID` = `user_id` (`Owns`) | 681 | `R_WATCHLIST_ITEMS` | `watchlist_items` | `W_ID` = `watchlist_id` (`Contains`), `C_ID` = `crypto_id` (`Lists`) | 682 683 The two methods produce the foreign keys differently. In P2 they come from a transformation 684 rule: a 1:N relationship becomes a column on the N side. Here, each one appears because a 685 dependency of kind (R), for example `O_ID → U_ID`, keeps the other entity's identifier in the 686 same relation as the entity that depends on it. The candidate keys also match, including the 687 composite ones (`{C_ID, M_QUOTE_CURRENCY}`, `{U_ID, C_ID}`, `{M_ID, MC_TIMEFRAME, 688 MC_CANDLE_TIME}`, `{W_ID, C_ID}`). They are exactly the `UNIQUE` constraints in 689 [`schema_creation.sql`](../../server/db/schema_creation.sql). 690 691 **The one difference: `transactions.user_id`.** The decomposition drops `U_ID` from 692 `R_TRANSACTIONS`, because `T_ID → U_ID` follows from `T_ID → O_ID` and `O_ID → U_ID`. That is 693 correct for every ledger entry that settles an order. It does not work for a **deposit**. 694 `Settles` is partial, so a deposit has no order, and without `user_id` a deposit would have no 695 owner at all. The de-normalized relation cannot show this case. Every one of its tuples 696 contains an order (every key contains `OE_ID`, and every order event has an order), so a 697 ledger entry without an order cannot appear in it. P2 therefore keeps `user_id` (the 698 relationship `Records`) as a deliberate exception. As a result, the implemented 699 `transactions` table is in **2NF but not in 3NF** (`related_order → user_id` is a transitive 700 dependency), and this is by design. For entries with an order, 701 `transactions.user_id` repeats the order's user. The only code that sets `related_order` (the buy 702 and sell inserts in `advanced_db.sql`) writes the user and the id of the same order row. No 703 database constraint enforces this. 704 705 **Two order columns in `market_trades`.** `FillsBuy` and `FillsSell` needed two role 706 attributes already in the de-normalized relation, and both end up in `R_MARKET_TRADES`. 707 They correspond to `buy_order_id` and `sell_order_id`. 708 709 **`R_KEY` belongs to the formal result, but it is not implemented as a table.** It is the 710 relation that contains a key of `R_EDUBERZA`, and the lossless-join result above holds for all 711 12 relations *including* it. It records no fact of the domain. It only says which ledger 712 entry, order event, trade, candle and watchlist item were put into the same tuple, and that 713 combination exists only because we started from one single relation. Not implementing it is 714 an implementation decision. The eleven implemented tables are not claimed to reconstruct 715 `R_EDUBERZA` on their own. They keep every attribute and every dependency of the canonical 716 cover, and that is what the application needs. 717 718 **`holdings.avg_price`** is shown as a *derived* attribute in the ER model: it can be 719 recomputed from the buy history. It is still stored, and that is a deliberate 720 denormalisation (see [RelationalDesign](../P2-RelationalDesign/RelationalDesign.md#normalisation)). 721 Normalisation cannot detect this. `H_ID → H_AVG_PRICE` is an ordinary functional dependency, 722 because "derivable from rows of another entity" is a property of the application logic 723 that maintains the value (see [UseCase0004](../P3-UseCaseModel/UseCase0004.md), 724 `ON CONFLICT … DO UPDATE`), not a dependency between attributes of one tuple. 725 726 **Which design is used going forward:** P2's, unchanged. The eleven data relations coincide 727 with the eleven tables of [`schema_creation.sql`](../../server/db/schema_creation.sql) and 728 [`advanced_db.sql`](../../server/db/advanced_db.sql) column for column, except for the 729 deliberately kept `transactions.user_id` explained above. So there are no database objects 730 to restructure, and the prototype and the reports of P6/P7 keep working against the same 731 schema. -
docs/P5-Normalization/wiki/Normalization.md
r0cee8ec r1549dae 1 1 = Normalization = 2 2 3 This phase deliberately ignores the design from [wiki:ERModel] 4 (P1) and [wiki:RelationalDesign] (P2) as a starting 5 point. Instead it starts over from a single flat relation containing every attribute of the 6 model, derives the functional dependencies that hold on it, and decomposes it formally, 7 step by step. The 8 final section compares what falls out of that process with 9 the P2 design. 3 This phase does not use the relations of 4 [wiki:RelationalDesign] (P2) as a starting point. 5 It starts from the attributes of [wiki:ERModel] '''v05''' (P1), 6 put into one de-normalized relation. It states the functional dependencies that the 7 model's rules impose on those attributes, '''computes''' the keys of that relation from the 8 dependencies, and then decomposes it step by step through 2NF, 3NF and BCNF. Every step is 9 checked for a lossless join and for dependency preservation. The 10 final section compares the result with P2. 10 11 11 12 == De-normalized database form == 12 13 13 === Building one relation out of the whole model === 14 15 The ER model has ten entity/relationship sets carrying attributes (see 16 [wiki:ERModel]): `Users`, `Cryptos`, `Markets`, `Orders`, 17 `Transactions`, `MarketTrades`, `MarketCandles`, `Watchlists`, and the two attributed 18 relationships `Holds` and `Contains`. Eight more relationships (`QuotedOn`, `PlacedOn`, 19 `Places`, `Records`, `Settles`, `Fills`, `Aggregates`, `Owns`) carry no attributes of their 20 own — in Chen notation they need none, because the diagram expresses the link itself as a 21 relationship, not a column. A single flat relation has no such device: the only way to keep 22 one entity's rows pointed at another's is a plain attribute holding the referenced key, 23 which is exactly what P2's ER-to-relational transformation already introduces for each of 24 those eight relationships (`markets.crypto_id`, `orders.market_id`, `orders.user_id`, 25 `transactions.user_id`, `transactions.related_order`, `market_trades.market_id`, 26 `market_candles.market_id`, `watchlists.user_id`). Those linking attributes are included 27 below for that reason — not because they were copied from P2's design, but because a "single 28 table with everything in it" cannot represent the model at all without them. 29 30 Every attribute name is prefixed by a two-or-three-letter code for the entity/relationship it 31 came from, because several names repeat across the model (`id`, `created_at`, `quantity`, 32 `type`, `name`, `price`, `side` all appear more than once) and the de-normalized relation may 33 not contain duplicate names. 34 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` || 14 === Which attributes go into the relation === 15 16 The relation contains '''the attributes of the ER model and nothing else'''. In v05 all 17 attributes belong to the 11 entity sets. None of the 15 relationships has attributes of its 18 own. 19 20 A relationship adds '''no column'''. Foreign-key columns such as `crypto_id` or `watchlist_id` 21 belong to the relational model of P2, not to the ER model, so they do not appear here. What a 22 relationship contributes is a '''functional dependency''' between attributes that are already 23 in the relation. For example, `Contains` (Watchlists 1 : N !WatchlistItems) says that every 24 watchlist item is on exactly one watchlist, which is the dependency `WI_ID → W_ID` in the 25 next section. It is not a column `WI_WATCHLIST_ID`. 26 27 Attribute names are prefixed with the entity set they come from, because several names repeat 28 across the model (`id`, `created_at`, `quantity`, `type`, `name`, `price`, `side`), and one 29 relation cannot contain the same name twice. 30 31 ||= Prefix =||= Entity set (P1) =||= Attributes =|| 32 || `U_` || Users || `U_ID, U_USERNAME, U_EMAIL, U_FULL_NAME, U_PASSWORD_HASH, U_AVAILABLE_BALANCE, U_INVESTED_BALANCE, U_RESERVED_BALANCE, U_CREATED_AT, U_UPDATED_AT` || 37 33 || `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` || 46 47 `H_ID` and `WI_ID` exist for the same reason they exist in P2: `Holds` and `Contains` are M:N 48 relationships with their own attributes, and giving each its own surrogate key (rather than 49 relying solely on the `{user,crypto}` / `{watchlist,crypto}` pair) is the same design choice 50 already justified in [wiki:RelationalDesign] (section "Descriptive representation of the relational schema"). 51 52 This gives '''one relation, `R_EDUBERZA`, of 68 attributes:''' 34 || `M_` || Markets || `M_ID, M_QUOTE_CURRENCY, M_IS_ACTIVE, M_CREATED_AT` || 35 || `H_` || Holdings || `H_ID, H_QUANTITY, H_RESERVED_QUANTITY, H_AVG_PRICE, H_CREATED_AT, H_UPDATED_AT` || 36 || `O_` || Orders || `O_ID, O_SIDE, O_TYPE, O_STATUS, O_QUANTITY, O_FILLED_QUANTITY, O_PRICE, O_PLACED_AT, O_EXECUTED_AT` || 37 || `T_` || Transactions || `T_ID, T_TYPE, T_AMOUNT, T_CURRENCY, T_CREATED_AT, T_DESCRIPTION` || 38 || `MT_` || !MarketTrades || `MT_ID, MT_EXECUTED_AT, MT_PRICE, MT_QUANTITY, MT_SIDE, MT_SOURCE` || 39 || `OE_` || !OrderEvents || `OE_ID, OE_EVENT_TYPE, OE_QUANTITY, OE_PRICE, OE_STATUS_AFTER, OE_CREATED_AT` || 40 || `MC_` || !MarketCandles || `MC_ID, MC_TIMEFRAME, MC_OPEN, MC_HIGH, MC_LOW, MC_CLOSE, MC_VOLUME, MC_CANDLE_TIME` || 41 || `W_` || Watchlists || `W_ID, W_NAME, W_CREATED_AT` || 42 || `WI_` || !WatchlistItems || `WI_ID, WI_ADDED_AT` || 43 44 That is 64 attributes. '''One case needs two more.''' `FillsBuy` and `FillsSell` are two 45 different relationships between the same two entity sets, Orders and !MarketTrades. A trade 46 can fill one buy order ''and'' one sell order, which are two different orders. One relation 47 has only one `O_ID` column, and one column cannot hold two different orders in the same 48 tuple. So the order's identifier appears once per '''role''', named after the relationship 49 that gives the role: 50 51 ||= Attribute =||= Meaning =|| 52 || `O_ID_FILLSBUY` || the `id` of Orders, in its role in `FillsBuy` (the buy order a trade filled) || 53 || `O_ID_FILLSSELL` || the `id` of Orders, in its role in `FillsSell` (the sell order a trade filled) || 54 55 These are not foreign keys copied from P2. They are the ER attribute `Orders.id` itself, once 56 for each of the two relationships. [wiki:ERModel] names these two 57 roles of `Orders` explicitly: ''the buy order'' of a trade in `FillsBuy`, and ''the sell order'' in 58 `FillsSell`. This is the only place where the model has two 59 relationships between the same pair of entity sets. Every other relationship is expressed with 60 the attributes above, without renaming. 61 62 This gives '''one relation, `R_EDUBERZA`, of 66 attributes:''' 53 63 54 64 {{{ 55 65 R_EDUBERZA( 56 66 U_ID, U_USERNAME, U_EMAIL, U_FULL_NAME, U_PASSWORD_HASH, U_AVAILABLE_BALANCE, 57 U_INVESTED_BALANCE, U_ CREATED_AT, U_UPDATED_AT,67 U_INVESTED_BALANCE, U_RESERVED_BALANCE, U_CREATED_AT, U_UPDATED_AT, 58 68 C_ID, C_SYMBOL, C_NAME, C_CREATED_AT, 59 M_ID, M_CRYPTO_ID, M_QUOTE_CURRENCY, M_IS_ACTIVE, M_CREATED_AT, 60 H_ID, H_USER_ID, H_CRYPTO_ID, H_QUANTITY, H_RESERVED_QUANTITY, H_AVG_PRICE, 61 H_CREATED_AT, H_UPDATED_AT, 62 O_ID, O_USER_ID, O_MARKET_ID, O_SIDE, O_TYPE, O_STATUS, O_QUANTITY, O_PRICE, 69 M_ID, M_QUOTE_CURRENCY, M_IS_ACTIVE, M_CREATED_AT, 70 H_ID, H_QUANTITY, H_RESERVED_QUANTITY, H_AVG_PRICE, H_CREATED_AT, H_UPDATED_AT, 71 O_ID, O_SIDE, O_TYPE, O_STATUS, O_QUANTITY, O_FILLED_QUANTITY, O_PRICE, 63 72 O_PLACED_AT, O_EXECUTED_AT, 64 T_ID, T_ USER_ID, T_TYPE, T_AMOUNT, T_CURRENCY, T_RELATED_ORDER, T_CREATED_AT,65 T_DESCRIPTION,66 MT_ID, MT_MARKET_ID, MT_EXECUTED_AT, MT_PRICE, MT_QUANTITY, MT_SIDE, MT_SOURCE,67 MC_ID, MC_MARKET_ID, MC_TIMEFRAME, MC_OPEN, MC_HIGH, MC_LOW, MC_CLOSE, MC_VOLUME,68 MC_ CANDLE_TIME,69 W_ID, W_ USER_ID, W_NAME, W_CREATED_AT,70 WI_ID, WI_ WATCHLIST_ID, WI_CRYPTO_ID, WI_ADDED_AT73 T_ID, T_TYPE, T_AMOUNT, T_CURRENCY, T_CREATED_AT, T_DESCRIPTION, 74 MT_ID, MT_EXECUTED_AT, MT_PRICE, MT_QUANTITY, MT_SIDE, MT_SOURCE, 75 O_ID_FILLSBUY, O_ID_FILLSSELL, 76 OE_ID, OE_EVENT_TYPE, OE_QUANTITY, OE_PRICE, OE_STATUS_AFTER, OE_CREATED_AT, 77 MC_ID, MC_TIMEFRAME, MC_OPEN, MC_HIGH, MC_LOW, MC_CLOSE, MC_VOLUME, MC_CANDLE_TIME, 78 W_ID, W_NAME, W_CREATED_AT, 79 WI_ID, WI_ADDED_AT 71 80 ) 72 81 }}} 73 82 83 To keep the tables below readable, '''`X_*`''' means the non-identifier attributes of prefix 84 `X_`. For example, `U_*` = `U_USERNAME … U_UPDATED_AT` (9 attributes), and `O_*` = 85 `O_SIDE … O_EXECUTED_AT` (8 attributes). `U_ID`, `O_ID`, … are always written out. 86 74 87 Every attribute is single-valued and atomic (a balance, a timestamp, a symbol, an amount — 75 88 nothing here is a list or a nested record), so `R_EDUBERZA` satisfies 1NF as soon as it is 76 written down. Whether it satisfies anything beyond that is exactly what the rest of this page 89 written down. 90 91 == Functional dependencies == 92 93 At this point `R_EDUBERZA` is just a set of attributes. It has '''no keys yet'''. `U_ID`, 94 `O_ID`, … are ordinary attributes of this relation, and which attribute sets are keys of 95 `R_EDUBERZA` is computed in the next section, from the 96 dependencies below. Each dependency is justified by a rule of the domain, as described in 97 the data requirements of [wiki:ERModel]. The rules are of four 98 kinds: 99 100 * '''(I) Identification.''' Every value of an identifier (`U_ID`, `C_ID`, …) is given to exactly one real object: one user, one crypto, one order. That object has exactly one username, one balance, one price, and so on. So the identifier's value fixes those values. 101 * '''(R) 1:N relationship.''' In a 1:N relationship, each object on the N side is linked to exactly one object on the 1 side. So the N side's identifier fixes the 1 side's identifier. Example: an order is placed by exactly one user (`Places`), so `O_ID → U_ID`. The opposite direction does not hold: a user places many orders, so `U_ID ↛ O_ID`. 102 * '''(U) Uniqueness rule.''' A rule of the form "at most one X per Y and Z" gives `Y, Z → X`. 103 * '''(N) Unique natural attribute.''' No two users share a username or an email, and no two cryptos share a symbol. 104 105 '''Only rules of the ER model are used.''' The dependencies below come from the rules stated in 106 [wiki:ERModel] v05 and nothing else. The analysis uses the 107 classical definitions (Armstrong's axioms), with no special treatment of `NULL`. Partial 108 relationships (`Settles`, `FillsBuy`, `FillsSell`) are discussed where they matter: 109 under Canonical cover and in the discussion. 110 111 ||= # =||= Functional dependency =||= Rule =||= Why it holds =|| 112 || FD1 || `U_ID → U_USERNAME, U_EMAIL, U_FULL_NAME, U_PASSWORD_HASH, U_AVAILABLE_BALANCE, U_INVESTED_BALANCE, U_RESERVED_BALANCE, U_CREATED_AT, U_UPDATED_AT` || I || one user, one value of each || 113 || FD2 || `U_USERNAME → U_ID` || N || usernames are unique || 114 || FD3 || `U_EMAIL → U_ID` || N || emails are unique || 115 || FD4 || `C_ID → C_SYMBOL, C_NAME, C_CREATED_AT` || I || one crypto, one value of each || 116 || FD5 || `C_SYMBOL → C_ID` || N || symbols are unique || 117 || FD6 || `M_ID → M_QUOTE_CURRENCY, M_IS_ACTIVE, M_CREATED_AT, C_ID` || I, R || …and a market is `QuotedOn` exactly one crypto || 118 || FD7 || `C_ID, M_QUOTE_CURRENCY → M_ID` || U || a crypto is quoted at most once per currency || 119 || FD8 || `H_ID → H_QUANTITY, H_RESERVED_QUANTITY, H_AVG_PRICE, H_CREATED_AT, H_UPDATED_AT, U_ID, C_ID` || I, R || …and a holding belongs to one user (`Holds`) and is a position in one crypto (`PositionIn`) || 120 || FD9 || `U_ID, C_ID → H_ID` || U || at most one holding per user and crypto || 121 || FD10 || `O_ID → O_SIDE, O_TYPE, O_STATUS, O_QUANTITY, O_FILLED_QUANTITY, O_PRICE, O_PLACED_AT, O_EXECUTED_AT, U_ID, M_ID` || I, R || …and an order is placed by one user (`Places`) on one market (`PlacedOn`) || 122 || FD11 || `T_ID → T_TYPE, T_AMOUNT, T_CURRENCY, T_CREATED_AT, T_DESCRIPTION, U_ID, O_ID` || I, R || …and a ledger entry belongs to one user (`Records`) and to at most one order (`Settles`) || 123 || FD12 || `MT_ID → MT_EXECUTED_AT, MT_PRICE, MT_QUANTITY, MT_SIDE, MT_SOURCE, M_ID, O_ID_FILLSBUY, O_ID_FILLSSELL` || I, R || …and a trade happened on one market (`Fills`) and filled at most one buy order (`FillsBuy`) and at most one sell order (`FillsSell`) || 124 || FD13 || `OE_ID → OE_EVENT_TYPE, OE_QUANTITY, OE_PRICE, OE_STATUS_AFTER, OE_CREATED_AT, O_ID` || I, R || …and an event belongs to one order (`Logs`) || 125 || FD14 || `MC_ID → MC_TIMEFRAME, MC_OPEN, MC_HIGH, MC_LOW, MC_CLOSE, MC_VOLUME, MC_CANDLE_TIME, M_ID` || I, R || …and a candle summarises one market (`Aggregates`) || 126 || FD15 || `M_ID, MC_TIMEFRAME, MC_CANDLE_TIME → MC_ID` || U || one candle per market, timeframe and bucket || 127 || FD16 || `W_ID → W_NAME, W_CREATED_AT, U_ID` || I, R || …and a watchlist is owned by one user (`Owns`) || 128 || FD17 || `WI_ID → WI_ADDED_AT, W_ID, C_ID` || I, R || …and an item is on one watchlist (`Contains`) and names one crypto (`Lists`) || 129 || FD18 || `W_ID, C_ID → WI_ID` || U || an asset appears at most once per watchlist || 130 131 '''Dependencies that do ''not'' hold''' are as important, because they are why some attributes 132 must be combined in the key later: 133 134 * The reverse of every (R) dependency, e.g. `U_ID ↛ O_ID`, `M_ID ↛ MT_ID`, `W_ID ↛ WI_ID`. These are 1:N, not 1:1. 135 * `M_ID, MT_EXECUTED_AT ↛ MT_ID`. Two trades on a market can share a timestamp. 136 * `U_ID, W_NAME ↛ W_ID`. The model does not require list names to be unique per user. 137 * `O_ID_FILLSBUY` and `O_ID_FILLSSELL` determine no other attribute of `R_EDUBERZA` '''by any rule of the ER model'''. The order data (`O_SIDE`, `O_PRICE`, …) describes the order in the `O_ID` column, not the order in a role column. (The database also has a rule that a trade and the orders it fills are on the same market. That rule is a trigger in P7 relating several entity sets, not a rule of the ER model, so it is not used here.) 138 139 === Canonical cover === 140 141 A canonical (minimal) cover is obtained in three steps. 142 143 '''Step 1 — single attribute on the right.''' Each FD above is read as one dependency per 144 right-side attribute, e.g. FD6 is `M_ID → M_QUOTE_CURRENCY`, `M_ID → M_IS_ACTIVE`, 145 `M_ID → M_CREATED_AT`, `M_ID → C_ID`. 146 147 '''Step 2 — no extraneous attribute on the left.''' Only FD7, FD9, FD15 and FD18 have more than one 148 attribute on the left. For each one, dropping any attribute makes the rule false: 149 150 ||= FD =||= Drop =||= Counter-example (the smaller left side does not determine the right side) =|| 151 || FD7 || `M_QUOTE_CURRENCY` || BTC is quoted in USD ''and'' in EUR: one `C_ID`, two markets || 152 || || `C_ID` || USD is the quote currency of many markets || 153 || FD9 || `C_ID` || one user holds several cryptos || 154 || || `U_ID` || one crypto is held by several users || 155 || FD15 || `M_ID` || every market has a `1h` candle starting at 10:00 || 156 || || `MC_TIMEFRAME` || a market has a `1m` and a `1h` candle both starting at 10:00 || 157 || || `MC_CANDLE_TIME` || a market has many `1h` candles || 158 || FD18 || `C_ID` || a watchlist has several items || 159 || || `W_ID` || a crypto is on several watchlists || 160 161 '''Step 3 — no redundant dependency.''' A dependency is redundant if it follows from the others. For 162 almost every dependency, its right-side attribute appears on the right of no other 163 dependency with a different left side (e.g. nothing but `U_ID` determines 164 `U_AVAILABLE_BALANCE`), so it cannot be derived. The candidates worth checking are the 165 identifiers that are reached from several places: 166 167 * '''`T_ID → U_ID` is redundant.''' It follows by transitivity from `T_ID → O_ID` (FD11) and `O_ID → U_ID` (FD10): a ledger entry's user is the user of the order it settles. It is therefore '''removed''' from FD11. The derivation is valid only for an entry that has an order. Every tuple of `R_EDUBERZA` does have one (see the discussion), so in the de-normalized relation the removal is correct. The consequence for deposits, which have no order, is taken up in the discussion. 168 * `H_ID → U_ID`, `H_ID → C_ID`, `WI_ID → W_ID`, `WI_ID → C_ID`, `M_ID → C_ID`, `MT_ID → M_ID`, `MC_ID → M_ID`, `OE_ID → O_ID`, `W_ID → U_ID` and `O_ID → U_ID`, `O_ID → M_ID`: for each, no other dependency with a different left side has that attribute on its right side and a left side reachable from this one, so none can be derived. 169 * The four (U) and three (N) dependencies go "backwards" from a non-identifier to an identifier. Nothing else produces an identifier from those attributes, so they are not derivable either. 170 171 Grouping the single-attribute dependencies back by left side gives FD1–FD18 as listed, 172 except that FD11 loses `U_ID`: 173 174 ||= # =||= Functional dependency (canonical cover) =|| 175 || FD11 || `T_ID → T_TYPE, T_AMOUNT, T_CURRENCY, T_CREATED_AT, T_DESCRIPTION, O_ID` || 176 177 '''FD1–FD18, with this FD11, is the canonical cover.''' From here on, "FD11" means this reduced 178 form. 179 180 == Candidate keys and primary key == 181 182 A candidate key is a minimal set of attributes whose closure under FD1–FD18 is all 66 183 attributes. 184 185 '''Attributes that must be in every key.''' `T_ID`, `OE_ID` and `MT_ID` appear on the right side 186 of no dependency. Nothing determines them, so every key must contain them. 187 188 '''Closure of `{T_ID, OE_ID, MT_ID}`:''' 189 190 ||= Step =||= Added =||= Using =|| 191 || start || `T_ID, OE_ID, MT_ID` || — || 192 || 1 || `T_*`, `O_ID` || FD11 || 193 || 2 || `OE_*` || FD13 || 194 || 3 || `MT_*`, `M_ID`, `O_ID_FILLSBUY`, `O_ID_FILLSSELL` || FD12 || 195 || 4 || `O_*`, `U_ID` || FD10 || 196 || 5 || `U_*` || FD1 || 197 || 6 || `M_*`, `C_ID` || FD6 || 198 || 7 || `C_*` || FD4 || 199 || 8 || `H_ID` || FD9 (`U_ID` and `C_ID` are both present) || 200 || 9 || `H_*` || FD8 || 201 202 That is 53 attributes. Still missing are all 8 `MC_` attributes, the 3 `W_` attributes and 203 the 2 `WI_` attributes: 204 205 * '''`MC_`:''' only `MC_ID` determines them (FD14), and `MC_ID` is reached only by FD15, which needs `M_ID` (already present), `MC_TIMEFRAME` and `MC_CANDLE_TIME`. So the key must add either `MC_ID` or both `MC_TIMEFRAME` and `MC_CANDLE_TIME`. Neither of those two alone is enough. 206 * '''`W_` and `WI_`:''' `WI_ID` gives `W_ID` (FD17), and `W_ID` gives `WI_ID` together with `C_ID`, which is already present (FD18). So adding either `WI_ID` or `W_ID` gives all five. 207 208 '''Candidate keys''' (each one's closure is all 66 attributes, and removing any member breaks 209 that, by the argument above): 210 211 ||= Key =||= Attributes =|| 212 || '''K1''' || `T_ID, OE_ID, MT_ID, MC_ID, WI_ID` || 213 || K2 || `T_ID, OE_ID, MT_ID, MC_ID, W_ID` || 214 || K3 || `T_ID, OE_ID, MT_ID, MC_TIMEFRAME, MC_CANDLE_TIME, WI_ID` || 215 || K4 || `T_ID, OE_ID, MT_ID, MC_TIMEFRAME, MC_CANDLE_TIME, W_ID` || 216 217 '''Primary key: K1.''' It consists only of identifiers, and it is the key that remains at the 218 end of the decomposition below. 219 220 '''Prime attributes''' (in at least one candidate key): `T_ID, OE_ID, MT_ID, MC_ID, 221 MC_TIMEFRAME, MC_CANDLE_TIME, W_ID, WI_ID`. The other 58 attributes are '''non-prime'''. The 222 difference matters: 2NF and 3NF only restrict dependencies of non-prime attributes, and BCNF 223 restricts all of them. 224 225 In words, a tuple of `R_EDUBERZA` puts together one ledger entry, one order event, one 226 trade, one candle and one watchlist item. Everything else in the tuple (the user, the order, 227 the market, the crypto, the holding, the watchlist) follows from those five. 228 229 '''Normal form of `R_EDUBERZA`:''' 1NF only. It is not in 2NF, because, for example, `T_AMOUNT` 230 depends on `T_ID` alone, a proper part of K1. 231 232 == 1NF decomposition == 233 234 No decomposition is needed. Every attribute of `R_EDUBERZA` is atomic and single-valued, and 235 the relation has no repeating groups (see 236 De-normalized database form). 237 238 == 2NF decomposition == 239 240 === How every step is described and checked === 241 242 Each step of 2NF, 3NF and BCNF below lists, in this order: the relation analyzed, its 243 dependencies, its candidate keys and primary key, and its normal form; the dependency that 244 violates the next normal form and is used for the split; the two resulting relations, each 245 with its dependencies, keys and normal form; and the dependency-preservation and lossless-join 77 246 checks. 78 247 79 == Functional dependencies == 80 81 === Canonical cover === 82 83 Read directly off the model: each entity's/relationship's own key determines its own 84 attributes, nothing more. This is already minimal — no functional dependency below has an 85 extraneous attribute on its left side, and no dependent attribute is repeated on the right 86 side of more than one dependency, which is what "canonical cover" requires. 87 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? 108 `M_CRYPTO_ID` alone does not determine `M_ID` — many markets can reference the same crypto in 109 different quote currencies (that is the entire point of the market entity), so two rows can 110 share `M_CRYPTO_ID` and disagree on `M_ID`. `M_QUOTE_CURRENCY` alone fails the same way in the 111 other direction. Neither attribute is extraneous, so the left side of FD7 cannot shrink. The 112 same check applies to FD9, FD14 and FD17, whose composite left sides come directly from the 113 `UNIQUE` constraints already justified per-relation in 114 [wiki:RelationalDesign]; none of those constraints 115 holds on a proper subset of its columns either. 116 117 '''No redundant dependency:''' each of FD1–FD17 has a right side that is not implied by any 118 other dependency in the set — for instance, nothing outside FD1 mentions `U_AVAILABLE_BALANCE`, 119 so FD1 cannot be derived from the rest and cannot be dropped. This set is the canonical cover. 120 121 === Dependencies carried by foreign keys === 122 123 Six attributes above are foreign keys: `M_CRYPTO_ID`, `H_USER_ID`, `H_CRYPTO_ID`, 124 `O_USER_ID`, `O_MARKET_ID`, `T_USER_ID`, `T_RELATED_ORDER`, `MT_MARKET_ID`, `MC_MARKET_ID`, 125 `W_USER_ID`, `WI_WATCHLIST_ID`, `WI_CRYPTO_ID` — each one draws its values from the same 126 domain as some other attribute's key. Because of that, every dependency that holds on the 127 referenced key also holds, by substitution, on the referencing attribute: 128 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 144 transitivity plus the foreign-key identity, which is exactly why a canonical cover excludes 145 them. They matter anyway: they are precisely the transitive dependencies the 3NF check below 146 has to rule out. 147 148 == Candidate keys and primary key == 149 150 `Orders`, `Transactions`, `MarketTrades`, `MarketCandles`, `Holds`, `Watchlists` and 151 `Contains` are, with respect to each other, independent record types: nothing about an 152 order's id says anything about which market-candle row, or which unrelated transaction, or 153 which watchlist item is in the same tuple of `R_EDUBERZA` — a user can exist with zero of any 154 of them, and having one order says nothing about how many holdings, trades or candles exist 155 alongside it. (The one FK that crosses between two of these — `T_RELATED_ORDER` — is 156 nullable, so it cannot be relied on to always connect a transaction row back to an order.) 157 That means no proper subset of attributes can functionally determine all 68 attributes of 158 `R_EDUBERZA`: the only way to pin down a `H_*` value, an `O_*` value, a `T_*` value, an 159 `MT_*` value, an `MC_*` value, a `W_*` value ''and'' a `WI_*` value at once is to state one 160 identifying attribute from each cluster explicitly. 161 162 '''Chosen primary key''' (closure shown below): 163 164 {{{ 165 { U_ID, C_ID, M_ID, H_ID, O_ID, T_ID, MT_ID, MC_ID, W_ID, WI_ID } 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 || 182 183 The closure now contains all 68 attributes, so the set is a superkey; removing any one of its 184 ten attributes drops an entire cluster that nothing else in the set can reach (e.g. drop 185 `T_ID` and no remaining attribute determines any `T_*` value), so it is minimal — a candidate 186 key. 187 188 '''It is not the only one.''' Any attribute that is itself a determinant of a whole cluster can 189 stand in for that cluster's id — `U_USERNAME` or `U_EMAIL` for `U_ID` (FD2/FD3), `C_SYMBOL` 190 for `C_ID` (FD5), `{M_CRYPTO_ID, M_QUOTE_CURRENCY}` for `M_ID` (FD7), `{H_USER_ID, H_CRYPTO_ID}` for `H_ID` (FD9), `{MC_MARKET_ID, MC_TIMEFRAME, MC_CANDLE_TIME}` for `MC_ID` 191 (FD14), `{WI_WATCHLIST_ID, WI_CRYPTO_ID}` for `WI_ID` (FD17) — giving 3 × 2 × 2 × 2 × 1 × 1 × 192 1 × 2 × 1 × 2 = 96 candidate keys in total. The all-surrogate-id combination above is chosen 193 as '''primary key''' for the same reason `id` was chosen over `username`/`email`/`symbol`/etc. 194 per entity in [wiki:ERModel]: it is opaque, and none of its parts 195 are things a user would ever legitimately change. 196 197 '''Normal form of `R_EDUBERZA` before decomposition:''' 1NF only, and barely that — see 2NF 198 below. It cannot be in 2NF, 3NF or BCNF, since each of those requires 2NF as a precondition. 199 200 == 1NF decomposition == 201 202 No decomposition happens at this step. 1NF requires atomic, single-valued attributes and no 203 repeating groups; `R_EDUBERZA` was built that way from the start (every column above is a 204 single scalar), so the relation already satisfies 1NF as written in 205 De-normalized database form. The real work starts at 2NF. 206 207 == 2NF decomposition == 208 209 '''Relation analyzed:''' `R_EDUBERZA`, all 68 attributes, primary key 210 `{U_ID, C_ID, M_ID, H_ID, O_ID, T_ID, MT_ID, MC_ID, W_ID, WI_ID}` (10 attributes), FD1–FD17 211 in force. 212 213 '''Current normal form:''' 1NF only (previous section). 214 215 '''Violations:''' 2NF forbids a non-prime attribute from depending on ''part'' of a candidate 216 key. Every single functional dependency in the canonical cover (FD1–FD17) has a left side 217 that is a '''proper subset''' of the ten-attribute primary key — `U_ID` alone, `C_ID` alone, …, 218 down to the two-attribute `{WI_WATCHLIST_ID, WI_CRYPTO_ID}`. There is no non-prime attribute 219 in `R_EDUBERZA` that depends on the whole ten-attribute key and nothing smaller. In other 220 words, ''every'' non-prime attribute violates 2NF at once — the violation is not a handful of 221 stray columns to peel off, it is the entire relation, because gluing ten independent record 222 types together under one artificial composite key was never going to satisfy 2NF to begin 223 with. 224 225 '''Decomposition.''' This uses 3NF/BCNF '''synthesis''' (Bernstein's algorithm) rather than the 226 binary decomposition algorithm: since the canonical cover is already in hand (as the phase 227 instructions recommend building first), synthesis creates one relation per left-hand side in 228 the cover directly, instead of hunting for one offending dependency at a time and splitting 229 in two repeatedly. Grouping FD1–FD17 by determinant produces ten relations: 230 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 245 determining, so 2NF holds trivially in each). 246 247 '''Dependency preservation.''' FD1–FD17 is the canonical cover of `R_EDUBERZA`. Each FD's 248 determinant and every one of its dependent attributes land inside exactly one of the ten new 249 relations (see the "Source FDs" column above — no FD is split across two relations). The 250 union of the FDs that hold on `R_USERS, …, R_WATCHLIST_ITEMS` is therefore exactly FD1–FD17 251 again: nothing was lost. 252 253 '''Lossless join — chase test.''' 254 255 > ''Note: the chase algorithm is not part of the course material. I was curious about a stricter way to test lossless join than the usual "the common attributes are a key of one side" argument, so I applied it here.'' 256 257 The chase decides whether a decomposition `R = R1 ∪ … ∪ Rn` is lossless under a set of 258 functional dependencies. Build a tableau with one column per attribute of `R` and one row per 259 relation `Ri`. In row `i`, put a distinguished symbol `a` in every column of `Ri` and a unique 260 symbol `b_i` in every other column. Then repeat, until nothing changes: for each FD `X → Y`, 261 whenever two rows agree on all of `X`, make them agree on `Y`. If they disagree, an `a` wins, 262 otherwise one `b` replaces the other. '''The decomposition is lossless exactly when some row ends up with `a` in every column.''' 263 264 All attributes of one cluster (`U_*`, `C_*`, `M_*`, …) always appear together, and FD1–FD17 never mix clusters. So each cluster is one column group below: `a` means every column of the group holds a distinguished symbol, and `b` means none of them does. The foreign-key attributes (`H_USER_ID`, `O_MARKET_ID`, …) belong to their own cluster (`H_*`, `O_*`, …), not to the cluster they reference. 265 266 '''Step 1 — the ten relations from the table above.''' 267 268 {{{ 269 U* C* M* H* O* T* MT* MC* W* WI* 270 R_USERS a b b b b b b b b b 271 R_CRYPTO b a b b b b b b b b 272 R_MARKETS b b a b b b b b b b 273 R_HOLDINGS b b b a b b b b b b 274 R_ORDERS b b b b a b b b b b 275 R_TRANSACTIONS b b b b b a b b b b 276 R_MARKET_TR. b b b b b b a b b b 277 R_MARKET_CA. b b b b b b b a b b 278 R_WATCHLISTS b b b b b b b b a b 279 R_WATCHLIST_I. b b b b b b b b b a 280 }}} 281 282 Every FD has its left side inside one cluster, for example `U_ID → U_*` or `H_USER_ID, H_CRYPTO_ID → H_ID`. For such an FD to fire, two rows would have to agree on that left side. But only one row has `a`s in that cluster, and the `b`s of different rows are all different, so no two rows ever agree on any left side. ''*The chase changes nothing, and no row becomes all `a`.'''' Under FD1–FD17 alone, the ten relations are ''not'' guaranteed to join back to `R_EDUBERZA`. This is not an accident of this model. It is exactly why Bernstein's synthesis algorithm has a final step: *if no synthesised relation contains a candidate key of `R`, add one that does.'' None of the ten contains the ten-attribute key. 283 284 '''Step 2 — add the key relation''' `R_KEY(U_ID, C_ID, M_ID, H_ID, O_ID, T_ID, MT_ID, MC_ID, 285 W_ID, WI_ID)`. Its row has `a` only in the ten ID columns, written `a·` for "`a` in the ID, 286 `b` in the rest of the group": 287 288 {{{ 289 U* C* M* H* O* T* MT* MC* W* WI* 290 R_KEY a· a· a· a· a· a· a· a· a· a· 291 (the ten rows of step 1 unchanged) 292 }}} 293 294 Now FD1 `U_ID → U_*` fires: row `R_KEY` and row `R_USERS` both have `a` in `U_ID`, so they must agree on the rest of `U_*`, and `R_USERS` has `a` there. `R_KEY` becomes `a` in the whole 295 `U*` group. The same happens with FD4 (`C*`), FD6 (`M*`), FD8 (`H*`), FD10 (`O*`), FD11 (`T*`), 296 FD12 (`MT*`), FD13 (`MC*`), FD15 (`W*`) and FD16 (`WI*`): 297 298 {{{ 299 U* C* M* H* O* T* MT* MC* W* WI* 300 R_KEY a a a a a a a a a a <- all distinguished 301 }}} 302 303 '''Row `R_KEY` is all `a`, so the decomposition into the ten relations plus `R_KEY` is lossless.''' 304 305 '''Why `R_KEY` is not kept in the final schema.''' An instance of `R_KEY` would only record 306 which ID of one cluster appears together with which ID of every other cluster. As shown under 307 ''Candidate keys and primary key'', the ten clusters are independent record types, and 308 `R_EDUBERZA` pairs every row of one with every row of the others. So `R_KEY` would be just the 309 cross product of the ten ID sets and would carry no information. The same independence means 310 the join dependency `⋈[R_USERS, …, R_WATCHLIST_ITEMS]` holds on `R_EDUBERZA` by construction. 311 Under that dependency the ten relations alone already reconstruct it: their natural join, with 312 no common attributes, is exactly that cross product. The chase makes this reasoning explicit. 313 FDs by themselves cannot prove the join lossless; you need either the key relation or the 314 independence of the clusters. That was hidden in the earlier "foreign key equals primary key" 315 argument, which described the equi-joins the application runs, not the natural join the 316 lossless-join property is about. 248 Every step splits one relation `R` into two: the '''extracted''' relation `Ri` and the 249 '''residual''' relation `R'` (what is left of `R`). The same two checks are made each time: 250 251 * '''Lossless join.''' The split of `R` into `Ri` and `R'` is lossless if the common attributes determine one of the two sides: `(Ri ∩ R') → Ri` or `(Ri ∩ R') → R'`. Every step below extracts `Ri = X ∪ (what X determines)` for some determinant `X` that stays in `R'`. So `X ⊆ Ri ∩ R'` and `X → Ri`, and the first condition holds. 252 * '''Dependency preservation.''' Every dependency of the canonical cover must end up with all its attributes inside one relation. So an attribute is removed from the residual only when no dependency still waiting in the residual needs it. Otherwise it is extracted '''and''' kept. 253 254 '''Relation analyzed first:''' `R_EDUBERZA` (66 attributes), dependencies FD1–FD18, candidate 255 keys K1–K4, primary key K1. '''Normal form:''' 1NF. 256 257 '''Dependencies that violate 2NF.''' 2NF forbids a non-prime attribute from depending on a proper 258 part of a candidate key. There are six such partial dependencies: 259 260 ||= Part of a key =||= Non-prime attributes that depend on it =||= Through =|| 261 || `T_ID` (K1–K4) || `T_*`, `O_ID`, and through them `O_*`, `U_ID`, `U_*`, `M_ID`, `M_*`, `C_ID`, `C_*`, `H_ID`, `H_*` || FD11, then FD10, FD1, FD6, FD4, FD9, FD8 || 262 || `OE_ID` (K1–K4) || `OE_*`, `O_ID` || FD13 || 263 || `MT_ID` (K1–K4) || `MT_*`, `M_ID`, `O_ID_FILLSBUY`, `O_ID_FILLSSELL` || FD12 || 264 || `MC_ID` (K1, K2) || `MC_OPEN, MC_HIGH, MC_LOW, MC_CLOSE, MC_VOLUME`, `M_ID` || FD14 || 265 || `W_ID` (K2, K4) || `W_*`, `U_ID` || FD16 || 266 || `WI_ID` (K1, K3) || `WI_ADDED_AT`, `C_ID` || FD17 || 267 268 The table lists the part of a key that each group depends on most directly. It is not the 269 only one: under K3/K4, for example, `MC_OPEN … MC_VOLUME` also depend on 270 `{MT_ID, MC_TIMEFRAME, MC_CANDLE_TIME}`, and under K1/K3 `W_*` depend on `WI_ID` through 271 `W_ID`. These lead to the same relations, so they need no extra steps. `MC_TIMEFRAME`, 272 `MC_CANDLE_TIME` and `W_ID` also depend on parts of keys, but they are prime, so 2NF does not 273 restrict them. They are handled under BCNF. 274 275 Each step below removes one row of this table, splitting the current relation into two. The 276 '''order''' is chosen so that no dependency is lost. `T_ID` goes first, because its group is the 277 largest and carries FD1–FD11 with it. Each later step handles a group whose determinant is 278 still in the residual relation. 279 280 === Step 2NF-1 — partial dependency on `T_ID` === 281 282 * '''Relation analyzed:''' `R_EDUBERZA` (66 attributes). 283 * '''Dependencies:''' FD1–FD18. '''Candidate keys:''' K1–K4. '''Primary key:''' K1. '''Normal form:''' 1NF. 284 * '''2NF violations:''' all six rows of the table above. '''Split first on `T_ID`''', the largest group (see the order explained above). 285 * '''Decomposition dependency:''' `T_ID → T_*, O_ID` (FD11), together with everything it determines transitively (FD10, FD1, FD6, FD4, FD9, FD8). `T_ID` is a proper part of K1, and `T_AMOUNT`, for example, is non-prime, so this violates 2NF. 286 * '''New relation `R_A`''' = `{ T_ID, T_*, O_ID, O_*, U_ID, U_*, M_ID, M_*, C_ID, C_*, H_ID, H_* }` (39 attributes). Dependencies: FD1–FD11. Candidate key and primary key: `T_ID`. Normal form: 2NF (it has a one-attribute key), but not 3NF (see 3NF). 287 * '''Residual relation `S1`''' = `R_EDUBERZA − { T_*, O_*, U_*, M_*, C_*, H_ID, H_* }` = `{ T_ID, O_ID, U_ID, M_ID, C_ID, OE_ID, OE_*, MT_ID, MT_*, O_ID_FILLSBUY, O_ID_FILLSSELL, MC_ID, MC_*, W_ID, W_*, WI_ID, WI_ADDED_AT }` (32 attributes). `O_ID`, `U_ID`, `M_ID` and `C_ID` stay, because FD13, FD16, FD12/FD14/FD15 and FD17/FD18 still need them. Dependencies: FD12–FD18, plus the projected dependencies between the identifiers kept here: `T_ID → O_ID, U_ID, M_ID, C_ID`, `O_ID → U_ID, M_ID, C_ID`, `M_ID → C_ID`, `OE_ID → U_ID, M_ID, C_ID`, `MT_ID → C_ID`, `MC_ID → C_ID`, `WI_ID → U_ID`. Candidate keys: K1–K4 (all their attributes are still here). Normal form: 1NF. 288 * '''Dependency preservation:''' FD1–FD11 lie entirely in `R_A`, and FD12–FD18 entirely in `S1`. ✓ 289 * '''Lossless join:''' `R_A ∩ S1 = { T_ID, O_ID, U_ID, M_ID, C_ID }` contains `T_ID`, and `T_ID → R_A`, so `(R_A ∩ S1) → R_A`. ✓ 290 291 === Step 2NF-2 — partial dependency on `OE_ID` === 292 293 * '''Relation analyzed:''' `S1` (32 attributes). Dependencies: as listed for `S1` in the previous step. Candidate keys: K1–K4. Primary key: K1. Normal form: 1NF. 294 * '''Remaining 2NF violations:''' the partial dependencies on `OE_ID`, `MT_ID`, `MC_ID`, `W_ID` and `WI_ID` (table above), and the partial dependencies of the kept identifiers `O_ID`, `U_ID`, `M_ID`, `C_ID` on `T_ID`. The kept identifiers cannot leave yet, because other groups still need them. Each one leaves with the last group that needs it (`O_ID` in 2NF-2, `M_ID` in 2NF-4, `U_ID` in 2NF-5, `C_ID` in 2NF-6). '''Split first on `OE_ID`''', because after it no group needs `O_ID` any more. 295 * '''Decomposition dependency:''' `OE_ID → OE_*, O_ID` (FD13). `OE_ID` is a proper part of K1 and `OE_*` are non-prime. 296 * '''New relation `R_B`''' = `{ OE_ID, OE_*, O_ID }` (7 attributes). Dependencies: FD13. Candidate key: `OE_ID`. Normal form: BCNF. 297 * '''Residual relation `S2`''' = `S1 − { OE_*, O_ID }` (26 attributes). No dependency still needed in the residual uses `O_ID`. Dependencies: FD12, FD14–FD18, plus the projected `T_ID → U_ID, M_ID, C_ID`, `OE_ID → U_ID, M_ID, C_ID`, `M_ID → C_ID`, `MT_ID → C_ID`, `MC_ID → C_ID`, `WI_ID → U_ID`. Candidate keys: K1–K4. Normal form: 1NF. 298 * '''Dependency preservation:''' FD13 is in `R_B`, and the others are in `S2`. `T_ID → O_ID` is already kept in `R_A`. ✓ 299 * '''Lossless join:''' `R_B ∩ S2 = { OE_ID }`, and `OE_ID → R_B` (FD13). ✓ 300 301 === Step 2NF-3 — partial dependency on `MT_ID` === 302 303 * '''Relation analyzed:''' `S2` (26 attributes). Dependencies: as listed for `S2` in the previous step. Candidate keys: K1–K4. Primary key: K1. Normal form: 1NF. 304 * '''Remaining 2NF violations:''' the groups of `MT_ID`, `MC_ID`, `W_ID`, `WI_ID`, and the kept identifiers `U_ID`, `M_ID`, `C_ID`. '''Split first on `MT_ID`''', the next group. `M_ID` must still stay for `MC_ID`. 305 * '''Decomposition dependency:''' `MT_ID → MT_*, M_ID, O_ID_FILLSBUY, O_ID_FILLSSELL` (FD12). 306 * '''New relation `R_C`''' = `{ MT_ID, MT_*, M_ID, O_ID_FILLSBUY, O_ID_FILLSSELL }` (9 attributes). Dependencies: FD12. Candidate key: `MT_ID`. Normal form: BCNF. 307 * '''Residual relation `S3`''' = `S2 − { MT_*, O_ID_FILLSBUY, O_ID_FILLSSELL }` (19 attributes). `M_ID` stays, because FD14/FD15 need it. Dependencies: FD14–FD18, plus the projected `T_ID → U_ID, M_ID, C_ID`, `OE_ID → U_ID, M_ID, C_ID`, `MT_ID → M_ID, C_ID`, `M_ID → C_ID`, `MC_ID → C_ID`, `WI_ID → U_ID`. Candidate keys: K1–K4. Normal form: 1NF. 308 * '''Dependency preservation:''' FD12 is in `R_C`, and FD14–FD18 are in `S3`. ✓ 309 * '''Lossless join:''' `R_C ∩ S3 = { MT_ID, M_ID }` contains `MT_ID`, and `MT_ID → R_C` (FD12). ✓ 310 311 === Step 2NF-4 — partial dependency on `MC_ID` === 312 313 * '''Relation analyzed:''' `S3` (19 attributes). Dependencies: as listed for `S3` in the previous step. Candidate keys: K1–K4. Primary key: K1. Normal form: 1NF. 314 * '''Remaining 2NF violations:''' the groups of `MC_ID`, `W_ID`, `WI_ID`, and the kept identifiers `U_ID`, `M_ID`, `C_ID`. '''Split first on `MC_ID`''', the last group that needs `M_ID`, so `M_ID` can leave with it. 315 * '''Decomposition dependency:''' `MC_ID → MC_OPEN, MC_HIGH, MC_LOW, MC_CLOSE, MC_VOLUME, M_ID` (FD14). `MC_ID` is a proper part of K1. The prime `MC_TIMEFRAME` and `MC_CANDLE_TIME` also go into the new relation, so that FD15, which needs them with `M_ID` and `MC_ID`, is preserved. 316 * '''New relation `R_D`''' = `{ MC_ID, MC_TIMEFRAME, MC_OPEN, MC_HIGH, MC_LOW, MC_CLOSE, MC_VOLUME, MC_CANDLE_TIME, M_ID }` (9 attributes). Dependencies: FD14, FD15. Candidate keys: `MC_ID` and `{M_ID, MC_TIMEFRAME, MC_CANDLE_TIME}`. Normal form: BCNF. 317 * '''Residual relation `S4`''' = `S3 − { MC_OPEN, MC_HIGH, MC_LOW, MC_CLOSE, MC_VOLUME, M_ID }` (13 attributes). `MC_TIMEFRAME` and `MC_CANDLE_TIME` are prime and stay. Dependencies: FD16–FD18, plus the projected `T_ID → U_ID, C_ID`, `OE_ID → U_ID, C_ID`, `MT_ID → C_ID`, `MC_ID → MC_TIMEFRAME, MC_CANDLE_TIME, C_ID`, `WI_ID → U_ID`. Candidate keys: K1–K4. Normal form: 1NF. 318 * '''Dependency preservation:''' FD14 and FD15 are in `R_D`, and FD16–FD18 are in `S4`. ✓ 319 * '''Lossless join:''' `R_D ∩ S4 = { MC_ID, MC_TIMEFRAME, MC_CANDLE_TIME }` contains `MC_ID`, and `MC_ID → R_D` (FD14). ✓ 320 321 === Step 2NF-5 — partial dependency on `W_ID` === 322 323 * '''Relation analyzed:''' `S4` (13 attributes). Dependencies: as listed for `S4` in the previous step. Candidate keys: K1–K4. Primary key: K1. Normal form: 1NF. 324 * '''Remaining 2NF violations:''' the groups of `W_ID` and `WI_ID`, and the kept identifiers `U_ID`, `C_ID`. '''Split first on `W_ID`''', the last group that needs `U_ID`. 325 * '''Decomposition dependency:''' `W_ID → W_NAME, W_CREATED_AT, U_ID` (FD16). `W_ID` is a proper part of K2. 326 * '''New relation `R_E`''' = `{ W_ID, W_NAME, W_CREATED_AT, U_ID }` (4 attributes). Dependencies: FD16. Candidate key: `W_ID`. Normal form: BCNF. 327 * '''Residual relation `S5`''' = `S4 − { W_NAME, W_CREATED_AT, U_ID }` (10 attributes). Dependencies: FD17, FD18, plus the projected `T_ID → C_ID`, `OE_ID → C_ID`, `MT_ID → C_ID`, `MC_ID → MC_TIMEFRAME, MC_CANDLE_TIME, C_ID`. Candidate keys: K1–K4. Normal form: 1NF. 328 * '''Dependency preservation:''' FD16 is in `R_E`, and FD17 and FD18 are in `S5`. ✓ 329 * '''Lossless join:''' `R_E ∩ S5 = { W_ID }`, and `W_ID → R_E` (FD16). ✓ 330 331 === Step 2NF-6 — partial dependency on `WI_ID` === 332 333 * '''Relation analyzed:''' `S5` (10 attributes). Dependencies: as listed for `S5` in the previous step. Candidate keys: K1–K4. Primary key: K1. Normal form: 1NF. 334 * '''Remaining 2NF violations:''' the group of `WI_ID`, and the kept identifier `C_ID`. '''Split on `WI_ID`''', the last group that needs `C_ID`. 335 * '''Decomposition dependency:''' `WI_ID → WI_ADDED_AT, W_ID, C_ID` (FD17). `WI_ID` is a proper part of K1, and `WI_ADDED_AT` and `C_ID` are non-prime. 336 * '''New relation `R_F`''' = `{ WI_ID, WI_ADDED_AT, W_ID, C_ID }` (4 attributes). Dependencies: FD17, FD18. Candidate keys: `WI_ID` and `{W_ID, C_ID}`. Normal form: BCNF. 337 * '''Residual relation `S6`''' = `S5 − { WI_ADDED_AT, C_ID }` = `{ T_ID, OE_ID, MT_ID, MC_ID, MC_TIMEFRAME, MC_CANDLE_TIME, W_ID, WI_ID }` (8 attributes). `W_ID` is prime and stays. Dependencies: no dependency of the cover lies entirely inside `S6`. The projected ones are `MC_ID → MC_TIMEFRAME, MC_CANDLE_TIME` and `WI_ID → W_ID`, plus derived ones such as `MT_ID, MC_TIMEFRAME, MC_CANDLE_TIME → MC_ID` and `MT_ID, W_ID → WI_ID`. Candidate keys: K1–K4. Normal form: 3NF, because every attribute is prime (and so 2NF). 338 * '''Dependency preservation:''' FD17 and FD18 are in `R_F`. ✓ 339 * '''Lossless join:''' `R_F ∩ S6 = { WI_ID, W_ID }` contains `WI_ID`, and `WI_ID → R_F` (FD17). ✓ 340 341 '''Result of 2NF:''' `R_A`, `R_B`, `R_C`, `R_D`, `R_E`, `R_F`, `S6`. All seven are in 2NF (`R_A` 342 only 2NF, `S6` 3NF, the rest BCNF). All 18 dependencies are preserved: FD1–FD11 in `R_A`, 343 FD13 in `R_B`, FD12 in `R_C`, FD14–FD15 in `R_D`, FD16 in `R_E`, FD17–FD18 in `R_F`. 317 344 318 345 == 3NF decomposition == 319 346 320 '''Relations analyzed:''' each of the ten relations produced above, individually. 321 322 For each relation, 3NF asks whether any non-prime attribute is ''transitively'' dependent on a 323 key — i.e. determined by another non-prime attribute rather than directly by the key. This is 324 exactly where the foreign-key-carried dependencies from 325 Dependencies carried by foreign keys have to be 326 checked, because that table is precisely the list of "dependency that would cause a problem at 327 the next higher normal form" the phase template asks for. 328 329 '''Worked example — `R_MARKETS`.''' Its key `M_ID` determines `M_CRYPTO_ID`, and 330 `M_CRYPTO_ID → C_SYMBOL, C_NAME, C_CREATED_AT` also holds (`M_CRYPTO_ID` draws its values from 331 `C_ID`'s domain). If `C_SYMBOL`, `C_NAME` and `C_CREATED_AT` were still columns of 332 `R_MARKETS`, this would be exactly the transitive dependency `M_ID → M_CRYPTO_ID → C_SYMBOL` 333 that violates 3NF. They are not: the 2NF step above already put them in `R_CRYPTO`, keyed 334 directly by `C_ID` (FD4), because FD4 — not the derived `M_CRYPTO_ID → C_SYMBOL` — is what the 335 canonical cover actually contains. `R_MARKETS` itself has no attribute that determines another 336 non-prime attribute of `R_MARKETS`; the transitive dependency is real, but it points ''out'' of 337 the relation, not within it. 338 339 The same reasoning applies to every other foreign key in the list: `H_USER_ID`/`H_CRYPTO_ID`, 340 `O_USER_ID`/`O_MARKET_ID`, `T_USER_ID`/`T_RELATED_ORDER`, `MT_MARKET_ID`, `MC_MARKET_ID`, 341 `W_USER_ID`, `WI_WATCHLIST_ID`/`WI_CRYPTO_ID` are all foreign keys sitting ''alongside'' a 342 non-key attribute set that depends only on their own relation's key, never on the foreign key 343 itself. None of `R_USERS`, `R_CRYPTO`, `R_HOLDINGS`, `R_ORDERS`, `R_TRANSACTIONS`, 344 `R_MARKET_TRADES`, `R_MARKET_CANDLES`, `R_WATCHLISTS`, `R_WATCHLIST_ITEMS` has a non-prime 345 attribute that another non-prime attribute of the ''same'' relation determines. 346 347 '''Conclusion:''' synthesising directly from the canonical cover in the 2NF step already 348 avoided every transitive dependency — there is nothing left to decompose for 3NF. All ten 349 relations from the previous section satisfy 3NF unchanged. 347 Only `R_A` is not in 3NF. `R_B`–`R_F` are already in BCNF, and `S6` is in 3NF (all its 348 attributes are prime). 349 350 '''Dependencies that violate 3NF in `R_A`.''' 3NF forbids a non-prime attribute from depending on 351 a key only '''transitively''', through a determinant that is not a superkey. The only key of 352 `R_A` is `T_ID`, but inside `R_A`: 353 354 * `U_ID → U_*` (FD1), `U_USERNAME → U_ID` (FD2), `U_EMAIL → U_ID` (FD3) 355 * `C_ID → C_*` (FD4), `C_SYMBOL → C_ID` (FD5) 356 * `U_ID, C_ID → H_ID` (FD9), `H_ID → H_*, U_ID, C_ID` (FD8) 357 * `M_ID → M_*, C_ID` (FD6), `C_ID, M_QUOTE_CURRENCY → M_ID` (FD7) 358 * `O_ID → O_*, U_ID, M_ID` (FD10) 359 360 None of these determinants is a superkey of `R_A`. For example, `T_ID → O_ID → O_PRICE` is a 361 transitive dependency of the non-prime `O_PRICE` on the key. 362 363 '''Order of the steps.''' An attribute can leave the residual only after every dependency that 364 needs it has been extracted. FD9 needs `U_ID` and `C_ID` together, and extracting `Markets` 365 takes `C_ID` out of the residual, so `Holdings` must come before `Markets`. Extracting 366 `Orders` takes `M_ID` and `U_ID` out, so `Orders` comes last. The dependencies are therefore 367 taken from the "leaves" of the chain `T_ID → O_ID → {U_ID, M_ID → C_ID}` inward. 368 369 === Step 3NF-1 — transitive dependency through `U_ID` === 370 371 * '''Relation analyzed:''' `R_A` (39 attributes), dependencies FD1–FD11, candidate key and primary candidate key and primary key `T_ID`, normal form 2NF. 372 * '''3NF violations:''' all five groups listed above. '''Split first on `U_ID`'''. It is a leaf of the chain: its dependents determine nothing outside its own group. 373 * '''Decomposition dependency:''' `U_ID → U_*` (FD1). `U_ID` is not a superkey of `R_A`. 374 * '''New relation `R_USERS`''' = `{ U_ID, U_* }` (10 attributes). Dependencies: FD1, FD2, FD3. Candidate keys: `U_ID`, `U_USERNAME`, `U_EMAIL`. Primary key: `U_ID`. Normal form: BCNF. 375 * '''Residual relation `R_A1`''' = `R_A − U_*` (30 attributes). Dependencies: FD4–FD11, which also imply `T_ID → H_ID` and `O_ID → H_ID` (through `U_ID, C_ID`). Candidate key and primary key: `T_ID`. Normal form: 2NF. 376 * '''Dependency preservation:''' FD1–FD3 are in `R_USERS`, and FD4–FD11 are in `R_A1`. ✓ 377 * '''Lossless join:''' `R_USERS ∩ R_A1 = { U_ID }`, and `U_ID → R_USERS` (FD1). ✓ 378 379 === Step 3NF-2 — transitive dependency through `C_ID` === 380 381 * '''Relation analyzed:''' `R_A1` (30 attributes), dependencies FD4–FD11, candidate key and primary key `T_ID`, normal form 2NF. 382 * '''3NF violations:''' `C_ID → C_*`, `U_ID, C_ID → H_ID → H_*`, `M_ID → M_*, C_ID`, `O_ID → O_*, U_ID, M_ID`. '''Split first on `C_ID`''', the next leaf. 383 * '''Decomposition dependency:''' `C_ID → C_*` (FD4). 384 * '''New relation `R_CRYPTO`''' = `{ C_ID, C_* }` (4 attributes). Dependencies: FD4, FD5. Candidate keys: `C_ID`, `C_SYMBOL`. Primary key: `C_ID`. Normal form: BCNF. 385 * '''Residual relation `R_A2`''' = `R_A1 − C_*` (27 attributes). Dependencies: FD6–FD11. Candidate key and primary key: `T_ID`. Normal form: 2NF. 386 * '''Dependency preservation:''' FD4 and FD5 are in `R_CRYPTO`, and FD6–FD11 are in `R_A2`. ✓ 387 * '''Lossless join:''' `R_CRYPTO ∩ R_A2 = { C_ID }`, and `C_ID → R_CRYPTO` (FD4). ✓ 388 389 === Step 3NF-3 — transitive dependency through `{U_ID, C_ID}` === 390 391 * '''Relation analyzed:''' `R_A2` (27 attributes), dependencies FD6–FD11, candidate key and primary key `T_ID`, normal form 2NF. 392 * '''3NF violations:''' `U_ID, C_ID → H_ID → H_*`, `M_ID → M_*, C_ID`, `O_ID → O_*, U_ID, M_ID`. '''Split first on `{U_ID, C_ID}`''', because it must come before `Markets` takes `C_ID` away. 393 * '''Decomposition dependency:''' `U_ID, C_ID → H_ID` (FD9), together with `H_ID → H_*` (FD8). 394 * '''New relation `R_HOLDINGS`''' = `{ H_ID, H_*, U_ID, C_ID }` (8 attributes). Dependencies: FD8, FD9. Candidate keys: `H_ID`, `{U_ID, C_ID}`. Primary key: `H_ID`. Normal form: BCNF. 395 * '''Residual relation `R_A3`''' = `R_A2 − { H_ID, H_* }` (21 attributes). Dependencies: FD6, FD7, FD10, FD11. Candidate key and primary key: `T_ID`. Normal form: 2NF. 396 * '''Dependency preservation:''' FD8 and FD9 are in `R_HOLDINGS`, and the others are in `R_A3`. ✓ 397 * '''Lossless join:''' `R_HOLDINGS ∩ R_A3 = { U_ID, C_ID }`, and `U_ID, C_ID → H_ID → H_*`, so `{U_ID, C_ID} → R_HOLDINGS`. ✓ 398 399 === Step 3NF-4 — transitive dependency through `M_ID` === 400 401 * '''Relation analyzed:''' `R_A3` (21 attributes), dependencies FD6, FD7, FD10, FD11, key `T_ID`, normal form 2NF. 402 * '''3NF violations:''' `M_ID → M_*, C_ID` and `O_ID → O_*, U_ID, M_ID`. '''Split first on `M_ID`''', because `Orders` still needs `M_ID`. 403 * '''Decomposition dependency:''' `M_ID → M_*, C_ID` (FD6). 404 * '''New relation `R_MARKETS`''' = `{ M_ID, M_*, C_ID }` (5 attributes). Dependencies: FD6, FD7. Candidate keys: `M_ID`, `{C_ID, M_QUOTE_CURRENCY}`. Primary key: `M_ID`. Normal form: BCNF. 405 * '''Residual relation `R_A4`''' = `R_A3 − { M_*, C_ID }` (17 attributes). No dependency left needs `C_ID`. Dependencies: FD10, FD11. Candidate key and primary key: `T_ID`. Normal form: 2NF. 406 * '''Dependency preservation:''' FD6 and FD7 are in `R_MARKETS`, and FD10 and FD11 are in `R_A4`. ✓ 407 * '''Lossless join:''' `R_MARKETS ∩ R_A4 = { M_ID }`, and `M_ID → R_MARKETS` (FD6). ✓ 408 409 === Step 3NF-5 — transitive dependency through `O_ID` === 410 411 * '''Relation analyzed:''' `R_A4` = `{ T_ID, T_*, O_ID, O_*, U_ID, M_ID }` (17 attributes), dependencies FD10, FD11, candidate key and primary key `T_ID`, normal form 2NF. 412 * '''3NF violations:''' only `O_ID → O_*, U_ID, M_ID`. '''Split on `O_ID`'''. 413 * '''Decomposition dependency:''' `O_ID → O_*, U_ID, M_ID` (FD10). 414 * '''New relation `R_ORDERS`''' = `{ O_ID, O_*, U_ID, M_ID }` (11 attributes). Dependencies: FD10. Candidate key: `O_ID`. Normal form: BCNF. 415 * '''Residual relation `R_TRANSACTIONS`''' = `R_A4 − { O_*, U_ID, M_ID }` = `{ T_ID, T_*, O_ID }` (7 attributes). Dependencies: FD11. Candidate key: `T_ID`. Normal form: BCNF. Keeping `U_ID` here would have left the transitive dependency `T_ID → O_ID → U_ID` inside the relation. `T_ID → U_ID` was removed from the cover as redundant, so nothing is lost. 416 * '''Dependency preservation:''' FD10 is in `R_ORDERS`, and FD11 is in `R_TRANSACTIONS`. ✓ 417 * '''Lossless join:''' `R_ORDERS ∩ R_TRANSACTIONS = { O_ID }`, and `O_ID → R_ORDERS` (FD10). ✓ 418 419 '''Result of 3NF:''' `R_USERS`, `R_CRYPTO`, `R_HOLDINGS`, `R_MARKETS`, `R_ORDERS`, 420 `R_TRANSACTIONS` (from `R_A`), and `R_B`, `R_C`, `R_D`, `R_E`, `R_F`, `S6` unchanged. 12 421 relations, all in 3NF, and all except `S6` in BCNF. All 18 dependencies are preserved. 350 422 351 423 == BCNF if possible == 352 424 353 '''Relations analyzed:''' the same ten relations, checked against the stricter BCNF rule: every 354 determinant of every functional dependency that holds on the relation must be a candidate key 355 of that relation (3NF allows an exception when the dependent side is prime; BCNF does not). 356 357 ||= Relation =||= Functional dependencies in force =||= Determinant =||= Is it a candidate key? =|| 358 || `R_USERS` || FD1, FD2, FD3 || `U_ID`, `U_USERNAME`, `U_EMAIL` || Yes — all three are candidate keys || 359 || `R_CRYPTO` || FD4, FD5 || `C_ID`, `C_SYMBOL` || Yes — both candidate keys || 360 || `R_MARKETS` || FD6, FD7 || `M_ID`, `{M_CRYPTO_ID, M_QUOTE_CURRENCY}` || Yes — both candidate keys || 361 || `R_HOLDINGS` || FD8, FD9 || `H_ID`, `{H_USER_ID, H_CRYPTO_ID}` || Yes — both candidate keys || 362 || `R_ORDERS` || FD10 || `O_ID` || Yes — the only candidate key || 363 || `R_TRANSACTIONS` || FD11 || `T_ID` || Yes — the only candidate key || 364 || `R_MARKET_TRADES` || FD12 || `MT_ID` || Yes — the only candidate key || 365 || `R_MARKET_CANDLES` || FD13, FD14 || `MC_ID`, `{MC_MARKET_ID, MC_TIMEFRAME, MC_CANDLE_TIME}` || Yes — both candidate keys || 366 || `R_WATCHLISTS` || FD15 || `W_ID` || Yes — the only candidate key || 367 || `R_WATCHLIST_ITEMS` || FD16, FD17 || `WI_ID`, `{WI_WATCHLIST_ID, WI_CRYPTO_ID}` || Yes — both candidate keys || 368 369 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, 370 reached in the same step that fixed 2NF. This is not a coincidence: it happens because the 371 canonical cover already grouped each relation's own key directly against its own attributes 372 with no attribute appearing on the right side of two different relations' dependencies, which 373 is exactly what synthesis from a canonical cover guarantees when, as here, none of the 374 per-cluster functional dependencies overlap. 375 376 No further decomposition is possible or necessary; splitting any of the ten relations further 377 would only separate attributes that already depend on the ''whole'' key of a BCNF relation, 378 which cannot fix anything and only costs a join. 425 BCNF requires '''every''' determinant of a non-trivial dependency to be a superkey, even when 426 the dependent attribute is prime. 427 428 ||= Relation =||= Dependencies in force =||= Determinants =||= All superkeys? =|| 429 || `R_USERS` || FD1, FD2, FD3 || `U_ID`, `U_USERNAME`, `U_EMAIL` || yes || 430 || `R_CRYPTO` || FD4, FD5 || `C_ID`, `C_SYMBOL` || yes || 431 || `R_MARKETS` || FD6, FD7 || `M_ID`, `{C_ID, M_QUOTE_CURRENCY}` || yes || 432 || `R_HOLDINGS` || FD8, FD9 || `H_ID`, `{U_ID, C_ID}` || yes || 433 || `R_ORDERS` || FD10 || `O_ID` || yes || 434 || `R_TRANSACTIONS` || FD11 || `T_ID` || yes || 435 || `R_B` || FD13 || `OE_ID` || yes || 436 || `R_C` || FD12 || `MT_ID` || yes || 437 || `R_D` || FD14, FD15 || `MC_ID`, `{M_ID, MC_TIMEFRAME, MC_CANDLE_TIME}` || yes || 438 || `R_E` || FD16 || `W_ID` || yes || 439 || `R_F` || FD17, FD18 || `WI_ID`, `{W_ID, C_ID}` || yes || 440 || `S6` || `MC_ID → MC_TIMEFRAME, MC_CANDLE_TIME`; `WI_ID → W_ID`; derived ones such as `MT_ID, MC_TIMEFRAME, MC_CANDLE_TIME → MC_ID` and `MT_ID, W_ID → WI_ID` || `MC_ID`, `WI_ID`, `{MT_ID, MC_TIMEFRAME, MC_CANDLE_TIME}`, `{MT_ID, W_ID}`, … || '''no''' || 441 442 '''Dependencies that violate BCNF — only in `S6`.''' `MC_ID` determines `MC_TIMEFRAME` and 443 `MC_CANDLE_TIME`, and `WI_ID` determines `W_ID`, but neither `MC_ID` nor `WI_ID` is a superkey 444 of `S6`. 3NF allowed this because the dependent attributes are prime. BCNF does not. The derived 445 dependencies all involve `W_ID` or `MC_TIMEFRAME`/`MC_CANDLE_TIME`, so they disappear once the 446 two steps below remove those attributes. 447 448 === Step BCNF-1 — `MC_ID → MC_TIMEFRAME, MC_CANDLE_TIME` === 449 450 * '''Relation analyzed:''' `S6` (8 attributes), dependencies as in the table above, candidate keys K1–K4, primary key K1, normal form 3NF. 451 * '''BCNF violations:''' `MC_ID → MC_TIMEFRAME, MC_CANDLE_TIME` and `WI_ID → W_ID`, and the derived ones that depend on them. '''Split first on `MC_ID`'''. The order does not matter here, because the two violations share no attribute. 452 * '''Decomposition dependency:''' `MC_ID → MC_TIMEFRAME, MC_CANDLE_TIME`. `MC_ID` is not a superkey of `S6`. 453 * '''New relation''' `{ MC_ID, MC_TIMEFRAME, MC_CANDLE_TIME }`. Dependencies: `MC_ID → MC_TIMEFRAME, MC_CANDLE_TIME`. Key: `MC_ID`. Normal form: BCNF. It is a projection of `R_D`, which already contains these attributes with the same key, so it adds no information and is merged into `R_D`. 454 * '''Residual relation `S7`''' = `{ T_ID, OE_ID, MT_ID, MC_ID, W_ID, WI_ID }` (6 attributes). Dependencies: `WI_ID → W_ID`, and derived ones such as `MT_ID, W_ID → WI_ID`. Candidate keys: `{T_ID, OE_ID, MT_ID, MC_ID, WI_ID}` (K1) and `{T_ID, OE_ID, MT_ID, MC_ID, W_ID}` (K2). Normal form: 3NF. 455 * '''Dependency preservation:''' no dependency of the cover is affected. FD14 and FD15 are in `R_D`. ✓ 456 * '''Lossless join:''' the intersection is `{ MC_ID }`, and `MC_ID → { MC_ID, MC_TIMEFRAME, MC_CANDLE_TIME }`. ✓ 457 458 === Step BCNF-2 — `WI_ID → W_ID` === 459 460 * '''Relation analyzed:''' `S7` (6 attributes), dependencies `WI_ID → W_ID` and derived ones, candidate keys K1, K2, primary key K1, normal form 3NF. 461 * '''BCNF violations:''' only `WI_ID → W_ID` (and the derived `MT_ID, W_ID → WI_ID`). '''Split on `WI_ID`'''. 462 * '''Decomposition dependency:''' `WI_ID → W_ID`. `WI_ID` is not a superkey of `S7`. 463 * '''New relation''' `{ WI_ID, W_ID }`. Dependencies: `WI_ID → W_ID`. Key: `WI_ID`. Normal form: BCNF. For the same reason as in BCNF-1, it is merged into `R_F`. 464 * '''Residual relation `R_KEY`''' = `{ T_ID, OE_ID, MT_ID, MC_ID, WI_ID }` (5 attributes). No non-trivial dependency holds among these attributes. Candidate key: all five (= K1). Normal form: BCNF. 465 * '''Dependency preservation:''' no dependency of the cover is affected. FD17 and FD18 are in `R_F`. The derived dependencies of `S6`/`S7` follow from FD12, FD15, FD17 and FD18, which are all preserved. ✓ 466 * '''Lossless join:''' the intersection is `{ WI_ID }`, and `WI_ID → { WI_ID, W_ID }`. ✓ 467 468 '''Result: every relation is in BCNF.''' The decomposition into these 12 relations is lossless 469 (each of the 13 binary steps passed the test) and preserves all 18 dependencies of the 470 canonical cover. 379 471 380 472 == Final result and discussion == 381 473 382 474 === Normalized relational model === 475 476 Each relation is followed by its keys (primary key first). An attribute that is the 477 identifier of another relation is marked `→` with that relation. 383 478 384 479 {{{ 385 480 R_USERS (U_ID, U_USERNAME, U_EMAIL, U_FULL_NAME, U_PASSWORD_HASH, 386 U_AVAILABLE_BALANCE, U_INVESTED_BALANCE, U_CREATED_AT, U_UPDATED_AT) 481 U_AVAILABLE_BALANCE, U_INVESTED_BALANCE, U_RESERVED_BALANCE, 482 U_CREATED_AT, U_UPDATED_AT) 483 keys: U_ID; U_USERNAME; U_EMAIL 387 484 R_CRYPTO (C_ID, C_SYMBOL, C_NAME, C_CREATED_AT) 388 R_MARKETS (M_ID, M_CRYPTO_ID → R_CRYPTO, M_QUOTE_CURRENCY, M_IS_ACTIVE, M_CREATED_AT) 389 R_HOLDINGS (H_ID, H_USER_ID → R_USERS, H_CRYPTO_ID → R_CRYPTO, H_QUANTITY, 390 H_RESERVED_QUANTITY, H_AVG_PRICE, H_CREATED_AT, H_UPDATED_AT) 391 R_ORDERS (O_ID, O_USER_ID → R_USERS, O_MARKET_ID → R_MARKETS, O_SIDE, O_TYPE, 392 O_STATUS, O_QUANTITY, O_PRICE, O_PLACED_AT, O_EXECUTED_AT) 393 R_TRANSACTIONS (T_ID, T_USER_ID → R_USERS, T_TYPE, T_AMOUNT, T_CURRENCY, 394 T_RELATED_ORDER → R_ORDERS, T_CREATED_AT, T_DESCRIPTION) 395 R_MARKET_TRADES (MT_ID, MT_MARKET_ID → R_MARKETS, MT_EXECUTED_AT, MT_PRICE, MT_QUANTITY, 396 MT_SIDE, MT_SOURCE) 397 R_MARKET_CANDLES (MC_ID, MC_MARKET_ID → R_MARKETS, MC_TIMEFRAME, MC_OPEN, MC_HIGH, MC_LOW, 398 MC_CLOSE, MC_VOLUME, MC_CANDLE_TIME) 399 R_WATCHLISTS (W_ID, W_USER_ID → R_USERS, W_NAME, W_CREATED_AT) 400 R_WATCHLIST_ITEMS(WI_ID, WI_WATCHLIST_ID → R_WATCHLISTS, WI_CRYPTO_ID → R_CRYPTO, WI_ADDED_AT) 485 keys: C_ID; C_SYMBOL 486 R_MARKETS (M_ID, C_ID → R_CRYPTO, M_QUOTE_CURRENCY, M_IS_ACTIVE, M_CREATED_AT) 487 keys: M_ID; {C_ID, M_QUOTE_CURRENCY} 488 R_HOLDINGS (H_ID, U_ID → R_USERS, C_ID → R_CRYPTO, H_QUANTITY, 489 H_RESERVED_QUANTITY, H_AVG_PRICE, H_CREATED_AT, H_UPDATED_AT) 490 keys: H_ID; {U_ID, C_ID} 491 R_ORDERS (O_ID, U_ID → R_USERS, M_ID → R_MARKETS, O_SIDE, O_TYPE, O_STATUS, 492 O_QUANTITY, O_FILLED_QUANTITY, O_PRICE, O_PLACED_AT, O_EXECUTED_AT) 493 key: O_ID 494 R_TRANSACTIONS (T_ID, O_ID → R_ORDERS, T_TYPE, T_AMOUNT, T_CURRENCY, T_CREATED_AT, 495 T_DESCRIPTION) 496 key: T_ID 497 R_MARKET_TRADES (MT_ID, M_ID → R_MARKETS, MT_EXECUTED_AT, MT_PRICE, MT_QUANTITY, 498 MT_SIDE, MT_SOURCE, O_ID_FILLSBUY → R_ORDERS (nullable), 499 O_ID_FILLSSELL → R_ORDERS (nullable)) [= R_C] 500 key: MT_ID 501 R_ORDER_EVENTS (OE_ID, O_ID → R_ORDERS, OE_EVENT_TYPE, OE_QUANTITY, OE_PRICE, 502 OE_STATUS_AFTER, OE_CREATED_AT) [= R_B] 503 key: OE_ID 504 R_MARKET_CANDLES (MC_ID, M_ID → R_MARKETS, MC_TIMEFRAME, MC_OPEN, MC_HIGH, MC_LOW, 505 MC_CLOSE, MC_VOLUME, MC_CANDLE_TIME) [= R_D] 506 keys: MC_ID; {M_ID, MC_TIMEFRAME, MC_CANDLE_TIME} 507 R_WATCHLISTS (W_ID, U_ID → R_USERS, W_NAME, W_CREATED_AT) [= R_E] 508 key: W_ID 509 R_WATCHLIST_ITEMS(WI_ID, W_ID → R_WATCHLISTS, C_ID → R_CRYPTO, WI_ADDED_AT) [= R_F] 510 keys: WI_ID; {W_ID, C_ID} 511 R_KEY (T_ID, OE_ID, MT_ID, MC_ID, WI_ID) [= R_KEY] 512 key: all five 401 513 }}} 402 514 403 Ten relations, every one in BCNF, connected by the eleven foreign keys spelled out above.404 405 515 === Discussion === 406 516 407 '''This is the P2 design.''' Strip the `U_`/`C_`/`M_`/… prefixes back to plain column names and 408 `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 409 key, `users, crypto, markets, holdings, orders, transactions, market_trades, market_candles, watchlists, watchlist_items` from 410 [wiki:RelationalDesign]. Every foreign key matches, 411 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 412 phase shows the stronger result that the design is actually in BCNF). 413 414 That is not a coincidence of two people happening to agree — it is what should happen when a 415 design is derived correctly twice by two different methods from the same underlying model: 416 P2 got here by applying the standard ER-to-relational transformation rules (each entity 417 becomes a table on its own key, each attributed M:N relationship becomes a table on the 418 combined key, each attributeless 1:N relationship becomes a foreign key on the "many" side). 419 This phase got here by ignoring that transformation entirely, writing down only the 420 attributes and the functional dependencies they obey, and mechanically applying 2NF/3NF/BCNF 421 synthesis. Landing on the same ten relations either means the P2 transformation rules are 422 sound for this particular model (which they are, for exactly the reason [wiki:RelationalDesign] (Normalisation section) 423 already argued: single-column UUID primary keys everywhere rule out partial dependencies by 424 construction, and no non-key attribute references another non-key attribute anywhere in the 425 model, which rules out transitive dependencies too), or it is a coincidence spanning ten 426 independently-checked relations and dozens of functional dependencies — the first explanation 427 is the only credible one. 428 429 '''The one substantive difference''' is `holdings.avg_price`, which P2 documents as a 430 ''derived'' attribute — the running weighted-average buy price, recomputable from the `buy` rows 431 in `transactions` — kept as a stored column anyway for read performance 432 ([wiki:RelationalDesign] (Normalisation section) calls this out 433 explicitly as an accepted denormalisation). Nothing in this phase's functional-dependency 434 analysis can see that `H_AVG_PRICE` is derivable from `T_*` rows rather than stored 435 independently — FD8 (`H_ID → H_AVG_PRICE`) is a perfectly ordinary functional dependency 436 either way, because ''derivability from a different relation's rows'' is a property of the data 437 and the application logic that maintains it (see 438 [wiki:UseCase0004]'s `ON CONFLICT … DO UPDATE`), not something 439 that shows up as a violation of any single-relation normal form. Formal normalization and "no 440 column is a cached computation of other columns" are related but different concerns; this 441 phase only checked the first one. 442 443 '''Which design is used going forward:''' P2's, unchanged. Since the two designs coincide 444 exactly, "restructuring the database objects" means confirming there is nothing to change 445 rather than writing new DDL. `server/db/schema_creation.sql` 446 already matches `R_USERS`…`R_WATCHLIST_ITEMS` column-for-column (including 447 `holdings.reserved_quantity`, added between P2 and this phase — see 448 [wiki:RelationalDesignAIUsage] (section "Session 3 — 2026-09-16") 449 — which is `H_RESERVED_QUANTITY` above, correctly grouped under `R_HOLDINGS`'s key alongside 450 `H_QUANTITY` and not treated as needing a relation of its own). P4's prototype 451 (`server/trade.go`, `server/portfolio.go`) keeps working against the same schema without 452 change. [wiki:RelationalDesign] has been updated with a 453 short note pointing here as the formal validation of its normal-form claim. 454 455 The table definitions in `server/db/schema_creation.sql`: 517 '''The eleven data relations are the P2 design, with one difference''' (`transactions.user_id`, 518 explained below). Each relation is one entity set of the ER model: 519 520 ||= P5 relation =||= P2 table =||= How the relationships appear =|| 521 || `R_USERS` || `users` || — || 522 || `R_CRYPTO` || `crypto` || — || 523 || `R_MARKETS` || `markets` || `C_ID` = `crypto_id` (`QuotedOn`) || 524 || `R_HOLDINGS` || `holdings` || `U_ID` = `user_id` (`Holds`), `C_ID` = `crypto_id` (`PositionIn`) || 525 || `R_ORDERS` || `orders` || `U_ID` = `user_id` (`Places`), `M_ID` = `market_id` (`PlacedOn`) || 526 || `R_TRANSACTIONS` || `transactions` || `O_ID` = `related_order` (`Settles`); P2 also stores `user_id` (`Records`), see below || 527 || `R_MARKET_TRADES` || `market_trades` || `M_ID` = `market_id` (`Fills`), `O_ID_FILLSBUY` = `buy_order_id`, `O_ID_FILLSSELL` = `sell_order_id` || 528 || `R_ORDER_EVENTS` || `order_events` || `O_ID` = `order_id` (`Logs`) || 529 || `R_MARKET_CANDLES` || `market_candles` || `M_ID` = `market_id` (`Aggregates`) || 530 || `R_WATCHLISTS` || `watchlists` || `U_ID` = `user_id` (`Owns`) || 531 || `R_WATCHLIST_ITEMS` || `watchlist_items` || `W_ID` = `watchlist_id` (`Contains`), `C_ID` = `crypto_id` (`Lists`) || 532 533 The two methods produce the foreign keys differently. In P2 they come from a transformation 534 rule: a 1:N relationship becomes a column on the N side. Here, each one appears because a 535 dependency of kind (R), for example `O_ID → U_ID`, keeps the other entity's identifier in the 536 same relation as the entity that depends on it. The candidate keys also match, including the 537 composite ones (`{C_ID, M_QUOTE_CURRENCY}`, `{U_ID, C_ID}`, `{M_ID, MC_TIMEFRAME, 538 MC_CANDLE_TIME}`, `{W_ID, C_ID}`). They are exactly the `UNIQUE` constraints in 539 `schema_creation.sql`. 540 541 '''The one difference: `transactions.user_id`.''' The decomposition drops `U_ID` from 542 `R_TRANSACTIONS`, because `T_ID → U_ID` follows from `T_ID → O_ID` and `O_ID → U_ID`. That is 543 correct for every ledger entry that settles an order. It does not work for a '''deposit'''. 544 `Settles` is partial, so a deposit has no order, and without `user_id` a deposit would have no 545 owner at all. The de-normalized relation cannot show this case. Every one of its tuples 546 contains an order (every key contains `OE_ID`, and every order event has an order), so a 547 ledger entry without an order cannot appear in it. P2 therefore keeps `user_id` (the 548 relationship `Records`) as a deliberate exception. As a result, the implemented 549 `transactions` table is in '''2NF but not in 3NF''' (`related_order → user_id` is a transitive 550 dependency), and this is by design. For entries with an order, 551 `transactions.user_id` repeats the order's user. The only code that sets `related_order` (the buy 552 and sell inserts in `advanced_db.sql`) writes the user and the id of the same order row. No 553 database constraint enforces this. 554 555 '''Two order columns in `market_trades`.''' `FillsBuy` and `FillsSell` needed two role 556 attributes already in the de-normalized relation, and both end up in `R_MARKET_TRADES`. 557 They correspond to `buy_order_id` and `sell_order_id`. 558 559 '''`R_KEY` belongs to the formal result, but it is not implemented as a table.''' It is the 560 relation that contains a key of `R_EDUBERZA`, and the lossless-join result above holds for all 561 12 relations ''including'' it. It records no fact of the domain. It only says which ledger 562 entry, order event, trade, candle and watchlist item were put into the same tuple, and that 563 combination exists only because we started from one single relation. Not implementing it is 564 an implementation decision. The eleven implemented tables are not claimed to reconstruct 565 `R_EDUBERZA` on their own. They keep every attribute and every dependency of the canonical 566 cover, and that is what the application needs. 567 568 '''`holdings.avg_price`''' is shown as a ''derived'' attribute in the ER model: it can be 569 recomputed from the buy history. It is still stored, and that is a deliberate 570 denormalisation (see [wiki:RelationalDesign] (section "Normalisation")). 571 Normalisation cannot detect this. `H_ID → H_AVG_PRICE` is an ordinary functional dependency, 572 because "derivable from rows of another entity" is a property of the application logic 573 that maintains the value (see [wiki:UseCase0004], 574 `ON CONFLICT … DO UPDATE`), not a dependency between attributes of one tuple. 575 576 '''Which design is used going forward:''' P2's, unchanged. The eleven data relations coincide 577 with the eleven tables of `schema_creation.sql` and 578 `advanced_db.sql` column for column, except for the 579 deliberately kept `transactions.user_id` explained above. So there are no database objects 580 to restructure, and the prototype and the reports of P6/P7 keep working against the same 581 schema. 582 583 The table definitions in `server/db/schema_creation.sql` and `server/db/advanced_db.sql`: 456 584 457 585 {{{ … … 464 592 available_balance numeric(18,4) NOT NULL DEFAULT 0 CHECK (available_balance >= 0), 465 593 invested_balance numeric(18,4) NOT NULL DEFAULT 0 CHECK (invested_balance >= 0), 594 -- P7: cash committed to the user's active buy orders, moved out of 595 -- available_balance when the order is placed and consumed as it fills. 596 reserved_balance numeric(18,4) NOT NULL DEFAULT 0 CHECK (reserved_balance >= 0), 466 597 created_at timestamptz NOT NULL DEFAULT now(), 467 598 updated_at timestamptz 468 599 ); 469 600 470 -- ============================================================================471 -- CRYPTO472 -- Catalog of crypto assets available on the platform.473 -- ============================================================================474 601 CREATE TABLE project.crypto ( 475 602 id uuid PRIMARY KEY DEFAULT gen_random_uuid(), … … 479 606 ); 480 607 481 -- ============================================================================482 -- MARKETS483 -- A market is a (crypto, quote_currency) pair, e.g. BTC/USD.484 -- ============================================================================485 608 CREATE TABLE project.markets ( 486 609 id uuid PRIMARY KEY DEFAULT gen_random_uuid(), … … 492 615 ); 493 616 494 -- ============================================================================495 -- HOLDINGS496 -- Per-user crypto position with running weighted average entry price.497 -- ============================================================================498 617 CREATE TABLE project.holdings ( 499 618 id uuid PRIMARY KEY DEFAULT gen_random_uuid(), … … 514 633 ); 515 634 516 -- ============================================================================517 -- ORDERS518 -- Orders placed by users on a market.519 -- ============================================================================520 635 CREATE TABLE project.orders ( 521 636 id uuid PRIMARY KEY DEFAULT gen_random_uuid(), … … 524 639 side varchar(4) NOT NULL CHECK (side IN ('buy', 'sell')), 525 640 type varchar(20) NOT NULL CHECK (type IN ('market', 'limit')), 526 status varchar(20) NOT NULL CHECK (status IN ('open', ' executed', 'cancelled')),641 status varchar(20) NOT NULL CHECK (status IN ('open', 'partially_filled', 'executed', 'cancelled')), 527 642 quantity numeric(20,4) NOT NULL CHECK (quantity > 0), 643 -- P7: how much of the order has been traded so far; remaining is 644 -- quantity - filled_quantity. Maintained from market_trades. 645 filled_quantity numeric(20,4) NOT NULL DEFAULT 0 646 CHECK (filled_quantity >= 0 AND filled_quantity <= quantity), 528 647 price numeric(18,6), 529 648 placed_at timestamptz NOT NULL DEFAULT now(), … … 531 650 ); 532 651 533 CREATE INDEX idx_orders_user ON project.orders(user_id);534 CREATE INDEX idx_orders_market ON project.orders(market_id);535 CREATE INDEX idx_orders_status ON project.orders(status);536 537 -- ============================================================================538 -- TRANSACTIONS539 -- Financial ledger: deposits, buys, sells, fees.540 -- ============================================================================541 652 CREATE TABLE project.transactions ( 542 653 id uuid PRIMARY KEY DEFAULT gen_random_uuid(), … … 550 661 ); 551 662 552 CREATE INDEX idx_transactions_user ON project.transactions(user_id, created_at DESC);553 554 -- ============================================================================555 -- MARKET TRADES556 -- Raw executed trades on a market. Source of truth for current price.557 -- ============================================================================558 663 CREATE TABLE project.market_trades ( 559 664 id bigserial PRIMARY KEY, … … 563 668 quantity numeric(20,6) NOT NULL CHECK (quantity > 0), 564 669 side varchar(4) CHECK (side IN ('buy', 'sell')), 565 source varchar(50) NOT NULL DEFAULT 'simulation' 566 ); 567 568 CREATE INDEX idx_market_trades_market_time ON project.market_trades(market_id, executed_at DESC); 569 570 -- ============================================================================ 571 -- MARKET CANDLES 572 -- OHLCV aggregates over standard timeframes. 573 -- ============================================================================ 670 source varchar(50) NOT NULL DEFAULT 'simulation', 671 -- P7: the orders this trade filled. NULL on a side means the counterparty 672 -- was the simulated market (bot ticks have both NULL). 673 buy_order_id uuid REFERENCES project.orders(id), 674 sell_order_id uuid REFERENCES project.orders(id) 675 ); 676 574 677 CREATE TABLE project.market_candles ( 575 678 id bigserial PRIMARY KEY, … … 585 688 ); 586 689 587 CREATE INDEX idx_market_candles_market_tf_time ON project.market_candles(market_id, timeframe, candle_time DESC);588 589 -- ============================================================================590 -- WATCHLISTS591 -- ============================================================================592 690 CREATE TABLE project.watchlists ( 593 691 id uuid PRIMARY KEY DEFAULT gen_random_uuid(), … … 604 702 CONSTRAINT uq_watchlist_crypto UNIQUE (watchlist_id, crypto_id) 605 703 ); 704 705 CREATE TABLE project.order_events ( 706 id bigserial PRIMARY KEY, 707 order_id uuid NOT NULL REFERENCES project.orders(id) ON DELETE CASCADE, 708 event_type varchar(20) NOT NULL 709 CHECK (event_type IN ('placed', 'partially_filled', 'filled', 'cancelled')), 710 quantity numeric(20,4) NOT NULL, 711 price numeric(18,6), 712 status_after varchar(20) NOT NULL, 713 created_at timestamptz NOT NULL DEFAULT clock_timestamp() 714 ); 606 715 }}} -
docs/README.md
r0cee8ec r1549dae 30 30 `ERModel`, `RelationalDesign`, `UseCaseModel`, `UseCase0001`, …, 31 31 `PrototypeImplementation`, `BuildInstructions`, and the four `*AIUsage` pages. 32 Attachments (`ERModel_v0 1.xml`, `ERModel_v01.png`, `schema_creation.sql`,33 `data_load.sql`, `relational_ schema.jpg`, the screenshots) attach to the page32 Attachments (`ERModel_v05.xml`, `ERModel_v05.png`, `schema_creation.sql`, 33 `data_load.sql`, `relational_diagram_v4.png`, the screenshots) attach to the page 34 34 that documents them. 35 35 … … 80 80 | File | Phase | What | 81 81 |------|-------|------| 82 | [`ERModel_v03.xml`](P1-ConceptualModel/ERModel_v03.xml) | P1 | TerraER source, current version | 83 | [`ERModel_v03.png`](P1-ConceptualModel/ERModel_v03.png) | P1 | Exported diagram image, current version | 82 | [`ERModel_v05.xml`](P1-ConceptualModel/ERModel_v05.xml) | P1 | TerraER source, current version | 83 | [`ERModel_v05.png`](P1-ConceptualModel/ERModel_v05.png) | P1 | Exported diagram image, current version | 84 | [`ERModel_v04.xml`](P1-ConceptualModel/ERModel_v04.xml), [`ERModel_v03.xml`](P1-ConceptualModel/ERModel_v03.xml) | P1 | TerraER source, previous versions (kept per P1 rules) | 85 | [`ERModel_v04.png`](P1-ConceptualModel/ERModel_v04.png), [`ERModel_v03.png`](P1-ConceptualModel/ERModel_v03.png) | P1 | Exported diagram images, previous versions | 84 86 | [`ERModel_v02.xml`](P1-ConceptualModel/ERModel_v02.xml) | P1 | TerraER source, previous version (kept per P1 rules) | 85 87 | [`ERModel_v02.png`](P1-ConceptualModel/ERModel_v02.png) | P1 | Exported diagram image, previous version | … … 88 90 | [`../server/db/schema_creation.sql`](../server/db/schema_creation.sql) | P2 | DDL — drops and recreates the `project` schema | 89 91 | [`../server/db/data_load.sql`](../server/db/data_load.sql) | P2 | DML — truncates and reloads sample data | 90 | [`relational_ schema.jpg`](P2-RelationalDesign/relational_schema.jpg) | P2 | Crow's-foot diagram exported from DBeaver|92 | [`relational_diagram_v4.png`](P2-RelationalDesign/relational_diagram_v4.png) | P2 | Relational diagram exported from DBeaver, laid out like `ERModel_v05.png` | 91 93 | [`../server/db/reports_demo_data.sql`](../server/db/reports_demo_data.sql) | P6 | Optional multi-quarter demo data for the two reports (not part of `-init`) | 92 94 | [`../server/db/advanced_db.sql`](../server/db/advanced_db.sql) | P7 | Triggers, functions, views and the background job (part of `-init`) |
Note:
See TracChangeset
for help on using the changeset viewer.
