From 0ddfa081b2627973d0ca0862e5568e7ef5f92bca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20P=C3=A9rez-Aradros=20Herce?= Date: Thu, 28 May 2026 16:52:12 +0200 Subject: [PATCH 1/3] Add lesson 10-advanced-joins Covers RIGHT JOIN (and why LEFT is usually preferred), FULL OUTER JOIN for reconciliation, CROSS JOIN for Cartesian products, the self-join pattern for in-table hierarchies (employees/manager_id), and multi-table chains. Closes with a "when to use which" decision table. Also updates 01-joins's "Up next" pointer to lead into this lesson. Part of #6. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../03-combining-tables/01-joins/lesson.mdx | 2 +- .../02-advanced-joins/lesson.mdx | 128 ++++++++++++++++++ .../02-advanced-joins/lesson.yaml | 19 +++ .../02-advanced-joins/seed.sql | 47 +++++++ 4 files changed, 195 insertions(+), 1 deletion(-) create mode 100644 lessons/03-combining-tables/02-advanced-joins/lesson.mdx create mode 100644 lessons/03-combining-tables/02-advanced-joins/lesson.yaml create mode 100644 lessons/03-combining-tables/02-advanced-joins/seed.sql diff --git a/lessons/03-combining-tables/01-joins/lesson.mdx b/lessons/03-combining-tables/01-joins/lesson.mdx index 9c0521e..4df76bc 100644 --- a/lessons/03-combining-tables/01-joins/lesson.mdx +++ b/lessons/03-combining-tables/01-joins/lesson.mdx @@ -112,4 +112,4 @@ Revenue per category, in one query. This is the bread-and-butter of analytics wo Mark this lesson done — we'll just confirm the sandbox is healthy. -Up next: subqueries and Common Table Expressions (`WITH`) — composing queries from smaller queries. +Up next: the rest of the join family — `RIGHT`, `FULL`, `CROSS`, and joining a table to itself. diff --git a/lessons/03-combining-tables/02-advanced-joins/lesson.mdx b/lessons/03-combining-tables/02-advanced-joins/lesson.mdx new file mode 100644 index 0000000..51f523c --- /dev/null +++ b/lessons/03-combining-tables/02-advanced-joins/lesson.mdx @@ -0,0 +1,128 @@ +The previous lesson covered `INNER JOIN` and `LEFT JOIN` — the two you'll use most. This one rounds out the family: `RIGHT`, `FULL`, `CROSS`, and the trick of joining a table to itself. + +The seed has a small organization: `employees` (with a `manager_id` pointing back into the same table) and `departments` (one of which has no employees). There are also two tiny `shirt_sizes`/`shirt_colors` tables for the CROSS JOIN example. + +## RIGHT JOIN: LEFT JOIN, mirrored + +`RIGHT JOIN` keeps every row from the *right* table, NULL-padding the left when there's no match. It's `LEFT JOIN` with the tables swapped — and that's almost always how people write it instead, because reading order matters. + + +SELECT d.name AS department, e.full_name +FROM employees e +RIGHT JOIN departments d ON d.id = e.department_id +ORDER BY d.name, e.full_name; + + +Operations has no employees — it shows up with `full_name` as NULL. Same shape if you reversed the tables and used `LEFT JOIN`. In practice: pick `LEFT` and put the "keep all of these" table on the left. Reads more naturally. + +## FULL JOIN: keep both sides + +`FULL OUTER JOIN` (the `OUTER` is optional) keeps **every** row from both tables: matched pairs where possible, plus left rows with no right match, plus right rows with no left match — each padded with NULLs on the side that didn't have a partner. + + +SELECT e.full_name, d.name AS department +FROM employees e +FULL JOIN departments d ON d.id = e.department_id +ORDER BY e.full_name NULLS LAST, d.name; + + +Three flavors in the result: + +- Most rows: an employee with a department. +- Edsger and Donald (contractors): `department` is NULL. +- Operations: `full_name` is NULL. + +`FULL JOIN` shines when you need a single result that reconciles two sources — "all the things we know about, from either side, no matter which side knew about them". + +### Finding the "only-one-side" rows + +A `FULL JOIN` filtered to rows where one side is NULL gives you the asymmetric difference: things on the left with no match, or vice versa. Combine both NULL checks for "rows missing from one or both sides": + + +SELECT e.full_name, d.name AS department +FROM employees e +FULL JOIN departments d ON d.id = e.department_id +WHERE e.id IS NULL OR e.department_id IS NULL; + + +Useful for reconciling: "who's missing from the departments table?", "which departments have nobody assigned?" + +## CROSS JOIN: every row paired with every row + +`CROSS JOIN` is the Cartesian product — no `ON` clause, every left row paired with every right row. Result size is `left × right`. + + +SELECT size, color +FROM shirt_sizes +CROSS JOIN shirt_colors +ORDER BY size, color; + + +Three sizes × three colors = nine combinations. Nine shirts to stock. CROSS JOIN earns its keep for: + +- **Generating combinations** (sizes × colors, dates × users, regions × products). +- **Pairing every row with a single-row computation** — `CROSS JOIN (SELECT now() AS t)` injects the current time alongside every row. +- **`generate_series` plus a table** — fill in missing days in a time series. + +The two implicit-comma forms (`FROM a, b`) and `CROSS JOIN a, b` are equivalent, but `CROSS JOIN` makes the intent explicit. Never accidentally CROSS JOIN a large table — millions × millions is a long afternoon. + +## Self-join: a table joined to itself + +When rows have relationships to *other rows in the same table*, you join the table to itself with a different alias for each "side". The classic example is an employee/manager hierarchy. + + +SELECT e.full_name AS employee, m.full_name AS manager +FROM employees e +LEFT JOIN employees m ON m.id = e.manager_id +ORDER BY m.full_name NULLS FIRST, e.full_name; + + +Two aliases for the same table — `e` is the row we're describing, `m` is the row we're looking up. The `LEFT JOIN` keeps Ada in the result even though her `manager_id` is NULL (she's at the top). + +Self-joins also handle peers (employees in the same department) and chains (manager's manager). For *deep* recursion — "everyone underneath Ada, however many levels down" — you reach for a recursive CTE, which is its own future lesson. + +## Multi-table joins + +Each `JOIN` adds one more table. The query reads top-to-bottom as a pipeline: start with a row, attach a related row, then another. + + +SELECT e.full_name AS employee, + m.full_name AS manager, + d.name AS department +FROM employees e +LEFT JOIN employees m ON m.id = e.manager_id +LEFT JOIN departments d ON d.id = e.department_id +ORDER BY d.name NULLS LAST, e.full_name; + + +Three tables (two of them the same table aliased differently), one result. The `LEFT JOIN` on departments keeps the contractors in the listing even though they have no department. + +## When to use which + +A useful mental table: + +| You want… | Use | +| ------------------------------------------------------------ | -------------- | +| Rows that match on both sides | `INNER JOIN` | +| All rows from the left, matches from the right where present | `LEFT JOIN` | +| Same, but with the tables in the other order | `RIGHT JOIN` | +| All rows from both sides, NULLs where they don't match | `FULL JOIN` | +| Every combination of left × right | `CROSS JOIN` | +| A row's relationship to other rows in the same table | self-join | + +A working rule: prefer `LEFT` over `RIGHT` so the "keep all" table is on the left. Reach for `FULL` only when both sides legitimately matter. Use `CROSS` deliberately — never by accident. + +## What you learned + +- `RIGHT JOIN` = `LEFT JOIN` with the tables flipped; pick one shape and stick with it. +- `FULL OUTER JOIN` keeps unmatched rows from both sides, NULL-padded. +- `WHERE side.id IS NULL` filters down to the asymmetric difference — "missing on this side". +- `CROSS JOIN` is the Cartesian product; useful for generating combinations, deadly by accident. +- Self-joins use two aliases for the same table — the standard pattern for parent/child within one table. +- Chain `JOIN`s to bring in more tables; read top-to-bottom as a pipeline. + + +Mark this lesson done — we'll just confirm the sandbox is healthy. + + +Up next: combining results vertically with `UNION`, `INTERSECT`, and `EXCEPT`. diff --git a/lessons/03-combining-tables/02-advanced-joins/lesson.yaml b/lessons/03-combining-tables/02-advanced-joins/lesson.yaml new file mode 100644 index 0000000..b3acd34 --- /dev/null +++ b/lessons/03-combining-tables/02-advanced-joins/lesson.yaml @@ -0,0 +1,19 @@ +title: Advanced joins +summary: RIGHT, FULL, and CROSS joins, self-joins for hierarchies, and chaining joins across many tables. +estimatedMinutes: 14 +tags: + - join + - right-join + - full-join + - cross-join + - self-join +authors: + - exekias +seed: seed.sql +checks: + - id: seed-loaded + type: row-count + description: The seeded employees table has 12 rows — click to mark this lesson done. + table: employees + expect: + rowCount: 12 diff --git a/lessons/03-combining-tables/02-advanced-joins/seed.sql b/lessons/03-combining-tables/02-advanced-joins/seed.sql new file mode 100644 index 0000000..d14d221 --- /dev/null +++ b/lessons/03-combining-tables/02-advanced-joins/seed.sql @@ -0,0 +1,47 @@ +-- Seed for "02-advanced-joins": a small org with employees, departments, and +-- projects so we have material for self-joins (manager_id), FULL joins +-- (department with no employees + employee with no department), and three- +-- table chains. A few sizes and colors give CROSS JOIN something to do. + +CREATE TABLE departments ( + id serial PRIMARY KEY, + name text NOT NULL UNIQUE +); + +INSERT INTO departments (name) VALUES + ('Engineering'), + ('Design'), + ('Sales'), + ('Operations'); -- intentionally has no employees, for FULL JOIN + +CREATE TABLE employees ( + id serial PRIMARY KEY, + full_name text NOT NULL, + department_id int REFERENCES departments(id), -- nullable: contractors + manager_id int REFERENCES employees(id), + hired_at date NOT NULL +); + +INSERT INTO employees (full_name, department_id, manager_id, hired_at) VALUES + ('Ada Lovelace', 1, NULL, '2020-01-10'), -- 1, no manager (CEO-ish) + ('Alan Turing', 1, 1, '2020-03-04'), -- 2, manager: Ada + ('Grace Hopper', 1, 1, '2020-04-12'), -- 3, manager: Ada + ('Linus Torvalds', 1, 2, '2021-02-19'), -- 4, manager: Alan + ('Margaret Hamilton', 2, 1, '2020-06-01'), -- 5, manager: Ada (design lead) + ('Barbara Liskov', 2, 5, '2021-05-15'), -- 6, manager: Margaret + ('Dennis Ritchie', 3, 1, '2021-09-22'), -- 7, manager: Ada (sales lead) + ('Ken Thompson', 3, 7, '2022-01-11'), -- 8, manager: Dennis + ('Edsger Dijkstra', NULL, NULL, '2023-03-03'), -- 9, contractor, no dept + ('Donald Knuth', NULL, NULL, '2023-08-17'), -- 10, contractor, no dept + ('Guido van Rossum', 1, 2, '2024-02-20'), -- 11, manager: Alan + ('Bjarne Stroustrup', 2, 5, '2024-07-09'); -- 12, manager: Margaret + +CREATE TABLE shirt_sizes ( + size text PRIMARY KEY +); +INSERT INTO shirt_sizes (size) VALUES ('S'), ('M'), ('L'); + +CREATE TABLE shirt_colors ( + color text PRIMARY KEY +); +INSERT INTO shirt_colors (color) VALUES ('black'), ('white'), ('navy'); From 96f81d775b01ab4cfbf459d740ad689d85ba8b8f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 28 May 2026 15:56:28 +0000 Subject: [PATCH 2/3] Fix table rendering: replace GFM markdown table with HTML table --- .../02-advanced-joins/lesson.mdx | 42 +++++-- package-lock.json | 105 +----------------- 2 files changed, 36 insertions(+), 111 deletions(-) diff --git a/lessons/03-combining-tables/02-advanced-joins/lesson.mdx b/lessons/03-combining-tables/02-advanced-joins/lesson.mdx index 51f523c..44d69aa 100644 --- a/lessons/03-combining-tables/02-advanced-joins/lesson.mdx +++ b/lessons/03-combining-tables/02-advanced-joins/lesson.mdx @@ -101,14 +101,40 @@ Three tables (two of them the same table aliased differently), one result. The ` A useful mental table: -| You want… | Use | -| ------------------------------------------------------------ | -------------- | -| Rows that match on both sides | `INNER JOIN` | -| All rows from the left, matches from the right where present | `LEFT JOIN` | -| Same, but with the tables in the other order | `RIGHT JOIN` | -| All rows from both sides, NULLs where they don't match | `FULL JOIN` | -| Every combination of left × right | `CROSS JOIN` | -| A row's relationship to other rows in the same table | self-join | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
You want…Use
Rows that match on both sidesINNER JOIN
All rows from the left, matches from the right where presentLEFT JOIN
Same, but with the tables in the other orderRIGHT JOIN
All rows from both sides, NULLs where they don't matchFULL JOIN
Every combination of left × rightCROSS JOIN
A row's relationship to other rows in the same tableself-join
A working rule: prefer `LEFT` over `RIGHT` so the "keep all" table is on the left. Reach for `FULL` only when both sides legitimately matter. Use `CROSS` deliberately — never by accident. diff --git a/package-lock.json b/package-lock.json index e63df2a..b2d27ed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -84,7 +84,6 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -298,7 +297,6 @@ "resolved": "https://registry.npmjs.org/@better-auth/core/-/core-1.6.11.tgz", "integrity": "sha512-LrwidLCV8azdMGjvtwp30nj9tIv1BwI3VhtC0UaGSjQkAVWw4bN42I8qwbxRziPeSQoj+zUVkOpxZzAWBDARtQ==", "license": "MIT", - "peer": true, "dependencies": { "@opentelemetry/semantic-conventions": "^1.39.0", "@standard-schema/spec": "^1.1.0", @@ -417,7 +415,6 @@ "resolved": "https://registry.npmjs.org/@better-auth/utils/-/utils-0.4.0.tgz", "integrity": "sha512-RpMtLUIQAEWMgdPLNVbIF5ON2mm+CH0U3rCdUCU1VyeAUui4m38DyK7/aXMLZov2YDjG684pS1D0MBllrmgjQA==", "license": "MIT", - "peer": true, "dependencies": { "@noble/hashes": "^2.0.1" } @@ -425,8 +422,7 @@ "node_modules/@better-fetch/fetch": { "version": "1.1.21", "resolved": "https://registry.npmjs.org/@better-fetch/fetch/-/fetch-1.1.21.tgz", - "integrity": "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A==", - "peer": true + "integrity": "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A==" }, "node_modules/@drizzle-team/brocli": { "version": "0.10.2", @@ -487,7 +483,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -504,7 +499,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -521,7 +515,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -538,7 +531,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -555,7 +547,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -572,7 +563,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -589,7 +579,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -606,7 +595,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -623,7 +611,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -640,7 +627,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -657,7 +643,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -674,7 +659,6 @@ "cpu": [ "mips64el" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -691,7 +675,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -708,7 +691,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -725,7 +707,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -742,7 +723,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -759,7 +739,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -776,7 +755,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -793,7 +771,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -810,7 +787,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -827,7 +803,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -844,7 +819,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -911,7 +885,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -928,7 +901,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -945,7 +917,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -962,7 +933,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -979,7 +949,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -996,7 +965,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1013,7 +981,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1030,7 +997,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1047,7 +1013,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1064,7 +1029,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1081,7 +1045,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1098,7 +1061,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1115,7 +1077,6 @@ "cpu": [ "mips64el" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1132,7 +1093,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1149,7 +1109,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1166,7 +1125,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1183,7 +1141,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1200,7 +1157,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1217,7 +1173,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1234,7 +1189,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1251,7 +1205,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1268,7 +1221,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1285,7 +1237,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1302,7 +1253,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1319,7 +1269,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1336,7 +1285,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2153,7 +2101,6 @@ "resolved": "https://registry.npmjs.org/@neondatabase/serverless/-/serverless-1.1.0.tgz", "integrity": "sha512-r3ZZhRjEcfEdKIZnoB1RusNgvHuaBRqfCzV4Gi+5A9yUX0S4HTws/ASWqt13wL4y4I+0rqsWGdA2w7EQXHi3+Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=19.0.0" } @@ -2791,7 +2738,6 @@ "integrity": "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@types/node": "*", "pg-protocol": "*", @@ -2803,7 +2749,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -2879,7 +2824,6 @@ "integrity": "sha512-HPwA+hVkfcriajbNvTmZv4VRauibay+cWArYUYq7u7W7PmGShMxbPxLvrwDme55a6d5alG3nrYfhyJ/G28XlLg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.59.3", "@typescript-eslint/types": "8.59.3", @@ -3425,7 +3369,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3852,7 +3795,6 @@ "resolved": "https://registry.npmjs.org/better-call/-/better-call-1.3.5.tgz", "integrity": "sha512-kOFJkBP7utAQLEYrobZm3vkTH8mXq5GNgvjc5/XEST1ilVHaxXUXfeDeFlqoETMtyqS4+3/h4ONX2i++ebZrvA==", "license": "MIT", - "peer": true, "dependencies": { "@better-auth/utils": "^0.4.0", "@better-fetch/fetch": "^1.1.21", @@ -3912,7 +3854,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -4379,7 +4320,6 @@ "integrity": "sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", @@ -4395,7 +4335,6 @@ "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.45.2.tgz", "integrity": "sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q==", "license": "Apache-2.0", - "peer": true, "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", @@ -4839,7 +4778,6 @@ "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -5025,7 +4963,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -5492,7 +5429,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -6452,7 +6388,6 @@ "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/panva" } @@ -6553,7 +6488,6 @@ "resolved": "https://registry.npmjs.org/kysely/-/kysely-0.28.17.tgz", "integrity": "sha512-nbD8lB9EB3wNdMhOCdx5Li8DxnLbvKByylRLcJ1h+4SkrowVeECAyZlyiKMThF7xFdRz0jSQ2MoJr+wXux2y0Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=20.0.0" } @@ -7789,7 +7723,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": "^20.0.0 || >=22.0.0" } @@ -7822,7 +7755,6 @@ "resolved": "https://registry.npmjs.org/next/-/next-16.2.6.tgz", "integrity": "sha512-qOVgKJg1+At15NpeUP+eJgCHvTCgXsogweq87Ri/Ix7PkqQHg4sdaXmSFqKlgaIXE4kW0g25LE68W87UANlHtw==", "license": "MIT", - "peer": true, "dependencies": { "@next/env": "16.2.6", "@swc/helpers": "0.5.15", @@ -8208,7 +8140,6 @@ "resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz", "integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==", "license": "MIT", - "peer": true, "dependencies": { "pg-connection-string": "^2.12.0", "pg-pool": "^3.13.0", @@ -8472,7 +8403,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -8482,7 +8412,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -9336,8 +9265,7 @@ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.0.tgz", "integrity": "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/tapable": { "version": "2.3.3", @@ -9394,7 +9322,6 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -9507,7 +9434,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9524,7 +9450,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9541,7 +9466,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9558,7 +9482,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9575,7 +9498,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9592,7 +9514,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9609,7 +9530,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9626,7 +9546,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9643,7 +9562,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9660,7 +9578,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9677,7 +9594,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9694,7 +9610,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9711,7 +9626,6 @@ "cpu": [ "mips64el" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9728,7 +9642,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9745,7 +9658,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9762,7 +9674,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9779,7 +9690,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9796,7 +9706,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9813,7 +9722,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9830,7 +9738,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9847,7 +9754,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9864,7 +9770,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9881,7 +9786,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9898,7 +9802,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9915,7 +9818,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -9932,7 +9834,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -10081,7 +9982,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -10565,7 +10465,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } From 08d844e7c365ae994554cf5fa8c1eedcd94572f7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 28 May 2026 16:05:53 +0000 Subject: [PATCH 3/3] Add remark-gfm to enable GFM table rendering in MDX --- app/lessons/[slug]/page.tsx | 3 +- .../02-advanced-joins/lesson.mdx | 42 +-- package-lock.json | 294 ++++++++++++++++++ package.json | 1 + 4 files changed, 305 insertions(+), 35 deletions(-) diff --git a/app/lessons/[slug]/page.tsx b/app/lessons/[slug]/page.tsx index 92df4a6..b9265f5 100644 --- a/app/lessons/[slug]/page.tsx +++ b/app/lessons/[slug]/page.tsx @@ -3,6 +3,7 @@ import Link from "next/link"; import { headers } from "next/headers"; import { notFound } from "next/navigation"; import { MDXRemote } from "next-mdx-remote/rsc"; +import remarkGfm from "remark-gfm"; import { auth } from "@/lib/auth"; import { getAllLessons, getLesson } from "@/lib/lessons"; import { buildLessonComponents } from "@/components/lesson/mdx-components"; @@ -88,7 +89,7 @@ export default async function LessonPage({
- + {(prev || next) && (