From 2de760a954977ed2cb40314207e0bc69db3756f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20P=C3=A9rez-Aradros=20Herce?= Date: Thu, 2 Jul 2026 17:05:29 +0200 Subject: [PATCH] Add lesson 42: Extensions Part of #6 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../03-extensions/lesson.mdx | 123 ++++++++++++++++++ .../03-extensions/lesson.yaml | 21 +++ .../03-extensions/seed.sql | 16 +++ lessons/10-expert-and-operations/module.yaml | 3 + 4 files changed, 163 insertions(+) create mode 100644 lessons/10-expert-and-operations/03-extensions/lesson.mdx create mode 100644 lessons/10-expert-and-operations/03-extensions/lesson.yaml create mode 100644 lessons/10-expert-and-operations/03-extensions/seed.sql create mode 100644 lessons/10-expert-and-operations/module.yaml diff --git a/lessons/10-expert-and-operations/03-extensions/lesson.mdx b/lessons/10-expert-and-operations/03-extensions/lesson.mdx new file mode 100644 index 0000000..bf710d6 --- /dev/null +++ b/lessons/10-expert-and-operations/03-extensions/lesson.mdx @@ -0,0 +1,123 @@ +Postgres ships a small core and lets everything else plug in as an *extension* — a packaged bundle of types, functions, operators, and even index support that you switch on with a single command. This lesson installs one (`citext`), watches it change how equality works, and then tours the extensions worth knowing about. + +The seed is a four-row `users` table whose emails were typed with inconsistent capitalization — the kind of mess an extension is about to clean up. + + +SELECT * FROM users ORDER BY id; + + +## What's on the shelf + +Every extension available to install shows up in the `pg_available_extensions` catalog. Have a look at what your server carries: + + +SELECT name, default_version, left(comment, 40) AS comment +FROM pg_available_extensions +ORDER BY name +LIMIT 12; + + +That's the catalog of what you *could* install. A separate catalog, `pg_extension`, lists what's actually installed in this database right now — and on a fresh database it's just `plpgsql`, the one that powers `CREATE FUNCTION`: + + +SELECT extname, extversion FROM pg_extension ORDER BY extname; + + +## The email problem + +Plain `text` compares byte-for-byte, so capitalization matters. Ada's email is stored as `Ada@Example.com`, and a lookup with the lowercase form finds nothing: + + +SELECT * FROM users WHERE email = 'ada@example.com'; + + +Zero rows. You could paper over it with `lower(email) = lower('ada@example.com')` on every query, but that's easy to forget and it can't back a `UNIQUE` constraint. A dedicated type is cleaner. + +## Install an extension + +`CREATE EXTENSION` loads a bundle into the current database. The `citext` extension adds a case-insensitive text type. Install it: + + +CREATE EXTENSION IF NOT EXISTS citext; + + +`IF NOT EXISTS` makes the command safe to run twice — a second call is a no-op instead of an error, which matters in migrations. Note extensions are installed *per database*: this `citext` lives in `lab` only, not server-wide. + +**Trusted vs untrusted.** You just installed `citext` without being a superuser. That works because `citext` is *trusted* — Postgres marks a curated set of safe extensions (`citext`, `pg_trgm`, `unaccent`, `hstore`, `fuzzystrmatch`, …) as installable by any role with `CREATE` on the database. *Untrusted* extensions reach outside the database — think PostGIS or anything loading C code or touching the filesystem — and still require a superuser. It's a security boundary: trusted extensions can't escalate privileges, untrusted ones might. + +Confirm it landed in `pg_extension`: + + +SELECT extname, extversion FROM pg_extension WHERE extname = 'citext'; + + +## citext in action + +Now the same value cast to `citext` compares case-insensitively. This is the whole point of the type — run it and watch it return true: + + +SELECT 'Ada@Example.com'::citext = 'ada@example.com'::citext AS same_email; + + +The comparison ignores case, so the two spellings are equal. To make the `users` table behave that way permanently, change the column's type: + + +ALTER TABLE users ALTER COLUMN email TYPE citext; + + +The lookup that returned nothing a moment ago now finds Ada, no `lower()` gymnastics required: + + +SELECT * FROM users WHERE email = 'ada@example.com'; + + +Because equality is case-insensitive, a `UNIQUE` index on a `citext` column also rejects `Bob@x.com` when `bob@x.com` already exists — the correct behavior for emails and usernames, and impossible to get from plain `text` without extra machinery. + +## Managing what you installed + +Three commands round out the lifecycle: + +```sql +ALTER EXTENSION citext UPDATE; -- move to the newest installed version +ALTER EXTENSION citext UPDATE TO '1.6'; -- or a specific one +DROP EXTENSION citext; -- remove it (fails if objects depend on it) +``` + +`ALTER EXTENSION … UPDATE` runs the packaged upgrade script after you install a new build of Postgres or the extension. `DROP EXTENSION` refuses if something still depends on it — you'd need `DROP EXTENSION citext CASCADE` to drop the dependents too, which is exactly as dangerous as it sounds. + +## The extensions worth knowing + +`citext` is a gentle introduction; the ecosystem is where Postgres earns its reputation. A few high-value, mostly trusted ones: + +- **`pg_trgm`** — trigram similarity. It powers fuzzy matching (`similarity('Postgres', 'Postgre') → 0.5`) and, crucially, a GIN index that makes `LIKE '%foo%'` and `ILIKE` searches fast. A leading-wildcard `LIKE` normally can't use a B-tree index at all; `pg_trgm` is the standard fix for substring search. +- **`unaccent`** — strips accents (`unaccent('café') → 'cafe'`), so a search for "cafe" also matches "café". Often paired with `pg_trgm` or full-text search. +- **`hstore`** — a key/value type inside a single column. It predates `jsonb` and is lighter when you only need flat string-to-string maps. +- **`postgis`** — the big one: geospatial types, spatial indexes, and hundreds of functions for maps, distances, and geometry. It's *untrusted* (it links large C libraries), so installing it needs a superuser: + +```sql +CREATE EXTENSION postgis; -- requires superuser; adds geometry/geography types +``` + +The pattern is always the same: browse `pg_available_extensions`, `CREATE EXTENSION` the one you need, and a whole domain of types, functions, and operators appears — no recompiling Postgres. + +## Your turn + +You already ran it once above, but make sure `citext` is installed — the check below verifies it: + + +CREATE EXTENSION IF NOT EXISTS citext; + + + +Run `CREATE EXTENSION IF NOT EXISTS citext`. We'll confirm citext shows up exactly once in `pg_extension`. + + +## What you learned + +- Extensions are packaged add-ons — types, functions, operators, index support — that ship with or plug into Postgres; `pg_available_extensions` lists what you can install, `pg_extension` lists what's installed in this database. +- `CREATE EXTENSION [IF NOT EXISTS] name [SCHEMA …]` installs one per database; `ALTER EXTENSION … UPDATE` upgrades it and `DROP EXTENSION` removes it. +- *Trusted* extensions (`citext`, `pg_trgm`, `unaccent`, `hstore`, …) install for any role with `CREATE` on the database; *untrusted* ones like PostGIS need a superuser because they reach outside it. +- `citext` gives case-insensitive equality and uniqueness — perfect for emails and usernames — with no `lower()` scattered through your queries. +- The ecosystem is the real payoff: `pg_trgm` for fuzzy/substring search, `unaccent` for accent-folding, `hstore` for key/value, PostGIS for geospatial. + +Up next: the capstone — diagnosing and fixing a slow real-world query. diff --git a/lessons/10-expert-and-operations/03-extensions/lesson.yaml b/lessons/10-expert-and-operations/03-extensions/lesson.yaml new file mode 100644 index 0000000..1cb34a7 --- /dev/null +++ b/lessons/10-expert-and-operations/03-extensions/lesson.yaml @@ -0,0 +1,21 @@ +title: Extensions +summary: Add packaged types, functions, and index support to Postgres with CREATE EXTENSION — including the trusted ones a non-superuser can install. +estimatedMinutes: 14 +tags: + - extensions + - create-extension + - citext + - pg-trgm + - postgis +authors: + - exekias +seed: seed.sql +checks: + - id: citext-installed + type: query-returns + description: Install the citext extension with CREATE EXTENSION IF NOT EXISTS citext. + sql: SELECT count(*) FROM pg_extension WHERE extname = 'citext' + expect: + rowCount: 1 + rows: + - [1] diff --git a/lessons/10-expert-and-operations/03-extensions/seed.sql b/lessons/10-expert-and-operations/03-extensions/seed.sql new file mode 100644 index 0000000..5acf27d --- /dev/null +++ b/lessons/10-expert-and-operations/03-extensions/seed.sql @@ -0,0 +1,16 @@ +-- Seed for "03-extensions": a tiny users table with plain-text emails. The +-- email column is ordinary text, so 'Ada@Example.com' and 'ada@example.com' +-- are two different values here — exactly the pain the citext extension fixes. +-- No CREATE EXTENSION in the seed: the learner installs citext in the lesson. + +CREATE TABLE users ( + id int GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + name text NOT NULL, + email text NOT NULL +); + +INSERT INTO users (name, email) VALUES + ('Ada Lovelace', 'Ada@Example.com'), + ('Grace Hopper', 'grace@example.com'), + ('Linus Torvalds', 'Linus@Example.com'), + ('Margaret Hamilton','margaret@example.com'); diff --git a/lessons/10-expert-and-operations/module.yaml b/lessons/10-expert-and-operations/module.yaml new file mode 100644 index 0000000..c902b7c --- /dev/null +++ b/lessons/10-expert-and-operations/module.yaml @@ -0,0 +1,3 @@ +title: Expert and operations +difficulty: advanced +summary: Operate Postgres with confidence — roles and row-level security, vacuum and bloat, extensions, and troubleshooting.