@pgpm/reserved-names 0.0.0-stage → 0.47.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/LICENSE +22 -0
  2. package/Makefile +6 -0
  3. package/README.md +81 -2
  4. package/deploy/schemas/reserved_names/procedures/is_reserved.sql +16 -0
  5. package/deploy/schemas/reserved_names/procedures/reserved_by.sql +23 -0
  6. package/deploy/schemas/reserved_names/schema.sql +9 -0
  7. package/deploy/schemas/reserved_names/tables/kinds/fixtures/1792800000000_fixture.sql +14 -0
  8. package/deploy/schemas/reserved_names/tables/kinds/table.sql +16 -0
  9. package/deploy/schemas/reserved_names/tables/names/fixtures/1792800000002_fixture.sql +72 -0
  10. package/deploy/schemas/reserved_names/tables/names/indexes/names_reason_idx.sql +10 -0
  11. package/deploy/schemas/reserved_names/tables/names/table.sql +23 -0
  12. package/deploy/schemas/reserved_names/tables/reasons/fixtures/1792800000001_fixture.sql +14 -0
  13. package/deploy/schemas/reserved_names/tables/reasons/table.sql +16 -0
  14. package/package.json +37 -4
  15. package/pgpm-reserved-names.control +7 -0
  16. package/pgpm.plan +14 -0
  17. package/revert/schemas/reserved_names/procedures/is_reserved.sql +7 -0
  18. package/revert/schemas/reserved_names/procedures/reserved_by.sql +7 -0
  19. package/revert/schemas/reserved_names/schema.sql +7 -0
  20. package/revert/schemas/reserved_names/tables/kinds/fixtures/1792800000000_fixture.sql +7 -0
  21. package/revert/schemas/reserved_names/tables/kinds/table.sql +7 -0
  22. package/revert/schemas/reserved_names/tables/names/fixtures/1792800000002_fixture.sql +7 -0
  23. package/revert/schemas/reserved_names/tables/names/indexes/names_reason_idx.sql +7 -0
  24. package/revert/schemas/reserved_names/tables/names/table.sql +7 -0
  25. package/revert/schemas/reserved_names/tables/reasons/fixtures/1792800000001_fixture.sql +7 -0
  26. package/revert/schemas/reserved_names/tables/reasons/table.sql +7 -0
  27. package/sql/pgpm-reserved-names--0.47.5.bundle.tar.gz +0 -0
  28. package/sql/pgpm-reserved-names--0.47.5.sql +157 -0
  29. package/verify/schemas/reserved_names/procedures/is_reserved.sql +7 -0
  30. package/verify/schemas/reserved_names/procedures/reserved_by.sql +7 -0
  31. package/verify/schemas/reserved_names/schema.sql +7 -0
  32. package/verify/schemas/reserved_names/tables/kinds/fixtures/1792800000000_fixture.sql +7 -0
  33. package/verify/schemas/reserved_names/tables/kinds/table.sql +7 -0
  34. package/verify/schemas/reserved_names/tables/names/fixtures/1792800000002_fixture.sql +7 -0
  35. package/verify/schemas/reserved_names/tables/names/indexes/names_reason_idx.sql +7 -0
  36. package/verify/schemas/reserved_names/tables/names/table.sql +7 -0
  37. package/verify/schemas/reserved_names/tables/reasons/fixtures/1792800000001_fixture.sql +7 -0
  38. package/verify/schemas/reserved_names/tables/reasons/table.sql +7 -0
