Changeset 1549dae for docs/P2-RelationalDesign
- Timestamp:
- 09/29/26 20:55:13 (6 hours ago)
- Branches:
- main
- Parents:
- 0cee8ec
- Location:
- docs/P2-RelationalDesign
- Files:
-
- 1 added
- 5 edited
-
P2.zip (modified) ( previous)
-
RelationalDesign.md (modified) (5 diffs)
-
RelationalDesignAIUsage.md (modified) (2 diffs)
-
relational_diagram_v4.png (added)
-
wiki/RelationalDesign.md (modified) (10 diffs)
-
wiki/RelationalDesignAIUsage.md (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
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.
Note:
See TracChangeset
for help on using the changeset viewer.
