@coralai/sps-cli 0.42.0 → 0.44.0
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.
- package/README.md +59 -4
- package/dist/commands/consoleCommand.d.ts +2 -0
- package/dist/commands/consoleCommand.d.ts.map +1 -0
- package/dist/commands/consoleCommand.js +129 -0
- package/dist/commands/consoleCommand.js.map +1 -0
- package/dist/commands/projectInit.d.ts.map +1 -1
- package/dist/commands/projectInit.js +40 -53
- package/dist/commands/projectInit.js.map +1 -1
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +14 -2
- package/dist/commands/setup.js.map +1 -1
- package/dist/commands/skillCommand.d.ts +2 -0
- package/dist/commands/skillCommand.d.ts.map +1 -0
- package/dist/commands/skillCommand.js +235 -0
- package/dist/commands/skillCommand.js.map +1 -0
- package/dist/console-assets/assets/index-Bhd2f9AP.js +125 -0
- package/dist/console-assets/assets/index-bsAN2a12.css +1 -0
- package/dist/console-assets/index.html +16 -0
- package/dist/console-server/index.d.ts +29 -0
- package/dist/console-server/index.d.ts.map +1 -0
- package/dist/console-server/index.js +145 -0
- package/dist/console-server/index.js.map +1 -0
- package/dist/console-server/lib/lockFile.d.ts +17 -0
- package/dist/console-server/lib/lockFile.d.ts.map +1 -0
- package/dist/console-server/lib/lockFile.js +61 -0
- package/dist/console-server/lib/lockFile.js.map +1 -0
- package/dist/console-server/lib/portPicker.d.ts +3 -0
- package/dist/console-server/lib/portPicker.d.ts.map +1 -0
- package/dist/console-server/lib/portPicker.js +25 -0
- package/dist/console-server/lib/portPicker.js.map +1 -0
- package/dist/console-server/routes/projects.d.ts +11 -0
- package/dist/console-server/routes/projects.d.ts.map +1 -0
- package/dist/console-server/routes/projects.js +149 -0
- package/dist/console-server/routes/projects.js.map +1 -0
- package/dist/console-server/routes/system.d.ts +7 -0
- package/dist/console-server/routes/system.d.ts.map +1 -0
- package/dist/console-server/routes/system.js +19 -0
- package/dist/console-server/routes/system.js.map +1 -0
- package/dist/console-server/sse/eventBus.d.ts +25 -0
- package/dist/console-server/sse/eventBus.d.ts.map +1 -0
- package/dist/console-server/sse/eventBus.js +32 -0
- package/dist/console-server/sse/eventBus.js.map +1 -0
- package/dist/console-server/watchers/cardWatcher.d.ts +9 -0
- package/dist/console-server/watchers/cardWatcher.d.ts.map +1 -0
- package/dist/console-server/watchers/cardWatcher.js +42 -0
- package/dist/console-server/watchers/cardWatcher.js.map +1 -0
- package/dist/core/skillStore.d.ts +46 -0
- package/dist/core/skillStore.d.ts.map +1 -0
- package/dist/core/skillStore.js +210 -0
- package/dist/core/skillStore.js.map +1 -0
- package/dist/core/skillStore.test.d.ts +2 -0
- package/dist/core/skillStore.test.d.ts.map +1 -0
- package/dist/core/skillStore.test.js +203 -0
- package/dist/core/skillStore.test.js.map +1 -0
- package/dist/main.js +27 -17
- package/dist/main.js.map +1 -1
- package/package.json +8 -2
- package/skills/architecture-decision-records/SKILL.md +207 -0
- package/skills/backend/SKILL.md +62 -0
- package/skills/backend/references/api-design.md +168 -0
- package/skills/backend/references/caching.md +181 -0
- package/skills/backend/references/data-access.md +173 -0
- package/skills/backend/references/layering.md +181 -0
- package/skills/backend/references/observability.md +190 -0
- package/skills/backend/references/resilience.md +201 -0
- package/skills/backend/references/security.md +186 -0
- package/skills/backend-architect/SKILL.md +119 -0
- package/skills/code-reviewer/SKILL.md +143 -0
- package/skills/coding-standards/SKILL.md +60 -0
- package/skills/coding-standards/references/clean-code.md +258 -0
- package/skills/coding-standards/references/code-review.md +192 -0
- package/skills/coding-standards/references/commits-and-prs.md +226 -0
- package/skills/coding-standards/references/error-strategy.md +193 -0
- package/skills/coding-standards/references/naming.md +185 -0
- package/skills/coding-standards/references/tdd.md +171 -0
- package/skills/database/SKILL.md +53 -0
- package/skills/database/references/indexing.md +190 -0
- package/skills/database/references/migrations.md +199 -0
- package/skills/database/references/nosql.md +185 -0
- package/skills/database/references/queries.md +295 -0
- package/skills/database/references/scaling.md +203 -0
- package/skills/database/references/schema.md +191 -0
- package/skills/database-optimizer/SKILL.md +168 -0
- package/skills/debugging-workflow/SKILL.md +244 -0
- package/skills/devops/SKILL.md +55 -0
- package/skills/devops/references/ci-cd.md +204 -0
- package/skills/devops/references/containers.md +272 -0
- package/skills/devops/references/deploy.md +201 -0
- package/skills/devops/references/iac.md +252 -0
- package/skills/devops/references/observability.md +228 -0
- package/skills/devops/references/secrets.md +178 -0
- package/skills/devops-automator/SKILL.md +164 -0
- package/skills/frontend/SKILL.md +52 -0
- package/skills/frontend/references/accessibility.md +222 -0
- package/skills/frontend/references/components.md +206 -0
- package/skills/frontend/references/performance.md +219 -0
- package/skills/frontend/references/routing.md +209 -0
- package/skills/frontend/references/state.md +190 -0
- package/skills/frontend/references/testing.md +216 -0
- package/skills/frontend-developer/SKILL.md +115 -0
- package/skills/git-workflow/SKILL.md +355 -0
- package/skills/golang/SKILL.md +49 -0
- package/skills/golang/references/concurrency.md +284 -0
- package/skills/golang/references/errors.md +241 -0
- package/skills/golang/references/idioms.md +285 -0
- package/skills/golang/references/testing.md +238 -0
- package/skills/java/SKILL.md +50 -0
- package/skills/java/references/concurrency.md +194 -0
- package/skills/java/references/idioms.md +283 -0
- package/skills/java/references/testing.md +228 -0
- package/skills/kotlin/SKILL.md +47 -0
- package/skills/kotlin/references/coroutines.md +240 -0
- package/skills/kotlin/references/idioms.md +268 -0
- package/skills/kotlin/references/testing.md +219 -0
- package/skills/mobile/SKILL.md +50 -0
- package/skills/mobile/references/architecture.md +204 -0
- package/skills/mobile/references/navigation.md +158 -0
- package/skills/mobile/references/performance.md +152 -0
- package/skills/mobile/references/platform.md +166 -0
- package/skills/mobile/references/state-and-data.md +174 -0
- package/skills/python/SKILL.md +51 -0
- package/skills/python/THIRD_PARTY.md +14 -0
- package/skills/python/references/async.md +218 -0
- package/skills/python/references/error-handling.md +254 -0
- package/skills/python/references/idioms.md +279 -0
- package/skills/python/references/packaging.md +233 -0
- package/skills/python/references/testing.md +269 -0
- package/skills/python/references/typing.md +292 -0
- package/skills/qa-tester/SKILL.md +186 -0
- package/skills/rust/SKILL.md +50 -0
- package/skills/rust/references/async.md +224 -0
- package/skills/rust/references/errors.md +240 -0
- package/skills/rust/references/ownership.md +263 -0
- package/skills/rust/references/testing.md +274 -0
- package/skills/rust/references/traits.md +250 -0
- package/skills/security-engineer/SKILL.md +157 -0
- package/skills/swift/SKILL.md +48 -0
- package/skills/swift/references/concurrency.md +280 -0
- package/skills/swift/references/idioms.md +334 -0
- package/skills/swift/references/testing.md +229 -0
- package/skills/typescript/SKILL.md +51 -0
- package/skills/typescript/references/async.md +241 -0
- package/skills/typescript/references/errors.md +208 -0
- package/skills/typescript/references/idioms.md +246 -0
- package/skills/typescript/references/testing.md +225 -0
- package/skills/typescript/references/tooling.md +208 -0
- package/skills/typescript/references/types.md +259 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# TDD — Test-Driven Development
|
|
2
|
+
|
|
3
|
+
The cycle that keeps behavior ahead of implementation. Language-neutral.
|
|
4
|
+
|
|
5
|
+
## The cycle
|
|
6
|
+
|
|
7
|
+
1. **RED** — write the smallest failing test for the next bit of behavior.
|
|
8
|
+
2. **GREEN** — write the minimum code that makes it pass. Ugly is fine.
|
|
9
|
+
3. **REFACTOR** — clean up while keeping all tests green.
|
|
10
|
+
|
|
11
|
+
Repeat. Each loop should take minutes, not hours.
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
┌─────────┐ ┌──────────┐ ┌──────────┐
|
|
15
|
+
│ RED │────▶│ GREEN │────▶│ REFACTOR │──┐
|
|
16
|
+
│ (fail) │ │ (pass) │ │ (still │ │
|
|
17
|
+
└─────────┘ └──────────┘ │ pass) │ │
|
|
18
|
+
▲ └──────────┘ │
|
|
19
|
+
└─────────────────────────────────────────┘
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Why bother
|
|
23
|
+
|
|
24
|
+
- **You write less code.** You only write what a test demands.
|
|
25
|
+
- **You design the interface first.** Tests force you to use your API before anyone else does.
|
|
26
|
+
- **Regressions get names.** Every bug fix starts with a failing test that reproduces it.
|
|
27
|
+
- **Coverage is real.** Not a number chased after the fact.
|
|
28
|
+
|
|
29
|
+
TDD isn't about testing. It's about designing by answering "how would I use this?" before you build it.
|
|
30
|
+
|
|
31
|
+
## When TDD shines
|
|
32
|
+
|
|
33
|
+
- New business logic with clear inputs/outputs
|
|
34
|
+
- Fixing a bug (always: reproduce with a test first, then fix)
|
|
35
|
+
- Pure functions, state machines, parsers
|
|
36
|
+
- Rules-heavy code (pricing, validation, authz)
|
|
37
|
+
|
|
38
|
+
## When TDD is awkward
|
|
39
|
+
|
|
40
|
+
- UI polish (use snapshot / visual tests instead)
|
|
41
|
+
- Exploratory spikes (throw the code away; tests would be premature)
|
|
42
|
+
- Integration against an unknown third-party API (explore, then test)
|
|
43
|
+
- Glue code with no logic (one thin integration test is enough)
|
|
44
|
+
|
|
45
|
+
Awkward ≠ exempt. You still verify. You just choose the right tool.
|
|
46
|
+
|
|
47
|
+
## Writing the first test
|
|
48
|
+
|
|
49
|
+
Think: what's the smallest visible behavior?
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
# Feature: "sum two numbers"
|
|
53
|
+
|
|
54
|
+
# RED (smallest failing test)
|
|
55
|
+
assert add(2, 3) == 5
|
|
56
|
+
|
|
57
|
+
# GREEN (smallest passing code)
|
|
58
|
+
def add(a, b):
|
|
59
|
+
return a + b
|
|
60
|
+
|
|
61
|
+
# Now push the next edge case
|
|
62
|
+
assert add(-1, 1) == 0 # pass already — good
|
|
63
|
+
assert add(0.1, 0.2) == 0.3 # FAILS (float math) — now you've learned something real
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
If the first test passes without you writing code, either the feature exists, the test is vacuous, or you're testing the wrong thing. Stop and rethink.
|
|
67
|
+
|
|
68
|
+
## One assertion per behavior, not per test
|
|
69
|
+
|
|
70
|
+
A single test can have several assertions if they verify the same behavior from different angles:
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
# OK — all verifying "create returns a persisted user"
|
|
74
|
+
u = service.create(name="A", email="a@x.com")
|
|
75
|
+
assert u.id is not None
|
|
76
|
+
assert u.name == "A"
|
|
77
|
+
assert u.email == "a@x.com"
|
|
78
|
+
assert db.find(u.id) == u
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Not OK:
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
# Two behaviors in one test
|
|
85
|
+
u = service.create(...)
|
|
86
|
+
assert u.id is not None # behavior 1: create
|
|
87
|
+
service.delete(u.id)
|
|
88
|
+
assert db.find(u.id) is None # behavior 2: delete ← split
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
One test fails → one problem. Mixing behaviors makes failures ambiguous.
|
|
92
|
+
|
|
93
|
+
## Reproducing a bug
|
|
94
|
+
|
|
95
|
+
Every bug fix starts with a test that fails *because of the bug*.
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
# 1. Write the failing test (RED)
|
|
99
|
+
def test_negative_balance_rejected():
|
|
100
|
+
with pytest.raises(ValidationError):
|
|
101
|
+
account.withdraw(999_999)
|
|
102
|
+
|
|
103
|
+
# 2. Fix the code (GREEN)
|
|
104
|
+
|
|
105
|
+
# 3. Commit both in the same change
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Without the test, the bug can silently come back. With it, the regression is guaranteed to be caught.
|
|
109
|
+
|
|
110
|
+
## What a good test looks like
|
|
111
|
+
|
|
112
|
+
| Property | Why |
|
|
113
|
+
|---|---|
|
|
114
|
+
| **Fast** | You run it often; slow tests get skipped |
|
|
115
|
+
| **Isolated** | No order dependency; no shared mutable state |
|
|
116
|
+
| **Repeatable** | Same result every run; no flakes |
|
|
117
|
+
| **Self-verifying** | Pass/fail is automatic, not a human reading output |
|
|
118
|
+
| **Named for behavior** | `test_rejects_empty_email`, not `test_1` |
|
|
119
|
+
|
|
120
|
+
Flaky tests are worse than missing tests — they train you to ignore red.
|
|
121
|
+
|
|
122
|
+
## Test pyramid
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
▲
|
|
126
|
+
│ E2E ← few, slow, high-value
|
|
127
|
+
│ (5%)
|
|
128
|
+
│ Integration ← some, medium speed
|
|
129
|
+
│ (20%)
|
|
130
|
+
│ Unit ← many, fast
|
|
131
|
+
│ (75%)
|
|
132
|
+
▼
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Invert this (lots of E2E, no unit) and you get slow CI, brittle tests, and bugs that hide in the unit level. Unit tests are the backbone; E2E is the sanity check.
|
|
136
|
+
|
|
137
|
+
## Refactor only on green
|
|
138
|
+
|
|
139
|
+
Rule: never refactor with failing tests. You won't know whether the refactor broke something or the test was already broken.
|
|
140
|
+
|
|
141
|
+
Steps:
|
|
142
|
+
1. Get to green.
|
|
143
|
+
2. Commit.
|
|
144
|
+
3. Refactor.
|
|
145
|
+
4. Stay green.
|
|
146
|
+
5. Commit.
|
|
147
|
+
|
|
148
|
+
Small commits. Rollback is free.
|
|
149
|
+
|
|
150
|
+
## Coverage targets (sane defaults)
|
|
151
|
+
|
|
152
|
+
| Layer | Target |
|
|
153
|
+
|---|---|
|
|
154
|
+
| Pure / domain logic | ≥ 90% |
|
|
155
|
+
| Application / use cases | ≥ 80% |
|
|
156
|
+
| Adapters (DB, HTTP) | ≥ 60%; integration tests carry the rest |
|
|
157
|
+
| Glue (bootstrap, composition) | ≥ 0% — don't chase coverage here |
|
|
158
|
+
|
|
159
|
+
Coverage is a floor, not a ceiling. 100% coverage with weak assertions is still untested code.
|
|
160
|
+
|
|
161
|
+
## Anti-patterns
|
|
162
|
+
|
|
163
|
+
| Anti-pattern | Fix |
|
|
164
|
+
|---|---|
|
|
165
|
+
| Writing tests after the code "to hit coverage" | That's not TDD; and coverage isn't the goal |
|
|
166
|
+
| One mega-test per feature | Split by behavior |
|
|
167
|
+
| Testing mocks (the mock returns X, assert it returned X) | Test the thing, or delete the test |
|
|
168
|
+
| Asserting on implementation detail (private method called) | Assert on observable behavior |
|
|
169
|
+
| Tests that sleep to wait for async | Use deterministic scheduling / fake clocks |
|
|
170
|
+
| Disabling a failing test to unblock CI | Fix it or delete it. Disabled tests lie. |
|
|
171
|
+
| Fixture soup — hundreds of lines to set up a test | Your system is hard to use; that's the signal, not the fixture |
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: database
|
|
3
|
+
description: Database end skill — schema design, indexing, queries, migrations, scaling. Engine-neutral (Postgres / MySQL / SQLite / NoSQL patterns). Pair with a language skill (`python`, `typescript`, `golang`), `backend` for where it fits, and `coding-standards`.
|
|
4
|
+
origin: ecc-fork + original (https://github.com/affaan-m/everything-claude-code, MIT)
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Database
|
|
8
|
+
|
|
9
|
+
Schema design, indexing, queries, migrations, scaling. **Engine-neutral**. For query-level performance inside a service, also see `backend/references/data-access.md`.
|
|
10
|
+
|
|
11
|
+
## When to load
|
|
12
|
+
|
|
13
|
+
- Designing or reviewing schema changes
|
|
14
|
+
- Writing migrations
|
|
15
|
+
- Diagnosing slow queries / choosing indexes
|
|
16
|
+
- Planning multi-tenant / sharding / read-replica topology
|
|
17
|
+
- Choosing between relational, document, key-value, time-series, graph
|
|
18
|
+
|
|
19
|
+
## Core principles
|
|
20
|
+
|
|
21
|
+
1. **Model the domain, not the queries.** Normalized schema first; denormalize when measurement shows it's needed.
|
|
22
|
+
2. **Every table has a primary key.** Every foreign key is declared, so integrity is enforced by the DB, not hoped for in app code.
|
|
23
|
+
3. **Constraints over application validation for invariants.** `NOT NULL`, `CHECK`, `UNIQUE`, `FOREIGN KEY` — let the DB refuse bad data.
|
|
24
|
+
4. **Indexes are a cost.** Every index slows writes and costs storage. Add them to answer real queries; drop them when the queries change.
|
|
25
|
+
5. **Migrations are additive first, destructive later.** Deploy the read; deploy the write; backfill; then remove the old path.
|
|
26
|
+
6. **Test migrations against production-scale data.** A migration that runs in 5 seconds on 1 000 rows can lock a table for 30 minutes on 100 million.
|
|
27
|
+
7. **Keep transactions short.** Long transactions hold locks that block everyone else.
|
|
28
|
+
8. **Pick the right tool.** Postgres gets you far — but not everything is a relational problem.
|
|
29
|
+
|
|
30
|
+
## How to use references
|
|
31
|
+
|
|
32
|
+
| Reference | When to load |
|
|
33
|
+
|---|---|
|
|
34
|
+
| [`references/schema.md`](references/schema.md) | Normalization, keys, constraints, types, partitioning, soft vs. hard delete |
|
|
35
|
+
| [`references/indexing.md`](references/indexing.md) | B-tree, partial, expression, multi-column, GIN / GiST, covering indexes |
|
|
36
|
+
| [`references/queries.md`](references/queries.md) | JOINs, subqueries, CTEs, window functions, EXPLAIN |
|
|
37
|
+
| [`references/migrations.md`](references/migrations.md) | Zero-downtime migrations, rename columns, schema evolution, backfills |
|
|
38
|
+
| [`references/scaling.md`](references/scaling.md) | Replication, sharding, caching, pooling, partitioning |
|
|
39
|
+
| [`references/nosql.md`](references/nosql.md) | Document, key-value, time-series, graph — when each fits |
|
|
40
|
+
|
|
41
|
+
## Forbidden patterns (auto-reject)
|
|
42
|
+
|
|
43
|
+
- Tables without a primary key
|
|
44
|
+
- Foreign keys without a declared reference (rely-on-app-code pattern)
|
|
45
|
+
- `NULL` meaning two different things (e.g. "unknown" vs "not applicable")
|
|
46
|
+
- Storing JSON blobs as the primary way to represent structured data (use columns for what you query)
|
|
47
|
+
- `SELECT *` in production application code
|
|
48
|
+
- Indexes added without a query that uses them
|
|
49
|
+
- Destructive migrations without a tested rollback plan
|
|
50
|
+
- Running `ALTER TABLE` on a large table during peak hours without locking analysis
|
|
51
|
+
- Business logic in stored procedures that the code doesn't see
|
|
52
|
+
- `TIMESTAMP` without timezone where the app runs in multiple regions
|
|
53
|
+
- Money stored as `FLOAT` / `DOUBLE` — use `DECIMAL` / cents-as-integer
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# Indexing
|
|
2
|
+
|
|
3
|
+
B-tree, partial, expression, multi-column, GIN/GiST, covering. Read `EXPLAIN`, don't guess.
|
|
4
|
+
|
|
5
|
+
## Why indexes matter
|
|
6
|
+
|
|
7
|
+
Without an index, the DB does a sequential scan — O(n) per query. With an index, it's O(log n). On a table of 50 million rows, that's the difference between 2 ms and 20 seconds.
|
|
8
|
+
|
|
9
|
+
The catch: indexes cost storage and slow writes. Every `INSERT` / `UPDATE` to an indexed column updates the index too. More indexes → more write amplification.
|
|
10
|
+
|
|
11
|
+
## Default: B-tree
|
|
12
|
+
|
|
13
|
+
The workhorse. Use for equality and range queries on ordered data.
|
|
14
|
+
|
|
15
|
+
```sql
|
|
16
|
+
CREATE INDEX ix_users_email ON users(email);
|
|
17
|
+
|
|
18
|
+
-- Helps:
|
|
19
|
+
SELECT * FROM users WHERE email = 'a@x.com';
|
|
20
|
+
SELECT * FROM users WHERE email BETWEEN 'a' AND 'c';
|
|
21
|
+
SELECT * FROM users ORDER BY email LIMIT 20;
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Multi-column indexes
|
|
25
|
+
|
|
26
|
+
Order matters. An index on `(a, b)` helps:
|
|
27
|
+
|
|
28
|
+
```sql
|
|
29
|
+
WHERE a = ? AND b = ? -- yes
|
|
30
|
+
WHERE a = ? -- yes (uses prefix)
|
|
31
|
+
WHERE b = ? -- no (would need an index starting with b)
|
|
32
|
+
WHERE a = ? ORDER BY b -- yes, range-limited
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Rule: most-selective (highest-cardinality) column first, or the column used without the other.
|
|
36
|
+
|
|
37
|
+
Don't create `ix_a_b` AND `ix_a` AND `ix_a_b_c` — the broadest index already covers the narrower prefixes. Prune.
|
|
38
|
+
|
|
39
|
+
## Partial indexes
|
|
40
|
+
|
|
41
|
+
Index only the rows that matter for the hot query.
|
|
42
|
+
|
|
43
|
+
```sql
|
|
44
|
+
CREATE INDEX ix_orders_pending ON orders(created_at)
|
|
45
|
+
WHERE status = 'pending';
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Smaller index, faster. Ideal when the filtered subset is a small fraction of the table.
|
|
49
|
+
|
|
50
|
+
## Expression indexes
|
|
51
|
+
|
|
52
|
+
Index a computed value.
|
|
53
|
+
|
|
54
|
+
```sql
|
|
55
|
+
CREATE INDEX ix_users_email_lower ON users (LOWER(email));
|
|
56
|
+
|
|
57
|
+
-- Helps:
|
|
58
|
+
SELECT * FROM users WHERE LOWER(email) = 'a@x.com';
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Useful for case-insensitive search, computed columns, JSON extraction:
|
|
62
|
+
|
|
63
|
+
```sql
|
|
64
|
+
CREATE INDEX ix_events_user ON events ((payload->>'user_id'));
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Covering / include columns
|
|
68
|
+
|
|
69
|
+
Include extra columns in the index so the DB doesn't need to visit the table.
|
|
70
|
+
|
|
71
|
+
```sql
|
|
72
|
+
CREATE INDEX ix_orders_user_created ON orders(user_id, created_at)
|
|
73
|
+
INCLUDE (total_cents, status);
|
|
74
|
+
|
|
75
|
+
-- Index-only scan:
|
|
76
|
+
SELECT total_cents, status FROM orders
|
|
77
|
+
WHERE user_id = ? ORDER BY created_at LIMIT 20;
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The query is answered entirely from the index pages — big win on wide tables.
|
|
81
|
+
|
|
82
|
+
## Specialized indexes (Postgres examples; others vary)
|
|
83
|
+
|
|
84
|
+
| Type | Use |
|
|
85
|
+
|---|---|
|
|
86
|
+
| `GIN` | JSONB fields, arrays, full-text search |
|
|
87
|
+
| `GiST` | Ranges, geo, custom types |
|
|
88
|
+
| `BRIN` | Very large append-only tables (logs, time-series) |
|
|
89
|
+
| `Hash` | Equality on huge keys (Postgres 10+); B-tree usually wins |
|
|
90
|
+
|
|
91
|
+
Example — full text:
|
|
92
|
+
|
|
93
|
+
```sql
|
|
94
|
+
CREATE INDEX ix_articles_tsv ON articles
|
|
95
|
+
USING GIN (to_tsvector('english', title || ' ' || body));
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## When to add an index
|
|
99
|
+
|
|
100
|
+
Start with the query that's slow. Run `EXPLAIN (ANALYZE, BUFFERS)` on it.
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
Seq Scan on orders (cost=0.00..25000.00 rows=1000 width=48)
|
|
104
|
+
Filter: (status = 'pending')
|
|
105
|
+
Rows Removed by Filter: 499000
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
`Seq Scan` on a filter that returned 1 / 500 rows is wasteful — add an index. After:
|
|
109
|
+
|
|
110
|
+
```
|
|
111
|
+
Index Scan using ix_orders_pending (cost=0.42..8.44 rows=1)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Don't add indexes speculatively. Adding an index "in case someone queries by this" creates write cost for zero read benefit.
|
|
115
|
+
|
|
116
|
+
## When NOT to add an index
|
|
117
|
+
|
|
118
|
+
- Column is low-cardinality (`gender` with 3 values, `status` with 4 values on a 10-million table) — DB will prefer sequential scan. Exception: partial index on the rare value.
|
|
119
|
+
- Table is small (< ~10 000 rows). The optimizer picks seq scan anyway.
|
|
120
|
+
- Column is frequently updated and rarely queried. Write cost > read savings.
|
|
121
|
+
- Column is already covered by a broader index prefix.
|
|
122
|
+
|
|
123
|
+
## Check what's being used
|
|
124
|
+
|
|
125
|
+
Postgres:
|
|
126
|
+
|
|
127
|
+
```sql
|
|
128
|
+
SELECT indexname, idx_scan, idx_tup_read, idx_tup_fetch
|
|
129
|
+
FROM pg_stat_user_indexes
|
|
130
|
+
ORDER BY idx_scan ASC;
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Indexes with `idx_scan = 0` for weeks are candidates for drop.
|
|
134
|
+
|
|
135
|
+
## Maintenance
|
|
136
|
+
|
|
137
|
+
- **Postgres**: `VACUUM ANALYZE` keeps statistics fresh. Autovacuum handles most cases; tune thresholds on write-heavy tables.
|
|
138
|
+
- **MySQL**: `ANALYZE TABLE` periodically.
|
|
139
|
+
- **Rebuilding** fragmented indexes: `REINDEX CONCURRENTLY` (Postgres 12+) — online rebuild.
|
|
140
|
+
|
|
141
|
+
## Index creation on live tables
|
|
142
|
+
|
|
143
|
+
Naive `CREATE INDEX` locks the table for writes. On a hot table, that stalls traffic.
|
|
144
|
+
|
|
145
|
+
```sql
|
|
146
|
+
-- ❌ on a hot table
|
|
147
|
+
CREATE INDEX ix_x ON big_table (y);
|
|
148
|
+
|
|
149
|
+
-- ✅
|
|
150
|
+
CREATE INDEX CONCURRENTLY ix_x ON big_table (y);
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
`CONCURRENTLY` (Postgres) doesn't block writes. Takes longer; retry on failure (partial indexes linger as `INVALID`).
|
|
154
|
+
|
|
155
|
+
MySQL 5.6+ supports online index creation for most storage engines; check the engine.
|
|
156
|
+
|
|
157
|
+
## JOIN indexes
|
|
158
|
+
|
|
159
|
+
For a query that joins `orders.user_id = users.id`, both sides need indexes on those columns. `users.id` is already the PK; `orders.user_id` needs its own index (declaring a FK does NOT auto-create the index in Postgres).
|
|
160
|
+
|
|
161
|
+
## ORDER BY + LIMIT
|
|
162
|
+
|
|
163
|
+
For paginated lists:
|
|
164
|
+
|
|
165
|
+
```sql
|
|
166
|
+
SELECT * FROM events
|
|
167
|
+
WHERE tenant_id = ?
|
|
168
|
+
ORDER BY created_at DESC
|
|
169
|
+
LIMIT 50;
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Index: `(tenant_id, created_at DESC)`. With `LIMIT`, the DB reads from the index and stops at 50 — O(1) regardless of total rows.
|
|
173
|
+
|
|
174
|
+
## Covering index vs. cache
|
|
175
|
+
|
|
176
|
+
Sometimes the right answer is caching, not more indexes. If a query is inherently expensive (joins, aggregations), cache the result and invalidate on write. See `backend/references/caching.md`.
|
|
177
|
+
|
|
178
|
+
## Anti-patterns
|
|
179
|
+
|
|
180
|
+
| Anti-pattern | Fix |
|
|
181
|
+
|---|---|
|
|
182
|
+
| Index on every column "just in case" | Index for queries; drop unused |
|
|
183
|
+
| Single-column indexes where composite would serve | One good composite > three singles |
|
|
184
|
+
| Same leading column in many indexes | The broader index covers the narrower; prune |
|
|
185
|
+
| Function in WHERE without an expression index | Index the expression, or rewrite the query |
|
|
186
|
+
| `LIKE '%x%'` with hopes of index use | Only trailing wildcards use B-tree; use full-text for leading wildcard |
|
|
187
|
+
| Non-sargable queries: `WHERE DATE(col) = ?` | Use ranges: `col >= '2026-04-20' AND col < '2026-04-21'` |
|
|
188
|
+
| Indexing enum-like TEXT column with only 3 values on a hot write table | Partial index, or skip the index |
|
|
189
|
+
| Blocking `CREATE INDEX` on live prod | `CONCURRENTLY` / online |
|
|
190
|
+
| Forgetting to index foreign keys | Every FK column should be indexed for JOINs |
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# Migrations
|
|
2
|
+
|
|
3
|
+
Zero-downtime changes. Every migration survives prod traffic.
|
|
4
|
+
|
|
5
|
+
## Rules
|
|
6
|
+
|
|
7
|
+
1. **Every schema change is a migration file.** Checked in, versioned, applied in CI/CD.
|
|
8
|
+
2. **Never edit a merged migration.** Write a new one.
|
|
9
|
+
3. **Additive first, destructive later.** Add → backfill → switch → remove.
|
|
10
|
+
4. **Test against production-scale data.** Time on staging ≠ time on prod.
|
|
11
|
+
5. **Every migration has a rollback story.** Either a reverse migration or a forward-only fix plan.
|
|
12
|
+
|
|
13
|
+
## Migration tools
|
|
14
|
+
|
|
15
|
+
| Tool | Works with |
|
|
16
|
+
|---|---|
|
|
17
|
+
| Flyway, Liquibase | Java / any SQL |
|
|
18
|
+
| Django migrations | Django |
|
|
19
|
+
| Alembic | Python / SQLAlchemy |
|
|
20
|
+
| Rails migrations | Rails |
|
|
21
|
+
| Prisma migrate | Node / TypeScript |
|
|
22
|
+
| Goose, Atlas | Go / any |
|
|
23
|
+
| Sqitch | Standalone, VCS-driven |
|
|
24
|
+
| Raw SQL + shell | Simple, works everywhere |
|
|
25
|
+
|
|
26
|
+
Pick one per project; don't mix. All of them do the same core thing: apply ordered SQL files once, track what's applied in a meta table.
|
|
27
|
+
|
|
28
|
+
## Additive vs. destructive
|
|
29
|
+
|
|
30
|
+
| Additive (safe) | Destructive (careful) |
|
|
31
|
+
|---|---|
|
|
32
|
+
| `CREATE TABLE` | `DROP TABLE` |
|
|
33
|
+
| `ADD COLUMN ... NULL` | `DROP COLUMN` |
|
|
34
|
+
| `CREATE INDEX CONCURRENTLY` | `ALTER COLUMN ... TYPE` (if rewrites table) |
|
|
35
|
+
| New `CHECK` (not validated yet) | `ALTER COLUMN ... NOT NULL` on a non-null column existing data |
|
|
36
|
+
| New FK (not validated yet) | Renames |
|
|
37
|
+
|
|
38
|
+
Pure additive migrations are (almost) always safe. Destructive ones need a phased plan.
|
|
39
|
+
|
|
40
|
+
## The expand / contract pattern
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
t0: schema A app code A
|
|
44
|
+
t1: schema A + B app code A (expand: new schema coexists)
|
|
45
|
+
t2: schema A + B app code B (switch: app uses B)
|
|
46
|
+
t3: schema B app code B (contract: old schema removed)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Each step deployable independently. Rollback = go back one step.
|
|
50
|
+
|
|
51
|
+
### Example: rename a column
|
|
52
|
+
|
|
53
|
+
Don't just `ALTER TABLE users RENAME COLUMN email_address TO email`. Code that's still running reads from `email_address` and crashes.
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
Deploy 1: ADD COLUMN email TEXT; -- expand
|
|
57
|
+
Deploy 2: backfill: UPDATE users SET email = email_address WHERE email IS NULL;
|
|
58
|
+
Deploy 3: app reads/writes BOTH email and email_address (dual-write).
|
|
59
|
+
Deploy 4: app reads/writes email only.
|
|
60
|
+
Deploy 5: DROP COLUMN email_address; -- contract
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Tedious but survives any restart sequence.
|
|
64
|
+
|
|
65
|
+
### Example: adding a NOT NULL column
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
Deploy 1: ADD COLUMN role TEXT DEFAULT 'user'; -- default avoids rewrite in Postgres 11+
|
|
69
|
+
Deploy 2: app writes role on all new rows.
|
|
70
|
+
Deploy 3: backfill rows with NULL role.
|
|
71
|
+
Deploy 4: ALTER COLUMN role SET NOT NULL.
|
|
72
|
+
Deploy 5: (optional) DROP DEFAULT if you don't want it anymore.
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Don't do `ADD COLUMN NOT NULL DEFAULT ...` on a huge table in old DB versions — it rewrites the whole table.
|
|
76
|
+
|
|
77
|
+
## Index creation on live tables
|
|
78
|
+
|
|
79
|
+
```sql
|
|
80
|
+
-- ❌ locks the table
|
|
81
|
+
CREATE INDEX ix_users_email ON users(email);
|
|
82
|
+
|
|
83
|
+
-- ✅ online
|
|
84
|
+
CREATE INDEX CONCURRENTLY ix_users_email ON users(email);
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
`CONCURRENTLY` runs in its own transaction (no DDL bundled). Migration frameworks that wrap every migration in a transaction will reject this — check whether your tool supports "non-transactional migrations".
|
|
88
|
+
|
|
89
|
+
## Backfills
|
|
90
|
+
|
|
91
|
+
For large tables, don't backfill in one statement. Batch:
|
|
92
|
+
|
|
93
|
+
```sql
|
|
94
|
+
UPDATE users SET role = 'user'
|
|
95
|
+
WHERE id IN (
|
|
96
|
+
SELECT id FROM users
|
|
97
|
+
WHERE role IS NULL
|
|
98
|
+
ORDER BY id
|
|
99
|
+
LIMIT 10000
|
|
100
|
+
);
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Loop until no rows updated. Sleep between batches to give replicas time to catch up.
|
|
104
|
+
|
|
105
|
+
Script it outside the migration framework so a slow backfill doesn't block a deploy. The schema change goes through the migration file; the backfill is a job.
|
|
106
|
+
|
|
107
|
+
## Rename a table
|
|
108
|
+
|
|
109
|
+
Similar expand/contract:
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
Deploy 1: CREATE TABLE new_name (... same schema ...);
|
|
113
|
+
Deploy 2: app writes to both (dual-write); or create a view / sync trigger.
|
|
114
|
+
Deploy 3: backfill historical rows.
|
|
115
|
+
Deploy 4: app reads/writes new_name only.
|
|
116
|
+
Deploy 5: DROP TABLE old_name.
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Most teams avoid this unless strictly necessary.
|
|
120
|
+
|
|
121
|
+
## Change a column type
|
|
122
|
+
|
|
123
|
+
If the new type is a strict widening (`INT` → `BIGINT` in some DBs, `VARCHAR(50)` → `VARCHAR(100)`), the change is cheap and online.
|
|
124
|
+
|
|
125
|
+
If it rewrites (`TEXT` → `INT` with a cast), treat as destructive + expand/contract.
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
ALTER TABLE users ALTER COLUMN x TYPE BIGINT USING x::BIGINT;
|
|
129
|
+
-- Rewrites the column; blocks writes. On a big table, that hurts.
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Alternative: add new column, backfill, switch app, drop old.
|
|
133
|
+
|
|
134
|
+
## Dropping columns / tables
|
|
135
|
+
|
|
136
|
+
1. App stops reading the column (deploy).
|
|
137
|
+
2. App stops writing the column (deploy).
|
|
138
|
+
3. Migration drops it.
|
|
139
|
+
|
|
140
|
+
Skipping 1 and 2 causes crashes when old app instances hit the dropped column during a rolling deploy.
|
|
141
|
+
|
|
142
|
+
## Migration performance
|
|
143
|
+
|
|
144
|
+
```sql
|
|
145
|
+
-- Before running in prod
|
|
146
|
+
EXPLAIN ANALYZE <the migration statement>
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Or time it on a copy of prod. A migration that takes an hour of lock time is a migration that breaks the site.
|
|
150
|
+
|
|
151
|
+
Rule of thumb for Postgres:
|
|
152
|
+
- `CREATE TABLE`: instant.
|
|
153
|
+
- `ADD COLUMN` nullable (11+): instant (metadata only).
|
|
154
|
+
- `ADD COLUMN NOT NULL DEFAULT` (11+): instant.
|
|
155
|
+
- `ALTER COLUMN ... TYPE` that rewrites: `O(rows)`, blocks writes.
|
|
156
|
+
- `CREATE INDEX CONCURRENTLY`: online, takes time.
|
|
157
|
+
- Adding FK constraint `VALIDATED`: scans the full table. Create `NOT VALID` then `VALIDATE CONSTRAINT` to split the pain.
|
|
158
|
+
|
|
159
|
+
## Transactional DDL
|
|
160
|
+
|
|
161
|
+
Postgres wraps DDL in transactions. If something fails mid-way, the migration rolls back cleanly. Mix DDL + data: keep it short; long DDL transactions hold locks.
|
|
162
|
+
|
|
163
|
+
MySQL pre-8 didn't support transactional DDL — failed migrations left partial state. Recovery is manual.
|
|
164
|
+
|
|
165
|
+
## Rollback plans
|
|
166
|
+
|
|
167
|
+
Before deploying, write down:
|
|
168
|
+
- What's the rollback command?
|
|
169
|
+
- What if the migration has already partly run?
|
|
170
|
+
- What if the backfill ran but the schema change failed?
|
|
171
|
+
|
|
172
|
+
For irreversible migrations (dropping a column), rollback = restore from backup + re-apply changes after. State that explicitly; don't pretend it's reversible.
|
|
173
|
+
|
|
174
|
+
## Feature flags + schema
|
|
175
|
+
|
|
176
|
+
For risky schema changes, gate the new code path behind a flag:
|
|
177
|
+
|
|
178
|
+
```
|
|
179
|
+
if feature('new_schema_path'):
|
|
180
|
+
use new table
|
|
181
|
+
else:
|
|
182
|
+
use old table
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Flip at will, revert instantly without a migration. Pair with expand/contract for best results.
|
|
186
|
+
|
|
187
|
+
## Anti-patterns
|
|
188
|
+
|
|
189
|
+
| Anti-pattern | Fix |
|
|
190
|
+
|---|---|
|
|
191
|
+
| Editing an applied migration | New migration |
|
|
192
|
+
| Dropping a column in the same deploy as code stops using it | Stage deploys |
|
|
193
|
+
| `ALTER TABLE ADD COLUMN NOT NULL DEFAULT 'x'` on a 100M-row table (old Postgres) | Split: ADD nullable → backfill → SET NOT NULL |
|
|
194
|
+
| No rollback plan | Document; even "forward-only" needs a plan |
|
|
195
|
+
| Running backfill in a single statement on a huge table | Batch + pause |
|
|
196
|
+
| Putting `CREATE INDEX` inside a long transaction | Use `CONCURRENTLY`, separate transaction |
|
|
197
|
+
| Dropping FKs "for performance" during a backfill | Disable constraints for the migration window deliberately |
|
|
198
|
+
| Untested migrations on prod-like data | Run against a snapshot before production |
|
|
199
|
+
| Migrations that depend on app code behaviour | Keep migrations self-contained in SQL |
|