package/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 Dan Lynch <pyramation@gmail.com>
4
+ Copyright (c) 2025 Constructive
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
package/Makefile ADDED
@@ -0,0 +1,6 @@
1
+ EXTENSION = pgpm-reserved-names
2
+ DATA = sql/pgpm-reserved-names--0.47.5.sql
3
+
4
+ PG_CONFIG = pg_config
5
+ PGXS := $(shell $(PG_CONFIG) --pgxs)
6
+ include $(PGXS)
package/README.md CHANGED
@@ -1,3 +1,82 @@
1
- # Temporary Holding Version
1
+ # @pgpm/reserved-names
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ <p align="center" width="100%">
4
+ <img height="250" src="https://raw.githubusercontent.com/constructive-io/constructive/refs/heads/main/assets/outline-logo.svg" />
5
+ </p>
6
+
7
+ <p align="center" width="100%">
8
+ <a href="https://github.com/constructive-io/pgpm-modules/actions/workflows/ci.yml">
9
+ <img height="20" src="https://github.com/constructive-io/pgpm-modules/actions/workflows/ci.yml/badge.svg" />
10
+ </a>
11
+ <a href="https://github.com/constructive-io/pgpm-modules/blob/main/LICENSE"><img height="20" src="https://img.shields.io/badge/license-MIT-blue.svg"/></a>
12
+ <a href="https://www.npmjs.com/package/@pgpm/reserved-names"><img height="20" src="https://img.shields.io/github/package-json/v/constructive-io/pgpm-modules?filename=packages%2Freserved-names%2Fpackage.json"/></a>
13
+ </p>
14
+
15
+ Reserved names that nobody may claim — subdomains, usernames, slugs, or any other kind — kept as rows and checked live by one function.
16
+
17
+ ## Overview
18
+
19
+ `@pgpm/reserved-names` keeps one shared list, as tables, and callers check it live at the moment someone claims a name. A new entry takes effect at once for every caller, with no regeneration or redeploy of the callers.
20
+
21
+ | table | what it holds |
22
+ | --- | --- |
23
+ | `reserved_names.kinds` | categories of name: `subdomain`, `username`, `domain`, `slug` (add more with a migration) |
24
+ | `reserved_names.reasons` | why a name is reserved: `platform`, `brand`, `lookalike`, `abuse` |
25
+ | `reserved_names.names` | the entries: `kind`, `name` (lowercase), `match` (`exact` / `prefix` / `suffix`), `reason` |
26
+
27
+ Every entry must point at a real kind and reason (foreign keys), so a typo cannot quietly create an empty category. The primary key is `(kind, name, match)`.
28
+
29
+ The starter data, in fixtures, is the four kinds and four reasons plus a `subdomain` list: hostnames a visitor would read as the platform's own under a shared domain (`login`, `admin`, `www`, `mail`, `ns1`, …), and a `xn--` prefix entry that blocks punycode lookalikes (`аpple` with a Cyrillic `а`). The other kinds start empty.
30
+
31
+ ## Installation
32
+
33
+ ```bash
34
+ pgpm install @pgpm/reserved-names
35
+ pgpm deploy
36
+ ```
37
+
38
+ ## Usage
39
+
40
+ ```sql
41
+ SELECT reserved_names.is_reserved('subdomain', 'login'); -- true
42
+ SELECT reserved_names.is_reserved('subdomain', 'LOGIN'); -- true (case-insensitive)
43
+ SELECT reserved_names.is_reserved('subdomain', 'xn--pple-43d'); -- true (prefix entry)
44
+ SELECT reserved_names.is_reserved('subdomain', 'mystore'); -- false
45
+ SELECT reserved_names.is_reserved('username', 'login'); -- false (kinds are separate)
46
+
47
+ SELECT name, match, reason FROM reserved_names.reserved_by('subdomain', 'xn--pple-43d');
48
+ -- xn-- | prefix | lookalike
49
+ ```
50
+
51
+ - `is_reserved(kind, name) RETURNS boolean`: true when any entry matches.
52
+ - `reserved_by(kind, name) RETURNS SETOF reserved_names.names`: the matching entries, so an error can say why.
53
+
54
+ Both are `STABLE` SQL, lowercase `kind` and `name` before comparing, and treat `NULL` as not reserved. Matching:
55
+
56
+ - `exact`: the name itself.
57
+ - `prefix`: every name starting with it (`xn--`, `staff-`).
58
+ - `suffix`: every name ending with it. `.example.com` blocks `shop.example.com`, but not `example.com` itself (add an `exact` entry for that) and not `notexample.com`.
59
+
60
+ ### Adding names
61
+
62
+ The list changes only through migrations. Add a pgpm fixture change that inserts rows:
63
+
64
+ ```sql
65
+ INSERT INTO reserved_names.names (kind, name, match, reason) VALUES
66
+ ('subdomain', 'www2', 'exact', 'platform'),
67
+ ('subdomain', 'staff-', 'prefix', 'platform'),
68
+ ('domain', '.example.com', 'suffix', 'brand'),
69
+ ('username', 'support', 'exact', 'platform');
70
+ ```
71
+
72
+ A new category or reason is a row in `kinds` or `reasons` first.
73
+
74
+ ## Access
75
+
76
+ Everyone can read the three tables and call both functions, so a UI can say "that name is reserved" before submitting. The list is public, so there is no row-level security. No role is granted `INSERT`, `UPDATE` or `DELETE`: only the owner (migrations, or a `SECURITY DEFINER` function it owns) can change it.
77
+
78
+ ## Testing
79
+
80
+ ```bash
81
+ pnpm test
82
+ ```
@@ -0,0 +1,16 @@
1
+ -- Deploy schemas/reserved_names/procedures/is_reserved to pg
2
+
3
+ -- requires: schemas/reserved_names/schema
4
+ -- requires: schemas/reserved_names/procedures/reserved_by
5
+
6
+ BEGIN;
7
+
8
+ CREATE FUNCTION reserved_names.is_reserved(kind text, name text) RETURNS boolean AS $$
9
+ SELECT EXISTS (SELECT 1 FROM reserved_names.reserved_by(is_reserved.kind, is_reserved.name));
10
+ $$ LANGUAGE sql STABLE;
11
+
12
+ COMMENT ON FUNCTION reserved_names.is_reserved(text, text) IS 'True when name (any case) is reserved for kind';
13
+
14
+ GRANT EXECUTE ON FUNCTION reserved_names.is_reserved(text, text) TO public;
15
+
16
+ COMMIT;
@@ -0,0 +1,23 @@
1
+ -- Deploy schemas/reserved_names/procedures/reserved_by to pg
2
+
3
+ -- requires: schemas/reserved_names/schema
4
+ -- requires: schemas/reserved_names/tables/names/table
5
+
6
+ BEGIN;
7
+
8
+ CREATE FUNCTION reserved_names.reserved_by(kind text, name text) RETURNS SETOF reserved_names.names AS $$
9
+ SELECT n.*
10
+ FROM reserved_names.names n
11
+ WHERE n.kind = pg_catalog.lower(reserved_by.kind)
12
+ AND CASE n.match
13
+ WHEN 'exact' THEN n.name = pg_catalog.lower(reserved_by.name)
14
+ WHEN 'prefix' THEN pg_catalog.starts_with(pg_catalog.lower(reserved_by.name), n.name)
15
+ WHEN 'suffix' THEN pg_catalog.right(pg_catalog.lower(reserved_by.name), pg_catalog.length(n.name)) = n.name
16
+ END;
17
+ $$ LANGUAGE sql STABLE;
18
+
19
+ COMMENT ON FUNCTION reserved_names.reserved_by(text, text) IS 'The entries (with their reason) that reserve name for kind; empty when it is free';
20
+
21
+ GRANT EXECUTE ON FUNCTION reserved_names.reserved_by(text, text) TO public;
22
+
23
+ COMMIT;
@@ -0,0 +1,9 @@
1
+ -- Deploy schemas/reserved_names/schema to pg
2
+
3
+ BEGIN;
4
+
5
+ CREATE SCHEMA reserved_names;
6
+
7
+ GRANT USAGE ON SCHEMA reserved_names TO public;
8
+
9
+ COMMIT;
@@ -0,0 +1,14 @@
1
+ -- Deploy schemas/reserved_names/tables/kinds/fixtures/1792800000000_fixture to pg
2
+
3
+ -- requires: schemas/reserved_names/schema
4
+ -- requires: schemas/reserved_names/tables/kinds/table
5
+
6
+ BEGIN;
7
+
8
+ INSERT INTO reserved_names.kinds (kind, description) VALUES
9
+ ('subdomain', 'A label under a shared domain, like login in login.example.com'),
10
+ ('username', 'A person''s handle'),
11
+ ('domain', 'A whole domain name, like example.com'),
12
+ ('slug', 'A name in a URL path, like an organization or app slug');
13
+
14
+ COMMIT;
@@ -0,0 +1,16 @@
1
+ -- Deploy schemas/reserved_names/tables/kinds/table to pg
2
+
3
+ -- requires: schemas/reserved_names/schema
4
+
5
+ BEGIN;
6
+
7
+ CREATE TABLE reserved_names.kinds (
8
+ kind text PRIMARY KEY CHECK (kind ~ '^[a-z][a-z0-9_]*$'),
9
+ description text NOT NULL
10
+ );
11
+
12
+ COMMENT ON TABLE reserved_names.kinds IS 'Categories of names that can be reserved: subdomain, username, domain, slug, ...';
13
+
14
+ GRANT SELECT ON TABLE reserved_names.kinds TO public;
15
+
16
+ COMMIT;
@@ -0,0 +1,72 @@
1
+ -- Deploy schemas/reserved_names/tables/names/fixtures/1792800000002_fixture to pg
2
+
3
+ -- requires: schemas/reserved_names/schema
4
+ -- requires: schemas/reserved_names/tables/names/table
5
+ -- requires: schemas/reserved_names/tables/kinds/fixtures/1792800000000_fixture
6
+ -- requires: schemas/reserved_names/tables/reasons/fixtures/1792800000001_fixture
7
+
8
+ BEGIN;
9
+
10
+ INSERT INTO reserved_names.names (kind, name, match, reason) VALUES
11
+ ('subdomain', 'www', 'exact', 'platform'),
12
+ ('subdomain', 'api', 'exact', 'platform'),
13
+ ('subdomain', 'auth', 'exact', 'platform'),
14
+ ('subdomain', 'login', 'exact', 'platform'),
15
+ ('subdomain', 'logout', 'exact', 'platform'),
16
+ ('subdomain', 'signin', 'exact', 'platform'),
17
+ ('subdomain', 'signup', 'exact', 'platform'),
18
+ ('subdomain', 'register', 'exact', 'platform'),
19
+ ('subdomain', 'admin', 'exact', 'platform'),
20
+ ('subdomain', 'administrator', 'exact', 'platform'),
21
+ ('subdomain', 'root', 'exact', 'platform'),
22
+ ('subdomain', 'app', 'exact', 'platform'),
23
+ ('subdomain', 'apps', 'exact', 'platform'),
24
+ ('subdomain', 'dashboard', 'exact', 'platform'),
25
+ ('subdomain', 'console', 'exact', 'platform'),
26
+ ('subdomain', 'account', 'exact', 'platform'),
27
+ ('subdomain', 'accounts', 'exact', 'platform'),
28
+ ('subdomain', 'billing', 'exact', 'platform'),
29
+ ('subdomain', 'pay', 'exact', 'platform'),
30
+ ('subdomain', 'payments', 'exact', 'platform'),
31
+ ('subdomain', 'checkout', 'exact', 'platform'),
32
+ ('subdomain', 'support', 'exact', 'platform'),
33
+ ('subdomain', 'help', 'exact', 'platform'),
34
+ ('subdomain', 'status', 'exact', 'platform'),
35
+ ('subdomain', 'docs', 'exact', 'platform'),
36
+ ('subdomain', 'mail', 'exact', 'platform'),
37
+ ('subdomain', 'email', 'exact', 'platform'),
38
+ ('subdomain', 'smtp', 'exact', 'platform'),
39
+ ('subdomain', 'imap', 'exact', 'platform'),
40
+ ('subdomain', 'ftp', 'exact', 'platform'),
41
+ ('subdomain', 'ns', 'exact', 'platform'),
42
+ ('subdomain', 'ns1', 'exact', 'platform'),
43
+ ('subdomain', 'ns2', 'exact', 'platform'),
44
+ ('subdomain', 'mx', 'exact', 'platform'),
45
+ ('subdomain', 'cdn', 'exact', 'platform'),
46
+ ('subdomain', 'static', 'exact', 'platform'),
47
+ ('subdomain', 'assets', 'exact', 'platform'),
48
+ ('subdomain', 'files', 'exact', 'platform'),
49
+ ('subdomain', 'storage', 'exact', 'platform'),
50
+ ('subdomain', 'media', 'exact', 'platform'),
51
+ ('subdomain', 'ws', 'exact', 'platform'),
52
+ ('subdomain', 'wss', 'exact', 'platform'),
53
+ ('subdomain', 'graphql', 'exact', 'platform'),
54
+ ('subdomain', 'modules', 'exact', 'platform'),
55
+ ('subdomain', 'agent', 'exact', 'platform'),
56
+ ('subdomain', 'compute', 'exact', 'platform'),
57
+ ('subdomain', 'machines', 'exact', 'platform'),
58
+ ('subdomain', 'cluster', 'exact', 'platform'),
59
+ ('subdomain', 'migrate', 'exact', 'platform'),
60
+ ('subdomain', 'internal', 'exact', 'platform'),
61
+ ('subdomain', 'system', 'exact', 'platform'),
62
+ ('subdomain', 'platform', 'exact', 'platform'),
63
+ ('subdomain', 'security', 'exact', 'platform'),
64
+ ('subdomain', 'sso', 'exact', 'platform'),
65
+ ('subdomain', 'oauth', 'exact', 'platform'),
66
+ ('subdomain', 'id', 'exact', 'platform'),
67
+ ('subdomain', 'identity', 'exact', 'platform'),
68
+ ('subdomain', 'verify', 'exact', 'platform'),
69
+ ('subdomain', 'secure', 'exact', 'platform'),
70
+ ('subdomain', 'xn--', 'prefix', 'lookalike');
71
+
72
+ COMMIT;
@@ -0,0 +1,10 @@
1
+ -- Deploy schemas/reserved_names/tables/names/indexes/names_reason_idx to pg
2
+
3
+ -- requires: schemas/reserved_names/schema
4
+ -- requires: schemas/reserved_names/tables/names/table
5
+
6
+ BEGIN;
7
+
8
+ CREATE INDEX names_reason_idx ON reserved_names.names (reason);
9
+
10
+ COMMIT;
@@ -0,0 +1,23 @@
1
+ -- Deploy schemas/reserved_names/tables/names/table to pg
2
+
3
+ -- requires: schemas/reserved_names/schema
4
+ -- requires: schemas/reserved_names/tables/kinds/table
5
+ -- requires: schemas/reserved_names/tables/reasons/table
6
+
7
+ BEGIN;
8
+
9
+ CREATE TABLE reserved_names.names (
10
+ kind text NOT NULL REFERENCES reserved_names.kinds (kind),
11
+ name text NOT NULL CHECK (name <> '' AND name = lower(name)),
12
+ match text NOT NULL DEFAULT 'exact' CHECK (match IN ('exact', 'prefix', 'suffix')),
13
+ reason text NOT NULL REFERENCES reserved_names.reasons (reason),
14
+ PRIMARY KEY (kind, name, match)
15
+ );
16
+
17
+ COMMENT ON TABLE reserved_names.names IS 'Names nobody may claim, by kind. Changed only by migrations.';
18
+ COMMENT ON COLUMN reserved_names.names.name IS 'The reserved name, lowercase';
19
+ COMMENT ON COLUMN reserved_names.names.match IS 'exact: the name itself; prefix: every name starting with it; suffix: every name ending with it';
20
+
21
+ GRANT SELECT ON TABLE reserved_names.names TO public;
22
+
23
+ COMMIT;
@@ -0,0 +1,14 @@
1
+ -- Deploy schemas/reserved_names/tables/reasons/fixtures/1792800000001_fixture to pg
2
+
3
+ -- requires: schemas/reserved_names/schema
4
+ -- requires: schemas/reserved_names/tables/reasons/table
5
+
6
+ BEGIN;
7
+
8
+ INSERT INTO reserved_names.reasons (reason, description) VALUES
9
+ ('platform', 'Reads as the platform''s own (login, admin, status, ...)'),
10
+ ('brand', 'Our brand or product names'),
11
+ ('lookalike', 'Can be made to look like another name, e.g. punycode'),
12
+ ('abuse', 'Offensive or used for abuse');
13
+
14
+ COMMIT;
@@ -0,0 +1,16 @@
1
+ -- Deploy schemas/reserved_names/tables/reasons/table to pg
2
+
3
+ -- requires: schemas/reserved_names/schema
4
+
5
+ BEGIN;
6
+
7
+ CREATE TABLE reserved_names.reasons (
8
+ reason text PRIMARY KEY CHECK (reason ~ '^[a-z][a-z0-9_]*$'),
9
+ description text NOT NULL
10
+ );
11
+
12
+ COMMENT ON TABLE reserved_names.reasons IS 'Why a name is reserved: platform, brand, lookalike, abuse, ...';
13
+
14
+ GRANT SELECT ON TABLE reserved_names.reasons TO public;
15
+
16
+ COMMIT;
package/package.json CHANGED
@@ -1,6 +1,39 @@
1
1
  {
2
2
  "name": "@pgpm/reserved-names",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.47.5",
4
+ "description": "Reserved names (subdomains, usernames, domains, slugs, ...) that nobody may claim, as tables checked live by reserved_names.is_reserved(kind, name)",
5
+ "author": "Dan Lynch <pyramation@gmail.com>",
6
+ "contributors": [
7
+ "Constructive <developers@constructive.io>"
8
+ ],
9
+ "keywords": [
10
+ "postgresql",
11
+ "pgpm",
12
+ "reserved",
13
+ "names",
14
+ "subdomains"
15
+ ],
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "scripts": {
20
+ "bundle": "pgpm package",
21
+ "test": "jest",
22
+ "test:watch": "jest --watch"
23
+ },
24
+ "dependencies": {
25
+ "@pgpm/verify": "0.47.1"
26
+ },
27
+ "devDependencies": {
28
+ "pgpm": "^5"
29
+ },
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "https://github.com/constructive-io/pgpm-modules"
33
+ },
34
+ "homepage": "https://github.com/constructive-io/pgpm-modules",
35
+ "bugs": {
36
+ "url": "https://github.com/constructive-io/pgpm-modules/issues"
37
+ },
38
+ "gitHead": "6b16b597af00c20dc2ca9b21c15bda69b4ce1186"
39
+ }
@@ -0,0 +1,7 @@
1
+ # pgpm-reserved-names extension
2
+ comment = 'pgpm-reserved-names extension'
3
+ default_version = '0.47.5'
4
+ module_pathname = '$libdir/pgpm-reserved-names'
5
+ requires = 'plpgsql,pgpm-verify'
6
+ relocatable = false
7
+ superuser = false
package/pgpm.plan ADDED
@@ -0,0 +1,14 @@
1
+ %syntax-version=1.0.0
2
+ %project=pgpm-reserved-names
3
+ %uri=pgpm-reserved-names
4
+
5
+ schemas/reserved_names/schema [pgpm-verify:procedures/assert_function] 2026-10-22T19:40:00Z devin <devin@cognition.ai> # add schemas/reserved_names/schema
6
+ schemas/reserved_names/tables/kinds/table [schemas/reserved_names/schema] 2026-10-22T19:40:00Z devin <devin@cognition.ai> # add schemas/reserved_names/tables/kinds/table
7
+ schemas/reserved_names/tables/reasons/table [schemas/reserved_names/schema] 2026-10-22T19:40:00Z devin <devin@cognition.ai> # add schemas/reserved_names/tables/reasons/table
8
+ schemas/reserved_names/tables/names/table [schemas/reserved_names/schema schemas/reserved_names/tables/kinds/table schemas/reserved_names/tables/reasons/table] 2026-10-22T19:40:00Z devin <devin@cognition.ai> # add schemas/reserved_names/tables/names/table
9
+ schemas/reserved_names/tables/names/indexes/names_reason_idx [schemas/reserved_names/schema schemas/reserved_names/tables/names/table] 2026-10-22T19:40:00Z devin <devin@cognition.ai> # add schemas/reserved_names/tables/names/indexes/names_reason_idx
10
+ schemas/reserved_names/procedures/reserved_by [schemas/reserved_names/schema schemas/reserved_names/tables/names/table] 2026-10-22T19:40:00Z devin <devin@cognition.ai> # add schemas/reserved_names/procedures/reserved_by
11
+ schemas/reserved_names/procedures/is_reserved [schemas/reserved_names/schema schemas/reserved_names/procedures/reserved_by] 2026-10-22T19:40:00Z devin <devin@cognition.ai> # add schemas/reserved_names/procedures/is_reserved
12
+ schemas/reserved_names/tables/kinds/fixtures/1792800000000_fixture [schemas/reserved_names/schema schemas/reserved_names/tables/kinds/table] 2026-10-22T19:40:00Z devin <devin@cognition.ai> # add schemas/reserved_names/tables/kinds/fixtures/1792800000000_fixture
13
+ schemas/reserved_names/tables/reasons/fixtures/1792800000001_fixture [schemas/reserved_names/schema schemas/reserved_names/tables/reasons/table] 2026-10-22T19:40:00Z devin <devin@cognition.ai> # add schemas/reserved_names/tables/reasons/fixtures/1792800000001_fixture
14
+ schemas/reserved_names/tables/names/fixtures/1792800000002_fixture [schemas/reserved_names/schema schemas/reserved_names/tables/names/table schemas/reserved_names/tables/kinds/fixtures/1792800000000_fixture schemas/reserved_names/tables/reasons/fixtures/1792800000001_fixture] 2026-10-22T19:40:00Z devin <devin@cognition.ai> # add schemas/reserved_names/tables/names/fixtures/1792800000002_fixture
@@ -0,0 +1,7 @@
1
+ -- Revert schemas/reserved_names/procedures/is_reserved from pg
2
+
3
+ BEGIN;
4
+
5
+ DROP FUNCTION reserved_names.is_reserved(text, text);
6
+
7
+ COMMIT;
@@ -0,0 +1,7 @@
1
+ -- Revert schemas/reserved_names/procedures/reserved_by from pg
2
+
3
+ BEGIN;
4
+
5
+ DROP FUNCTION reserved_names.reserved_by(text, text);
6
+
7
+ COMMIT;
@@ -0,0 +1,7 @@
1
+ -- Revert schemas/reserved_names/schema from pg
2
+
3
+ BEGIN;
4
+
5
+ DROP SCHEMA reserved_names;
6
+
7
+ COMMIT;
@@ -0,0 +1,7 @@
1
+ -- Revert schemas/reserved_names/tables/kinds/fixtures/1792800000000_fixture from pg
2
+
3
+ BEGIN;
4
+
5
+ DELETE FROM reserved_names.kinds WHERE kind IN ('subdomain', 'username', 'domain', 'slug');
6
+
7
+ COMMIT;
@@ -0,0 +1,7 @@
1
+ -- Revert schemas/reserved_names/tables/kinds/table from pg
2
+
3
+ BEGIN;
4
+
5
+ DROP TABLE reserved_names.kinds;
6
+
7
+ COMMIT;
@@ -0,0 +1,7 @@
1
+ -- Revert schemas/reserved_names/tables/names/fixtures/1792800000002_fixture from pg
2
+
3
+ BEGIN;
4
+
5
+ DELETE FROM reserved_names.names WHERE kind = 'subdomain';
6
+
7
+ COMMIT;
@@ -0,0 +1,7 @@
1
+ -- Revert schemas/reserved_names/tables/names/indexes/names_reason_idx from pg
2
+
3
+ BEGIN;
4
+
5
+ DROP INDEX reserved_names.names_reason_idx;
6
+
7
+ COMMIT;
@@ -0,0 +1,7 @@
1
+ -- Revert schemas/reserved_names/tables/names/table from pg
2
+
3
+ BEGIN;
4
+
5
+ DROP TABLE reserved_names.names;
6
+
7
+ COMMIT;
@@ -0,0 +1,7 @@
1
+ -- Revert schemas/reserved_names/tables/reasons/fixtures/1792800000001_fixture from pg
2
+
3
+ BEGIN;
4
+
5
+ DELETE FROM reserved_names.reasons WHERE reason IN ('platform', 'brand', 'lookalike', 'abuse');
6
+
7
+ COMMIT;
@@ -0,0 +1,7 @@
1
+ -- Revert schemas/reserved_names/tables/reasons/table from pg
2
+
3
+ BEGIN;
4
+
5
+ DROP TABLE reserved_names.reasons;
6
+
7
+ COMMIT;
@@ -0,0 +1,157 @@
1
+ \echo Use "CREATE EXTENSION pgpm-reserved-names" to load this file. \quit
2
+ CREATE SCHEMA reserved_names;
3
+
4
+ GRANT USAGE ON SCHEMA reserved_names TO PUBLIC;
5
+
6
+ CREATE TABLE reserved_names.kinds (
7
+ kind text PRIMARY KEY CHECK (kind ~ '^[a-z][a-z0-9_]*$'),
8
+ description text NOT NULL
9
+ );
10
+
11
+ COMMENT ON TABLE reserved_names.kinds IS 'Categories of names that can be reserved: subdomain, username, domain, slug, ...';
12
+
13
+ GRANT SELECT ON reserved_names.kinds TO PUBLIC;
14
+
15
+ CREATE TABLE reserved_names.reasons (
16
+ reason text PRIMARY KEY CHECK (reason ~ '^[a-z][a-z0-9_]*$'),
17
+ description text NOT NULL
18
+ );
19
+
20
+ COMMENT ON TABLE reserved_names.reasons IS 'Why a name is reserved: platform, brand, lookalike, abuse, ...';
21
+
22
+ GRANT SELECT ON reserved_names.reasons TO PUBLIC;
23
+
24
+ CREATE TABLE reserved_names.names (
25
+ kind text NOT NULL REFERENCES reserved_names.kinds (kind),
26
+ name text NOT NULL CHECK (
27
+ name <> ''
28
+ AND name = lower(name)
29
+ ),
30
+ match text NOT NULL DEFAULT 'exact' CHECK (match IN ('exact', 'prefix', 'suffix')),
31
+ reason text NOT NULL REFERENCES reserved_names.reasons (reason),
32
+ PRIMARY KEY (kind, name, match)
33
+ );
34
+
35
+ COMMENT ON TABLE reserved_names.names IS 'Names nobody may claim, by kind. Changed only by migrations.';
36
+
37
+ COMMENT ON COLUMN reserved_names.names.name IS 'The reserved name, lowercase';
38
+
39
+ COMMENT ON COLUMN reserved_names.names.match IS 'exact: the name itself; prefix: every name starting with it; suffix: every name ending with it';
40
+
41
+ GRANT SELECT ON reserved_names.names TO PUBLIC;
42
+
43
+ CREATE INDEX names_reason_idx ON reserved_names.names (reason);
44
+
45
+ CREATE FUNCTION reserved_names.reserved_by(
46
+ kind text,
47
+ name text
48
+ ) RETURNS SETOF reserved_names.names AS $EOFCODE$
49
+ SELECT n.*
50
+ FROM reserved_names.names n
51
+ WHERE n.kind = pg_catalog.lower(reserved_by.kind)
52
+ AND CASE n.match
53
+ WHEN 'exact' THEN n.name = pg_catalog.lower(reserved_by.name)
54
+ WHEN 'prefix' THEN pg_catalog.starts_with(pg_catalog.lower(reserved_by.name), n.name)
55
+ WHEN 'suffix' THEN pg_catalog.right(pg_catalog.lower(reserved_by.name), pg_catalog.length(n.name)) = n.name
56
+ END;
57
+ $EOFCODE$ LANGUAGE sql STABLE;
58
+
59
+ COMMENT ON FUNCTION reserved_names.reserved_by(text, text) IS 'The entries (with their reason) that reserve name for kind; empty when it is free';
60
+
61
+ GRANT EXECUTE ON FUNCTION reserved_names.reserved_by(text, text) TO PUBLIC;
62
+
63
+ CREATE FUNCTION reserved_names.is_reserved(
64
+ kind text,
65
+ name text
66
+ ) RETURNS boolean AS $EOFCODE$
67
+ SELECT EXISTS (SELECT 1 FROM reserved_names.reserved_by(is_reserved.kind, is_reserved.name));
68
+ $EOFCODE$ LANGUAGE sql STABLE;
69
+
70
+ COMMENT ON FUNCTION reserved_names.is_reserved(text, text) IS 'True when name (any case) is reserved for kind';
71
+
72
+ GRANT EXECUTE ON FUNCTION reserved_names.is_reserved(text, text) TO PUBLIC;
73
+
74
+ INSERT INTO reserved_names.kinds (
75
+ kind,
76
+ description
77
+ ) VALUES
78
+ ('subdomain', 'A label under a shared domain, like login in login.example.com'),
79
+ ('username', 'A person''s handle'),
80
+ ('domain', 'A whole domain name, like example.com'),
81
+ ('slug', 'A name in a URL path, like an organization or app slug');
82
+
83
+ INSERT INTO reserved_names.reasons (
84
+ reason,
85
+ description
86
+ ) VALUES
87
+ ('platform', 'Reads as the platform''s own (login, admin, status, ...)'),
88
+ ('brand', 'Our brand or product names'),
89
+ ('lookalike', 'Can be made to look like another name, e.g. punycode'),
90
+ ('abuse', 'Offensive or used for abuse');
91
+
92
+ INSERT INTO reserved_names.names (
93
+ kind,
94
+ name,
95
+ match,
96
+ reason
97
+ ) VALUES
98
+ ('subdomain', 'www', 'exact', 'platform'),
99
+ ('subdomain', 'api', 'exact', 'platform'),
100
+ ('subdomain', 'auth', 'exact', 'platform'),
101
+ ('subdomain', 'login', 'exact', 'platform'),
102
+ ('subdomain', 'logout', 'exact', 'platform'),
103
+ ('subdomain', 'signin', 'exact', 'platform'),
104
+ ('subdomain', 'signup', 'exact', 'platform'),
105
+ ('subdomain', 'register', 'exact', 'platform'),
106
+ ('subdomain', 'admin', 'exact', 'platform'),
107
+ ('subdomain', 'administrator', 'exact', 'platform'),
108
+ ('subdomain', 'root', 'exact', 'platform'),
109
+ ('subdomain', 'app', 'exact', 'platform'),
110
+ ('subdomain', 'apps', 'exact', 'platform'),
111
+ ('subdomain', 'dashboard', 'exact', 'platform'),
112
+ ('subdomain', 'console', 'exact', 'platform'),
113
+ ('subdomain', 'account', 'exact', 'platform'),
114
+ ('subdomain', 'accounts', 'exact', 'platform'),
115
+ ('subdomain', 'billing', 'exact', 'platform'),
116
+ ('subdomain', 'pay', 'exact', 'platform'),
117
+ ('subdomain', 'payments', 'exact', 'platform'),
118
+ ('subdomain', 'checkout', 'exact', 'platform'),
119
+ ('subdomain', 'support', 'exact', 'platform'),
120
+ ('subdomain', 'help', 'exact', 'platform'),
121
+ ('subdomain', 'status', 'exact', 'platform'),
122
+ ('subdomain', 'docs', 'exact', 'platform'),
123
+ ('subdomain', 'mail', 'exact', 'platform'),
124
+ ('subdomain', 'email', 'exact', 'platform'),
125
+ ('subdomain', 'smtp', 'exact', 'platform'),
126
+ ('subdomain', 'imap', 'exact', 'platform'),
127
+ ('subdomain', 'ftp', 'exact', 'platform'),
128
+ ('subdomain', 'ns', 'exact', 'platform'),
129
+ ('subdomain', 'ns1', 'exact', 'platform'),
130
+ ('subdomain', 'ns2', 'exact', 'platform'),
131
+ ('subdomain', 'mx', 'exact', 'platform'),
132
+ ('subdomain', 'cdn', 'exact', 'platform'),
133
+ ('subdomain', 'static', 'exact', 'platform'),
134
+ ('subdomain', 'assets', 'exact', 'platform'),
135
+ ('subdomain', 'files', 'exact', 'platform'),
136
+ ('subdomain', 'storage', 'exact', 'platform'),
137
+ ('subdomain', 'media', 'exact', 'platform'),
138
+ ('subdomain', 'ws', 'exact', 'platform'),
139
+ ('subdomain', 'wss', 'exact', 'platform'),
140
+ ('subdomain', 'graphql', 'exact', 'platform'),
141
+ ('subdomain', 'modules', 'exact', 'platform'),
142
+ ('subdomain', 'agent', 'exact', 'platform'),
143
+ ('subdomain', 'compute', 'exact', 'platform'),
144
+ ('subdomain', 'machines', 'exact', 'platform'),
145
+ ('subdomain', 'cluster', 'exact', 'platform'),
146
+ ('subdomain', 'migrate', 'exact', 'platform'),
147
+ ('subdomain', 'internal', 'exact', 'platform'),
148
+ ('subdomain', 'system', 'exact', 'platform'),
149
+ ('subdomain', 'platform', 'exact', 'platform'),
150
+ ('subdomain', 'security', 'exact', 'platform'),
151
+ ('subdomain', 'sso', 'exact', 'platform'),
152
+ ('subdomain', 'oauth', 'exact', 'platform'),
153
+ ('subdomain', 'id', 'exact', 'platform'),
154
+ ('subdomain', 'identity', 'exact', 'platform'),
155
+ ('subdomain', 'verify', 'exact', 'platform'),
156
+ ('subdomain', 'secure', 'exact', 'platform'),
157
+ ('subdomain', 'xn--', 'prefix', 'lookalike');
@@ -0,0 +1,7 @@
1
+ -- Verify schemas/reserved_names/procedures/is_reserved on pg
2
+
3
+ BEGIN;
4
+
5
+ SELECT assert_function('reserved_names.is_reserved(text, text)'::regprocedure);
6
+
7
+ ROLLBACK;
@@ -0,0 +1,7 @@
1
+ -- Verify schemas/reserved_names/procedures/reserved_by on pg
2
+
3
+ BEGIN;
4
+
5
+ SELECT assert_function('reserved_names.reserved_by(text, text)'::regprocedure);
6
+
7
+ ROLLBACK;
@@ -0,0 +1,7 @@
1
+ -- Verify schemas/reserved_names/schema on pg
2
+
3
+ BEGIN;
4
+
5
+ SELECT assert_schema('reserved_names'::regnamespace);
6
+
7
+ ROLLBACK;
@@ -0,0 +1,7 @@
1
+ -- Verify schemas/reserved_names/tables/kinds/fixtures/1792800000000_fixture on pg
2
+
3
+ BEGIN;
4
+
5
+ SELECT 1/count(*) FROM reserved_names.kinds WHERE kind = 'subdomain';
6
+
7
+ ROLLBACK;
@@ -0,0 +1,7 @@
1
+ -- Verify schemas/reserved_names/tables/kinds/table on pg
2
+
3
+ BEGIN;
4
+
5
+ SELECT assert_table('reserved_names.kinds'::regclass);
6
+
7
+ ROLLBACK;
@@ -0,0 +1,7 @@
1
+ -- Verify schemas/reserved_names/tables/names/fixtures/1792800000002_fixture on pg
2
+
3
+ BEGIN;
4
+
5
+ SELECT 1/count(*) FROM reserved_names.names WHERE kind = 'subdomain' AND name = 'login';
6
+
7
+ ROLLBACK;
@@ -0,0 +1,7 @@
1
+ -- Verify schemas/reserved_names/tables/names/indexes/names_reason_idx on pg
2
+
3
+ BEGIN;
4
+
5
+ SELECT assert_index('reserved_names.names_reason_idx'::regclass, 'reserved_names.names'::regclass);
6
+
7
+ ROLLBACK;
@@ -0,0 +1,7 @@
1
+ -- Verify schemas/reserved_names/tables/names/table on pg
2
+
3
+ BEGIN;
4
+
5
+ SELECT assert_table('reserved_names.names'::regclass);
6
+
7
+ ROLLBACK;
@@ -0,0 +1,7 @@
1
+ -- Verify schemas/reserved_names/tables/reasons/fixtures/1792800000001_fixture on pg
2
+
3
+ BEGIN;
4
+
5
+ SELECT 1/count(*) FROM reserved_names.reasons WHERE reason = 'platform';
6
+
7
+ ROLLBACK;
@@ -0,0 +1,7 @@
1
+ -- Verify schemas/reserved_names/tables/reasons/table on pg
2
+
3
+ BEGIN;
4
+
5
+ SELECT assert_table('reserved_names.reasons'::regclass);
6
+
7
+ ROLLBACK;