@mrciphersmith/keryx 0.3.2 → 0.3.3
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/dist/cli.js +4634 -2445
- package/dist/core.js +66 -10
- package/package.json +1 -1
- package/src/gdskills/bundled/install-manifest.json +349 -2
- package/src/gdskills/bundled/rules/core/model-selection.mdc +18 -0
- package/src/gdskills/bundled/skills/orchestration/job-orchestrator/SKILL.md +1 -1
- package/src/gdskills/bundled/skills/planning/brainstorm/SKILL.md +1 -1
- package/src/gdskills/bundled/skills/planning/interviewer/SKILL.md +1 -1
- package/src/gdskills/bundled/skills/quality/deploy/SKILL.md +1 -1
- package/src/gdskills/bundled/skills/review/review-jev-contract/SKILL.md +193 -0
- package/src/gdskills/bundled/skills/review/review-orchestrator/SKILL.detail.md +81 -21
- package/src/gdskills/bundled/skills/review/review-orchestrator/SKILL.md +4 -4
- package/src/gdskills/bundled/stacks/c-cpp/agent-refs.json +4 -0
- package/src/gdskills/bundled/stacks/c-cpp/governance/eval.json +1777 -0
- package/src/gdskills/bundled/stacks/c-cpp/governance/scout.json +31 -0
- package/src/gdskills/bundled/stacks/c-cpp/pack.json +42 -0
- package/src/gdskills/bundled/stacks/c-cpp/rules/coding-style.mdc +80 -0
- package/src/gdskills/bundled/stacks/c-cpp/rules/patterns.mdc +87 -0
- package/src/gdskills/bundled/stacks/c-cpp/rules/security.mdc +90 -0
- package/src/gdskills/bundled/stacks/c-cpp/rules/testing.mdc +83 -0
- package/src/gdskills/bundled/stacks/c-cpp/skills/c-cpp-build-fix/SKILL.md +153 -0
- package/src/gdskills/bundled/stacks/c-cpp/skills/c-cpp-build-fix/evals.json +74 -0
- package/src/gdskills/bundled/stacks/c-cpp/skills/c-cpp-code-review/SKILL.md +132 -0
- package/src/gdskills/bundled/stacks/c-cpp/skills/c-cpp-code-review/evals.json +73 -0
- package/src/gdskills/bundled/stacks/c-cpp/skills/c-cpp-implementation/SKILL.md +151 -0
- package/src/gdskills/bundled/stacks/c-cpp/skills/c-cpp-implementation/evals.json +74 -0
- package/src/gdskills/bundled/stacks/c-cpp/skills/c-cpp-testing/SKILL.md +152 -0
- package/src/gdskills/bundled/stacks/c-cpp/skills/c-cpp-testing/evals.json +74 -0
- package/src/gdskills/bundled/stacks/ci-github-gitlab/agent-refs.json +4 -0
- package/src/gdskills/bundled/stacks/ci-github-gitlab/governance/eval.json +1295 -0
- package/src/gdskills/bundled/stacks/ci-github-gitlab/governance/scout.json +26 -0
- package/src/gdskills/bundled/stacks/ci-github-gitlab/pack.json +41 -0
- package/src/gdskills/bundled/stacks/ci-github-gitlab/rules/patterns.mdc +77 -0
- package/src/gdskills/bundled/stacks/ci-github-gitlab/rules/security.mdc +144 -0
- package/src/gdskills/bundled/stacks/ci-github-gitlab/skills/ci-pipeline-build-fix/SKILL.md +121 -0
- package/src/gdskills/bundled/stacks/ci-github-gitlab/skills/ci-pipeline-build-fix/evals.json +73 -0
- package/src/gdskills/bundled/stacks/ci-github-gitlab/skills/ci-pipeline-code-review/SKILL.md +139 -0
- package/src/gdskills/bundled/stacks/ci-github-gitlab/skills/ci-pipeline-code-review/evals.json +73 -0
- package/src/gdskills/bundled/stacks/ci-github-gitlab/skills/ci-pipeline-implementation/SKILL.md +147 -0
- package/src/gdskills/bundled/stacks/ci-github-gitlab/skills/ci-pipeline-implementation/evals.json +74 -0
- package/src/gdskills/bundled/stacks/docker-k8s-terraform/agent-refs.json +4 -0
- package/src/gdskills/bundled/stacks/docker-k8s-terraform/governance/eval.json +865 -0
- package/src/gdskills/bundled/stacks/docker-k8s-terraform/governance/scout.json +16 -0
- package/src/gdskills/bundled/stacks/docker-k8s-terraform/pack.json +46 -0
- package/src/gdskills/bundled/stacks/docker-k8s-terraform/rules/coding-style.mdc +74 -0
- package/src/gdskills/bundled/stacks/docker-k8s-terraform/rules/patterns.mdc +81 -0
- package/src/gdskills/bundled/stacks/docker-k8s-terraform/rules/security.mdc +146 -0
- package/src/gdskills/bundled/stacks/docker-k8s-terraform/rules/testing.mdc +61 -0
- package/src/gdskills/bundled/stacks/docker-k8s-terraform/skills/docker-k8s-terraform-build-fix/SKILL.md +151 -0
- package/src/gdskills/bundled/stacks/docker-k8s-terraform/skills/docker-k8s-terraform-build-fix/evals.json +74 -0
- package/src/gdskills/bundled/stacks/docker-k8s-terraform/skills/docker-k8s-terraform-review/SKILL.md +135 -0
- package/src/gdskills/bundled/stacks/docker-k8s-terraform/skills/docker-k8s-terraform-review/evals.json +76 -0
- package/src/gdskills/bundled/stacks/php-laravel/agent-refs.json +4 -0
- package/src/gdskills/bundled/stacks/php-laravel/governance/eval.json +1829 -0
- package/src/gdskills/bundled/stacks/php-laravel/governance/scout.json +33 -0
- package/src/gdskills/bundled/stacks/php-laravel/pack.json +41 -0
- package/src/gdskills/bundled/stacks/php-laravel/rules/coding-style.mdc +82 -0
- package/src/gdskills/bundled/stacks/php-laravel/rules/patterns.mdc +80 -0
- package/src/gdskills/bundled/stacks/php-laravel/rules/security.mdc +80 -0
- package/src/gdskills/bundled/stacks/php-laravel/rules/testing.mdc +82 -0
- package/src/gdskills/bundled/stacks/php-laravel/skills/php-laravel-build-fix/SKILL.md +143 -0
- package/src/gdskills/bundled/stacks/php-laravel/skills/php-laravel-build-fix/evals.json +74 -0
- package/src/gdskills/bundled/stacks/php-laravel/skills/php-laravel-code-review/SKILL.md +126 -0
- package/src/gdskills/bundled/stacks/php-laravel/skills/php-laravel-code-review/evals.json +76 -0
- package/src/gdskills/bundled/stacks/php-laravel/skills/php-laravel-implementation/SKILL.md +140 -0
- package/src/gdskills/bundled/stacks/php-laravel/skills/php-laravel-implementation/evals.json +75 -0
- package/src/gdskills/bundled/stacks/php-laravel/skills/php-laravel-testing/SKILL.md +124 -0
- package/src/gdskills/bundled/stacks/php-laravel/skills/php-laravel-testing/evals.json +74 -0
- package/src/gdskills/bundled/stacks/ruby-rails/agent-refs.json +4 -0
- package/src/gdskills/bundled/stacks/ruby-rails/governance/eval.json +1673 -0
- package/src/gdskills/bundled/stacks/ruby-rails/governance/scout.json +33 -0
- package/src/gdskills/bundled/stacks/ruby-rails/pack.json +42 -0
- package/src/gdskills/bundled/stacks/ruby-rails/rules/coding-style.mdc +69 -0
- package/src/gdskills/bundled/stacks/ruby-rails/rules/patterns.mdc +93 -0
- package/src/gdskills/bundled/stacks/ruby-rails/rules/security.mdc +90 -0
- package/src/gdskills/bundled/stacks/ruby-rails/rules/testing.mdc +89 -0
- package/src/gdskills/bundled/stacks/ruby-rails/skills/ruby-rails-build-fix/SKILL.md +143 -0
- package/src/gdskills/bundled/stacks/ruby-rails/skills/ruby-rails-build-fix/evals.json +73 -0
- package/src/gdskills/bundled/stacks/ruby-rails/skills/ruby-rails-code-review/SKILL.md +134 -0
- package/src/gdskills/bundled/stacks/ruby-rails/skills/ruby-rails-code-review/evals.json +71 -0
- package/src/gdskills/bundled/stacks/ruby-rails/skills/ruby-rails-implementation/SKILL.md +141 -0
- package/src/gdskills/bundled/stacks/ruby-rails/skills/ruby-rails-implementation/evals.json +72 -0
- package/src/gdskills/bundled/stacks/ruby-rails/skills/ruby-rails-testing/SKILL.md +125 -0
- package/src/gdskills/bundled/stacks/ruby-rails/skills/ruby-rails-testing/evals.json +72 -0
- package/src/gdskills/bundled/stacks/sql-db/agent-refs.json +4 -0
- package/src/gdskills/bundled/stacks/sql-db/governance/eval.json +1829 -0
- package/src/gdskills/bundled/stacks/sql-db/governance/scout.json +30 -0
- package/src/gdskills/bundled/stacks/sql-db/pack.json +40 -0
- package/src/gdskills/bundled/stacks/sql-db/rules/coding-style.mdc +69 -0
- package/src/gdskills/bundled/stacks/sql-db/rules/patterns.mdc +134 -0
- package/src/gdskills/bundled/stacks/sql-db/rules/security.mdc +74 -0
- package/src/gdskills/bundled/stacks/sql-db/rules/testing.mdc +83 -0
- package/src/gdskills/bundled/stacks/sql-db/skills/sql-db-build-fix/SKILL.md +147 -0
- package/src/gdskills/bundled/stacks/sql-db/skills/sql-db-build-fix/evals.json +72 -0
- package/src/gdskills/bundled/stacks/sql-db/skills/sql-db-code-review/SKILL.md +132 -0
- package/src/gdskills/bundled/stacks/sql-db/skills/sql-db-code-review/evals.json +73 -0
- package/src/gdskills/bundled/stacks/sql-db/skills/sql-db-implementation/SKILL.md +153 -0
- package/src/gdskills/bundled/stacks/sql-db/skills/sql-db-implementation/evals.json +77 -0
- package/src/gdskills/bundled/stacks/sql-db/skills/sql-db-testing/SKILL.md +129 -0
- package/src/gdskills/bundled/stacks/sql-db/skills/sql-db-testing/evals.json +73 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"query": "Use when writing SQL schema, migrations, or queries for Postgres/MySQL -- safe zero-downtime schema changes (adding NOT NULL, backfills), indexing (composite column order, covering/partial indexes), transaction discipline, and parameterized query design.",
|
|
4
|
+
"decision": "create",
|
|
5
|
+
"topMatch": "sql-db/sql-db-testing",
|
|
6
|
+
"recordedAt": "2026-09-25T15:30:40.157Z",
|
|
7
|
+
"skillName": "sql-db-implementation"
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"query": "Use when writing or fixing tests for SQL migrations and queries -- transactional test wrapping with rollback, fixture/seed data design, testing a migration's up/down path on representative row counts, and asserting on actual query results and query counts rather than just 'no error'.",
|
|
11
|
+
"decision": "create",
|
|
12
|
+
"topMatch": "quality/fresh-eyes",
|
|
13
|
+
"recordedAt": "2026-09-25T15:30:44.924Z",
|
|
14
|
+
"skillName": "sql-db-testing"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"query": "Use when reviewing a migration or query diff for SQL/database-specific risk -- injection (string-built SQL, ORM raw-SQL escape hatches), missing/misordered indexes, an unsafe blocking NOT NULL or index-creation statement, N+1 query shape, and transactions left open across a slow call. Read-only, no edits.",
|
|
18
|
+
"decision": "create",
|
|
19
|
+
"topMatch": "php-laravel/php-laravel-code-review",
|
|
20
|
+
"recordedAt": "2026-09-25T15:30:47.232Z",
|
|
21
|
+
"skillName": "sql-db-code-review"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"query": "Use when a migration fails to apply, a query planner regresses to a full table scan, or a constraint violation blocks a deploy -- resolves failed/broken migrations, missing-index query regressions, deadlocks, and NOT NULL/unique/foreign-key constraint violations with the smallest root-cause fix.",
|
|
25
|
+
"decision": "create",
|
|
26
|
+
"topMatch": "sql-db/sql-db-implementation",
|
|
27
|
+
"recordedAt": "2026-09-25T15:30:49.254Z",
|
|
28
|
+
"skillName": "sql-db-build-fix"
|
|
29
|
+
}
|
|
30
|
+
]
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "sql-db",
|
|
3
|
+
"family": "capability",
|
|
4
|
+
"modules": ["sql-db-rules", "sql-db-skills"],
|
|
5
|
+
"detectionMarkers": ["sql"],
|
|
6
|
+
"provenance": {
|
|
7
|
+
"origin": "authored",
|
|
8
|
+
"sourceRef": "flow 337, Wave 4 batch 5"
|
|
9
|
+
},
|
|
10
|
+
"stability": "experimental",
|
|
11
|
+
"skills": {
|
|
12
|
+
"implement": ["sql-db-implementation"],
|
|
13
|
+
"test": ["sql-db-testing"],
|
|
14
|
+
"review": ["sql-db-code-review"],
|
|
15
|
+
"build-fix": ["sql-db-build-fix"],
|
|
16
|
+
"migrate": []
|
|
17
|
+
},
|
|
18
|
+
"agentProfile": {
|
|
19
|
+
"displayName": "SQL / Database",
|
|
20
|
+
"auditFocus": [
|
|
21
|
+
"a query built with string concatenation or f-string/template interpolation of a user-influenced value instead of a parameterized placeholder or prepared statement, including inside an ORM's raw-SQL/raw-query escape hatch",
|
|
22
|
+
"an ADD COLUMN ... NOT NULL with no default, or a SET NOT NULL with no prior NOT VALID CHECK + VALIDATE CONSTRAINT step, on a table that can have existing rows",
|
|
23
|
+
"a WHERE/JOIN/ORDER BY column with no supporting index, or a composite index whose column order does not match the query's actual equality-then-range filter shape",
|
|
24
|
+
"a loop that issues one query per row (a classic N+1) where a JOIN, an IN-clause batch, or eager loading would do it in one round trip",
|
|
25
|
+
"a migration with no reverse/down operation, or one that is not safe to run twice (missing IF EXISTS/IF NOT EXISTS on a DDL statement that isn't naturally idempotent)",
|
|
26
|
+
"a transaction held open across a slow external call (an HTTP request, a queue publish, a sleep) instead of being committed before or reopened after it"
|
|
27
|
+
],
|
|
28
|
+
"buildCommands": [
|
|
29
|
+
"EXPLAIN (ANALYZE, BUFFERS) <query> -- Postgres: confirm no unexpected sequential scan on the touched tables",
|
|
30
|
+
"EXPLAIN ANALYZE <query> -- MySQL: confirm the query uses the intended index",
|
|
31
|
+
"the project's own migration runner in dry-run/plan mode when one exists, before applying against a real database"
|
|
32
|
+
],
|
|
33
|
+
"fixGuardrails": [
|
|
34
|
+
"Never fix a slow query by adding an index without checking its column order against the query's actual WHERE/JOIN/ORDER BY shape first.",
|
|
35
|
+
"Never rewrite a parameterized query into a string-concatenated one to work around a driver quirk; fix the parameter binding instead.",
|
|
36
|
+
"Never resolve a migration failure by dropping the failing constraint or making a NOT NULL column nullable again without saying so explicitly in the report.",
|
|
37
|
+
"Never skip or delete a migration step to reach a green migration run."
|
|
38
|
+
]
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
---
|
|
2
|
+
extends: common
|
|
3
|
+
paths: ["**/*.sql"]
|
|
4
|
+
metadata:
|
|
5
|
+
origin: authored
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# SQL coding style
|
|
9
|
+
|
|
10
|
+
Narrows `core-common-rules`' stack-agnostic style rules to raw `.sql` files
|
|
11
|
+
(schema definitions, migrations, hand-written query files) across Postgres,
|
|
12
|
+
MySQL, and generic SQL engines. Applies only to `*.sql` files — application
|
|
13
|
+
code that embeds SQL as strings (a Python/Go/JS query builder call) is
|
|
14
|
+
covered by that language's own stack pack, not this one.
|
|
15
|
+
|
|
16
|
+
## Naming
|
|
17
|
+
|
|
18
|
+
- Tables and columns are `snake_case`, plural for tables (`orders`,
|
|
19
|
+
`order_items`), singular for columns (`user_id`, not `user_ids` on a
|
|
20
|
+
scalar foreign key) — match whatever convention the project's existing
|
|
21
|
+
schema already uses instead of introducing a second one.
|
|
22
|
+
- Name a foreign-key column `<singular_referenced_table>_id`
|
|
23
|
+
(`orders.user_id` referencing `users.id`), so its relationship is
|
|
24
|
+
readable without opening the schema.
|
|
25
|
+
- Name an index `idx_<table>_<column(s)>` and a unique constraint
|
|
26
|
+
`uq_<table>_<column(s)>`; name a foreign key constraint
|
|
27
|
+
`fk_<table>_<referenced_table>` — explicit names are required so a
|
|
28
|
+
failing constraint's error message names something a reader recognizes,
|
|
29
|
+
instead of an engine-generated identifier like `orders_user_id_fkey1`.
|
|
30
|
+
|
|
31
|
+
## Formatting
|
|
32
|
+
|
|
33
|
+
- One clause per line for anything beyond a trivial single-table query —
|
|
34
|
+
`SELECT`, `FROM`, `WHERE`, `GROUP BY`, `ORDER BY` each start their own
|
|
35
|
+
line, with join conditions immediately under their `JOIN`. A query dense
|
|
36
|
+
enough to need review is dense enough to need this shape.
|
|
37
|
+
- Keywords uppercase (`SELECT`, `FROM`, `WHERE`, `JOIN`), identifiers
|
|
38
|
+
lowercase — do not mix case within one file's own convention.
|
|
39
|
+
- Explicit `JOIN ... ON` conditions, never an implicit comma join
|
|
40
|
+
(`FROM a, b WHERE a.id = b.a_id`) — the join type and its condition
|
|
41
|
+
belong together, not split across `FROM` and `WHERE`.
|
|
42
|
+
- Every migration file's DDL statements are wrapped in the smallest
|
|
43
|
+
transaction the engine allows (Postgres: a single implicit transaction
|
|
44
|
+
per migration is usually safe except around `CREATE INDEX CONCURRENTLY`,
|
|
45
|
+
which cannot run inside a transaction block at all — split it into its
|
|
46
|
+
own migration step rather than wrapping it anyway).
|
|
47
|
+
|
|
48
|
+
## Column and type choices
|
|
49
|
+
|
|
50
|
+
- Pick the narrowest correct type: an `enum`/`CHECK`-constrained text
|
|
51
|
+
column over a free `VARCHAR` for a closed set of values, `TIMESTAMPTZ`
|
|
52
|
+
(Postgres) / a UTC-normalized `DATETIME` (MySQL) over a naive local-time
|
|
53
|
+
column, `NUMERIC`/`DECIMAL` for money — never `FLOAT`/`DOUBLE` for a
|
|
54
|
+
value that must round exactly.
|
|
55
|
+
- A nullable column means "this can genuinely be absent," not "I didn't
|
|
56
|
+
decide yet" — a column with no legitimate absent case gets `NOT NULL`
|
|
57
|
+
from the migration that introduces it (with a default, or through the
|
|
58
|
+
safe multi-step pattern in `rules/patterns.mdc` when the table already
|
|
59
|
+
has rows).
|
|
60
|
+
|
|
61
|
+
## Comments and documentation
|
|
62
|
+
|
|
63
|
+
- A non-obvious index (a partial index's predicate, a composite index
|
|
64
|
+
whose column order looks surprising) gets a one-line comment stating
|
|
65
|
+
which query it serves — an index with no stated purpose is a maintenance
|
|
66
|
+
liability the next person can't safely drop or reorder.
|
|
67
|
+
- A migration that is not naturally reversible (a data-destructive
|
|
68
|
+
backfill, a column drop) states its rollback plan or its "no rollback,
|
|
69
|
+
here is why" reasoning in a comment at the top of the file.
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
---
|
|
2
|
+
extends: common
|
|
3
|
+
paths: ["**/*.sql"]
|
|
4
|
+
metadata:
|
|
5
|
+
origin: authored
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# SQL patterns
|
|
9
|
+
|
|
10
|
+
Narrows `core-common-rules`' stack-agnostic design guidance to migration
|
|
11
|
+
safety, query design, and indexing for `.sql` files across Postgres,
|
|
12
|
+
MySQL, and generic SQL engines. Extends the general N+1/index/migration
|
|
13
|
+
guidance in the core `database-patterns.mdc` rule with engine-specific
|
|
14
|
+
mechanics for the zero-downtime cases that rule states only as a general
|
|
15
|
+
three-step pattern. Applies only to `*.sql` files.
|
|
16
|
+
|
|
17
|
+
## Zero-downtime schema changes
|
|
18
|
+
|
|
19
|
+
- **Adding a nullable column** is always safe as a single migration on
|
|
20
|
+
both engines.
|
|
21
|
+
- **Adding a `NOT NULL` column with a constant default** does not require
|
|
22
|
+
a full table rewrite on modern Postgres (11+) or MySQL (8.0+, via
|
|
23
|
+
`ALGORITHM=INSTANT` for an appended column) — prefer `ADD COLUMN ...
|
|
24
|
+
NOT NULL DEFAULT <constant>` directly over the older three-step
|
|
25
|
+
add-nullable/backfill/constrain dance when every existing and new row
|
|
26
|
+
can share the same default value.
|
|
27
|
+
- **Adding `NOT NULL` to an existing column that needs a real backfill**
|
|
28
|
+
(not a constant default) still needs the multi-step sequence: add the
|
|
29
|
+
column nullable, deploy code that writes it on every insert/update,
|
|
30
|
+
backfill existing rows in batches (not one giant `UPDATE`, which holds
|
|
31
|
+
locks and generates a huge amount of WAL/binlog), then constrain.
|
|
32
|
+
- Postgres: prefer `ALTER TABLE t ADD CONSTRAINT t_col_not_null CHECK
|
|
33
|
+
(col IS NOT NULL) NOT VALID;` followed by a separate `ALTER TABLE t
|
|
34
|
+
VALIDATE CONSTRAINT t_col_not_null;` over a direct `ALTER TABLE t
|
|
35
|
+
ALTER COLUMN col SET NOT NULL` — `NOT VALID` skips the initial
|
|
36
|
+
table scan (applies only to new rows immediately), and
|
|
37
|
+
`VALIDATE CONSTRAINT` then scans under `SHARE UPDATE EXCLUSIVE`,
|
|
38
|
+
which does not block concurrent reads/writes the way the scan inside
|
|
39
|
+
a plain `SET NOT NULL` does.
|
|
40
|
+
- MySQL: `ALTER TABLE t MODIFY COLUMN col <type> NOT NULL` is an
|
|
41
|
+
in-place operation but still requires a full table scan/rebuild on
|
|
42
|
+
most storage engines — schedule it like any table-rewriting DDL
|
|
43
|
+
(off-peak, or through the project's online-migration tool such as
|
|
44
|
+
`gh-ost`/gantry-style tooling if one is already in use).
|
|
45
|
+
- **Adding an index on an existing table**: Postgres —
|
|
46
|
+
`CREATE INDEX CONCURRENTLY` (cannot run inside a transaction block; put
|
|
47
|
+
it in its own migration step) to avoid the `ACCESS EXCLUSIVE`-adjacent
|
|
48
|
+
write lock a plain `CREATE INDEX` takes. MySQL — `CREATE INDEX` on
|
|
49
|
+
InnoDB uses `ALGORITHM=INPLACE` by default for most index additions,
|
|
50
|
+
which permits concurrent DML; verify with `ALGORITHM=INPLACE, LOCK=NONE`
|
|
51
|
+
explicitly if the table is under sustained write load.
|
|
52
|
+
- **Renaming or changing a column's type**: no engine makes this safe as
|
|
53
|
+
one step under a rolling deploy — add the new column, dual-write from
|
|
54
|
+
application code, backfill, cut reads over, then drop the old column in
|
|
55
|
+
a later migration once nothing reads it.
|
|
56
|
+
- **Dropping a column or table**: stop reading/writing it from application
|
|
57
|
+
code first (a full deploy cycle), then drop it in a subsequent
|
|
58
|
+
migration — dropping while old code still reads the column breaks that
|
|
59
|
+
code immediately on deploy overlap.
|
|
60
|
+
|
|
61
|
+
## Indexing
|
|
62
|
+
|
|
63
|
+
- Index every foreign key column and every column that appears in a
|
|
64
|
+
`WHERE`, `JOIN ON`, or `ORDER BY` clause on a table large enough that a
|
|
65
|
+
sequential/full-table scan would be noticeable — confirm with
|
|
66
|
+
`EXPLAIN ANALYZE` on production-representative data volume, not on an
|
|
67
|
+
empty dev table where every plan looks fast.
|
|
68
|
+
- **Composite index column order**: put equality-filtered columns before
|
|
69
|
+
range-filtered ones, and put the columns a query's `WHERE` actually
|
|
70
|
+
restricts before columns only used for `ORDER BY`. An index on
|
|
71
|
+
`(status, created_at)` serves `WHERE status = 'open' ORDER BY
|
|
72
|
+
created_at` efficiently; the same index in the order `(created_at,
|
|
73
|
+
status)` cannot use the equality filter to narrow the scan first and
|
|
74
|
+
degrades toward a broader range scan.
|
|
75
|
+
- **Covering indexes**: when a query only ever selects a handful of
|
|
76
|
+
columns already present in an index's key or `INCLUDE`/covered columns
|
|
77
|
+
(Postgres `INCLUDE`, or MySQL's implicit covering when all selected
|
|
78
|
+
columns are in the index), the engine can satisfy it as an index-only
|
|
79
|
+
scan without touching the table's heap/pages at all — worth adding
|
|
80
|
+
narrowly for a hot, narrow query, not as a default habit.
|
|
81
|
+
- **An index has a write cost**: every index is maintained on every
|
|
82
|
+
insert/update/delete touching its columns. Do not add a composite index
|
|
83
|
+
for every column combination a report might someday filter by; add the
|
|
84
|
+
ones actual queries use, and periodically check for unused indexes
|
|
85
|
+
(`pg_stat_user_indexes.idx_scan = 0` on Postgres) before a schema
|
|
86
|
+
accumulates dead weight.
|
|
87
|
+
- A partial index (`CREATE INDEX ... WHERE <predicate>`, Postgres) is
|
|
88
|
+
narrower and cheaper to maintain than a full index when a query always
|
|
89
|
+
filters on a fixed condition (`WHERE deleted_at IS NULL`,
|
|
90
|
+
`WHERE status = 'active'`) — prefer it over a full index whose non-
|
|
91
|
+
matching rows the query never touches.
|
|
92
|
+
|
|
93
|
+
## Query design
|
|
94
|
+
|
|
95
|
+
- A query issued inside an application-code loop, once per row of an
|
|
96
|
+
outer result set, is an N+1 — replace it with a single `JOIN`, an
|
|
97
|
+
`IN (...)` batch, or the ORM's eager-load option before merging. This
|
|
98
|
+
narrows the core `database-patterns.mdc` N+1 rule to the SQL shape
|
|
99
|
+
itself: the fix is a query that returns everything needed in one round
|
|
100
|
+
trip, not a cache layered on top of the repeated queries.
|
|
101
|
+
- Select only the columns a caller actually needs — `SELECT *` in a
|
|
102
|
+
hand-written `.sql` file breaks silently when a column is added later
|
|
103
|
+
and can leak a sensitive column (a password hash, a token) into a
|
|
104
|
+
result set nothing downstream expected to see.
|
|
105
|
+
- Wrap a multi-statement write (anything touching more than one table, or
|
|
106
|
+
more than one row-affecting statement against the same table) in an
|
|
107
|
+
explicit transaction — `BEGIN`/`COMMIT` or the driver's transaction
|
|
108
|
+
API — so a failure partway through rolls back cleanly instead of
|
|
109
|
+
leaving related tables inconsistent.
|
|
110
|
+
- Keep a transaction's lifetime to the database work alone — commit
|
|
111
|
+
before making a slow external call (an HTTP request, a queue publish,
|
|
112
|
+
a `sleep`), or open a fresh transaction after it completes. A
|
|
113
|
+
transaction held open across an external call holds its locks for the
|
|
114
|
+
external call's entire duration, which can stall every other writer
|
|
115
|
+
touching the same rows.
|
|
116
|
+
- Prefer `UPSERT` (Postgres `INSERT ... ON CONFLICT DO UPDATE`, MySQL
|
|
117
|
+
`INSERT ... ON DUPLICATE KEY UPDATE`) over a read-then-decide-insert-or-
|
|
118
|
+
update round trip when the goal is "write this row whether or not it
|
|
119
|
+
already exists" — the read-then-write version races under concurrent
|
|
120
|
+
writers even inside a transaction unless it also takes an explicit lock.
|
|
121
|
+
|
|
122
|
+
## Migration hygiene
|
|
123
|
+
|
|
124
|
+
- Every migration that mutates existing data (not just schema) states its
|
|
125
|
+
reverse operation, or explicitly states there isn't one and why (a
|
|
126
|
+
destructive backfill, a data transformation that can't be inverted).
|
|
127
|
+
- Make DDL idempotent where the engine allows it (`IF NOT EXISTS` on
|
|
128
|
+
`CREATE TABLE`/`CREATE INDEX`, `IF EXISTS` on `DROP`) so a migration
|
|
129
|
+
runner that re-applies a partially-run migration after a crash does not
|
|
130
|
+
fail on an object that already exists.
|
|
131
|
+
- Never combine a long-running backfill and a blocking schema change in
|
|
132
|
+
the same migration step — split them so the schema change (fast) and
|
|
133
|
+
the backfill (potentially slow, batched) can be reviewed and rolled
|
|
134
|
+
back independently.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
---
|
|
2
|
+
extends: common
|
|
3
|
+
paths: ["**/*.sql"]
|
|
4
|
+
metadata:
|
|
5
|
+
origin: authored
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# SQL security
|
|
9
|
+
|
|
10
|
+
Narrows `core-common-rules`' stack-agnostic security rules to injection
|
|
11
|
+
safety and access control for `.sql` files across Postgres, MySQL, and
|
|
12
|
+
generic SQL engines. Applies only to `*.sql` files — parameterized-query
|
|
13
|
+
enforcement in application code (the driver/ORM call site) is covered by
|
|
14
|
+
that language's own stack pack; this file covers the `.sql` side (stored
|
|
15
|
+
procedures, functions, views) and the shared vocabulary both sides share.
|
|
16
|
+
|
|
17
|
+
## Injection
|
|
18
|
+
|
|
19
|
+
- Never build a query by concatenating or string-formatting a
|
|
20
|
+
user-influenced value into SQL text — this applies inside a stored
|
|
21
|
+
procedure or function body exactly as it does in application code: a
|
|
22
|
+
dynamic `EXECUTE`/`EXEC` built from concatenated input is injectable
|
|
23
|
+
even though the vulnerable code lives in the database, not the app.
|
|
24
|
+
- Postgres `EXECUTE`/`format()` inside `PL/pgSQL`: use `format('...%I...',
|
|
25
|
+
identifier)` for identifiers (table/column names that must be dynamic)
|
|
26
|
+
and `format('...%L...', value)` or a parameterized `USING` clause for
|
|
27
|
+
values — `%I`/`%L` quote-escape correctly; plain string concatenation
|
|
28
|
+
of either does not.
|
|
29
|
+
- MySQL stored procedures: build dynamic SQL with `PREPARE stmt FROM
|
|
30
|
+
@sql; EXECUTE stmt USING @param;` — bind user-influenced values through
|
|
31
|
+
`USING`, never interpolate them into `@sql` itself before `PREPARE`.
|
|
32
|
+
- An ORM or query builder's raw-SQL escape hatch (a `.raw()`, `.exec()`,
|
|
33
|
+
or template-literal query method) carries the same rule as a
|
|
34
|
+
hand-written query: pass user-influenced values through its
|
|
35
|
+
placeholder/parameter API, never through string interpolation into the
|
|
36
|
+
raw SQL text, even when the surrounding call looks like it's "just
|
|
37
|
+
building a string for convenience."
|
|
38
|
+
- A dynamic identifier (a table or column name chosen at runtime, common
|
|
39
|
+
in multi-tenant schema-per-tenant designs or admin tooling) cannot go
|
|
40
|
+
through a value placeholder at all — placeholders bind values, not
|
|
41
|
+
identifiers. Validate it against an allowlist of known-safe identifiers
|
|
42
|
+
before using it, never trust it as free text even when it "came from
|
|
43
|
+
our own config."
|
|
44
|
+
|
|
45
|
+
## Access control
|
|
46
|
+
|
|
47
|
+
- Grant the narrowest role/privilege a connection actually needs — an
|
|
48
|
+
application's runtime database user should not hold `DROP`/`CREATE`/
|
|
49
|
+
superuser privileges it never uses; a migration runner's credentials
|
|
50
|
+
(which do need DDL rights) should be separate from the application's
|
|
51
|
+
runtime connection.
|
|
52
|
+
- Use row-level security (Postgres `CREATE POLICY` on a table with `ROW
|
|
53
|
+
LEVEL SECURITY` enabled) or an equivalent application-enforced tenant
|
|
54
|
+
filter consistently — do not rely on every query remembering to add
|
|
55
|
+
`WHERE tenant_id = ?` by convention alone when the engine can enforce it
|
|
56
|
+
structurally.
|
|
57
|
+
- A view or stored procedure that runs with elevated privileges
|
|
58
|
+
(Postgres `SECURITY DEFINER`, a MySQL `DEFINER`-owned routine) is a
|
|
59
|
+
privilege-escalation surface if it accepts unvalidated input that
|
|
60
|
+
influences what it reads or writes — treat its parameters with the same
|
|
61
|
+
injection discipline as a top-level query, since callers with lower
|
|
62
|
+
privileges can reach the elevated code path through it.
|
|
63
|
+
|
|
64
|
+
## Data exposure
|
|
65
|
+
|
|
66
|
+
- Never `SELECT *` a table containing a sensitive column (password hash,
|
|
67
|
+
API token, PII) into a result set a caller with less trust than the
|
|
68
|
+
query's own privilege level will see — select named columns so adding a
|
|
69
|
+
sensitive column later can't leak silently into an existing query.
|
|
70
|
+
- Encrypt or hash sensitive columns before they reach storage
|
|
71
|
+
(`pgcrypto`'s `crypt()`/`digest()` on Postgres, an application-layer
|
|
72
|
+
hash before insert) rather than relying on transport encryption alone —
|
|
73
|
+
a compromised backup or replica exposes plaintext columns even when
|
|
74
|
+
every connection to the primary was encrypted.
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
---
|
|
2
|
+
extends: common
|
|
3
|
+
paths: ["**/*.sql"]
|
|
4
|
+
metadata:
|
|
5
|
+
origin: authored
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# SQL testing
|
|
9
|
+
|
|
10
|
+
Narrows `core-common-rules`' stack-agnostic testing rules to testing raw
|
|
11
|
+
`.sql` — migrations, schema, and hand-written query files — across
|
|
12
|
+
Postgres, MySQL, and generic SQL engines. Applies only to `*.sql` files;
|
|
13
|
+
testing a query or migration issued from application code (a language's
|
|
14
|
+
ORM test helpers) is covered by that language's own stack pack, and
|
|
15
|
+
should still follow the transactional-wrapping and fixture discipline
|
|
16
|
+
below where that language's test suite talks to a real database.
|
|
17
|
+
|
|
18
|
+
## Test isolation
|
|
19
|
+
|
|
20
|
+
- Wrap each test in a transaction that is rolled back at teardown
|
|
21
|
+
(`BEGIN` before the test, `ROLLBACK` after) rather than truncating and
|
|
22
|
+
re-seeding tables between tests — rollback is faster and guarantees no
|
|
23
|
+
leftover state regardless of what the test wrote.
|
|
24
|
+
- When a test must exercise something a transaction can't roll back
|
|
25
|
+
cleanly (`CREATE INDEX CONCURRENTLY`, an advisory-lock scenario, a
|
|
26
|
+
cross-transaction concurrency test), isolate it in its own
|
|
27
|
+
database/schema that is torn down and recreated instead of relying on
|
|
28
|
+
transactional rollback for that one case.
|
|
29
|
+
- Never point a test suite at a shared development or staging database —
|
|
30
|
+
a fixed, disposable test database (a local container, an ephemeral
|
|
31
|
+
schema per CI run) so tests don't observe or clobber each other's data.
|
|
32
|
+
|
|
33
|
+
## Fixtures and seed data
|
|
34
|
+
|
|
35
|
+
- Seed only the rows a given test actually needs, built through the same
|
|
36
|
+
helper/factory the rest of the suite uses — do not duplicate a large
|
|
37
|
+
shared seed script per test file, and do not depend on a global fixture
|
|
38
|
+
a different test also mutates.
|
|
39
|
+
- Name fixture data so a failure is legible (`user_with_expired_token`,
|
|
40
|
+
not `test_user_3`) — a table-driven or parameterized test case should
|
|
41
|
+
carry a descriptive case name for the same reason.
|
|
42
|
+
- A fixture representing a timestamp or "now"-relative value is
|
|
43
|
+
constructed relative to a fixed, injected clock/reference time in the
|
|
44
|
+
test, not `NOW()`/`CURRENT_TIMESTAMP` evaluated live — a live clock
|
|
45
|
+
value makes boundary cases (rows that just expired, rows created "just
|
|
46
|
+
now") flaky depending on when the test happens to run.
|
|
47
|
+
|
|
48
|
+
## Testing migrations
|
|
49
|
+
|
|
50
|
+
- Test a migration's `up` path against a database seeded to look like
|
|
51
|
+
production before the change: representative row counts for anything
|
|
52
|
+
the migration scans or backfills, not an empty table where every
|
|
53
|
+
approach looks instant.
|
|
54
|
+
- Test the reverse/`down` path when the migration states one — running
|
|
55
|
+
`up` then `down` should leave the schema equivalent to before, and
|
|
56
|
+
running `up` twice (or `down` twice) should not error when the
|
|
57
|
+
migration was written to be idempotent.
|
|
58
|
+
- For a batched backfill migration, test with a row count large enough to
|
|
59
|
+
exercise at least two batches, and assert every row was updated exactly
|
|
60
|
+
once (no row skipped at a batch boundary, none processed twice in a way
|
|
61
|
+
that would double-apply a non-idempotent update).
|
|
62
|
+
|
|
63
|
+
## Testing queries
|
|
64
|
+
|
|
65
|
+
- Assert on the query's actual result set (rows, values, ordering when
|
|
66
|
+
the query specifies `ORDER BY`), not merely that it executed without
|
|
67
|
+
error — a query testing only "did not throw" would pass even after a
|
|
68
|
+
regression silently returns zero or the wrong rows.
|
|
69
|
+
- For a query added to fix an N+1, assert the query count too (most
|
|
70
|
+
ORMs/test harnesses expose a query counter or log) — a regression test
|
|
71
|
+
for an N+1 fix should fail again if the fix regresses back to per-row
|
|
72
|
+
queries, not just check the final result shape.
|
|
73
|
+
- Test the empty-result case (a `WHERE` that matches nothing) and the
|
|
74
|
+
boundary case for any range condition (`created_at >= X`) explicitly —
|
|
75
|
+
these are exactly where an off-by-one in a range filter or a `JOIN`
|
|
76
|
+
that should have been a `LEFT JOIN` shows up.
|
|
77
|
+
|
|
78
|
+
## Verification
|
|
79
|
+
|
|
80
|
+
- Run the full suite against the transactional-rollback isolation
|
|
81
|
+
described above before considering a change to `.sql` test fixtures or
|
|
82
|
+
migration tests done — a test that passes in isolation but leaves state
|
|
83
|
+
for the next test to trip over is not actually isolated.
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sql-db-build-fix
|
|
3
|
+
description: "Use when a raw SQL migration fails to apply, a query planner regresses to a full table scan, or a constraint violation blocks a deploy -- resolves failed/broken migrations, missing-index query regressions, deadlocks, and NOT NULL/unique/foreign-key constraint violations with the smallest root-cause fix. Not for a Django/Rails/Laravel ORM's own migration file (a .py/.rb/.php migration -- use that stack's own build-fix skill)."
|
|
4
|
+
triggers:
|
|
5
|
+
- "this migration is failing to apply, help me fix it"
|
|
6
|
+
- "this query used to use the index and now does a sequential scan"
|
|
7
|
+
- "I'm getting a unique constraint violation running this migration"
|
|
8
|
+
- "this migration is failing with a foreign key violation"
|
|
9
|
+
- "the query planner stopped using this index after the last deploy"
|
|
10
|
+
- "two migrations conflict and one fails to apply"
|
|
11
|
+
metadata:
|
|
12
|
+
origin: authored
|
|
13
|
+
category: build-fix
|
|
14
|
+
version: "1.0.0"
|
|
15
|
+
compatible_harnesses: "claude,codex,cursor,zed,opencode"
|
|
16
|
+
license: "MIT"
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
# SQL / database build fix (Postgres, MySQL, generic SQL)
|
|
20
|
+
|
|
21
|
+
Resolve a failed or broken migration, a query planner regression (an
|
|
22
|
+
index that stopped being used), or a constraint violation blocking a
|
|
23
|
+
deploy — with the smallest change that fixes the actual root cause.
|
|
24
|
+
`rules/patterns.mdc` and `rules/security.mdc` govern what a "correct" fix
|
|
25
|
+
looks like; this skill never reaches for a suppression (dropping a
|
|
26
|
+
constraint, skipping a migration step) instead of a fix.
|
|
27
|
+
|
|
28
|
+
## Workflow
|
|
29
|
+
|
|
30
|
+
### Step 1: Reproduce and classify
|
|
31
|
+
|
|
32
|
+
Run the migration/query against a local or staging database that mirrors
|
|
33
|
+
the failure, and read the exact error text. Classify it:
|
|
34
|
+
|
|
35
|
+
- **Migration apply failure** — a DDL statement errors (object already
|
|
36
|
+
exists, object doesn't exist, syntax error, a lock timeout).
|
|
37
|
+
- **Constraint violation** — `NOT NULL`, `UNIQUE`, `CHECK`, or foreign-key
|
|
38
|
+
violation raised while running a migration's backfill or while the
|
|
39
|
+
application inserts/updates a row.
|
|
40
|
+
- **Planner regression** — a query that used an index before now runs a
|
|
41
|
+
sequential/full-table scan (`EXPLAIN`/`EXPLAIN ANALYZE` shows the
|
|
42
|
+
changed plan).
|
|
43
|
+
- **Deadlock/lock timeout** — two transactions each holding a lock the
|
|
44
|
+
other needs, or a migration's lock request timing out against live
|
|
45
|
+
traffic.
|
|
46
|
+
- **Migration ordering conflict** — two migrations both claim the same
|
|
47
|
+
version/sequence number, or one depends on schema state a
|
|
48
|
+
not-yet-applied migration would create.
|
|
49
|
+
|
|
50
|
+
### Step 2: Fix by category
|
|
51
|
+
|
|
52
|
+
**Migration apply failure:** read the exact error. "Object already
|
|
53
|
+
exists" on a re-run usually means the migration isn't idempotent — add
|
|
54
|
+
`IF NOT EXISTS`/`IF EXISTS` where the engine supports it, or check
|
|
55
|
+
whether a previous partial run needs manual cleanup before retrying
|
|
56
|
+
(state which, in the report). Never just delete/skip the migration file
|
|
57
|
+
to make the runner stop complaining.
|
|
58
|
+
|
|
59
|
+
**Constraint violation on backfill or insert:** find the actual row(s)
|
|
60
|
+
violating the constraint — do not disable or drop the constraint to make
|
|
61
|
+
the error go away. A `NOT NULL` violation during backfill usually means
|
|
62
|
+
the backfill's default/derivation logic missed a case (a row with a
|
|
63
|
+
legitimately different history); a `UNIQUE` violation usually means
|
|
64
|
+
duplicate data existed before the constraint was added — decide the
|
|
65
|
+
correct resolution (merge, dedupe, or a documented exception) rather than
|
|
66
|
+
loosening the constraint to accept invalid data.
|
|
67
|
+
|
|
68
|
+
**Planner regression (unused index):** confirm with
|
|
69
|
+
`EXPLAIN (ANALYZE, BUFFERS)` (Postgres) / `EXPLAIN ANALYZE` (MySQL)
|
|
70
|
+
whether the index still exists, whether statistics are stale
|
|
71
|
+
(`ANALYZE <table>` on Postgres, `ANALYZE TABLE` on MySQL, after a large
|
|
72
|
+
data change), or whether the query's own shape changed in a way that no
|
|
73
|
+
longer matches the index's column order (per `rules/patterns.mdc`'s
|
|
74
|
+
composite-order guidance) — fix the actual cause (re-run `ANALYZE`,
|
|
75
|
+
correct the index shape) rather than forcing a plan with a planner hint
|
|
76
|
+
as the first resort.
|
|
77
|
+
|
|
78
|
+
**Deadlock/lock timeout:** read which two operations held conflicting
|
|
79
|
+
locks (Postgres logs both queries and lock modes in a deadlock error;
|
|
80
|
+
MySQL's `SHOW ENGINE INNODB STATUS` shows the last deadlock). Fix by
|
|
81
|
+
making both code paths acquire locks in the same order, shortening the
|
|
82
|
+
transaction that holds the lock too long, or batching a migration's
|
|
83
|
+
backfill into smaller steps that each commit — not by retrying blindly or
|
|
84
|
+
raising the lock timeout to paper over the contention.
|
|
85
|
+
|
|
86
|
+
**Migration ordering conflict:** renumber/rebase the conflicting
|
|
87
|
+
migration against the current head per the project's migration tool's own
|
|
88
|
+
conflict-resolution convention; never force-apply one migration over
|
|
89
|
+
another's recorded state without understanding what schema each expects.
|
|
90
|
+
|
|
91
|
+
### Step 3: Verify
|
|
92
|
+
|
|
93
|
+
Re-run the migration end-to-end (up, and down if it has one) against a
|
|
94
|
+
representative dataset. Re-run the regressed query with
|
|
95
|
+
`EXPLAIN`/`EXPLAIN ANALYZE` and confirm the expected plan. Confirm no
|
|
96
|
+
constraint violation remains across the full backfill, not just the row
|
|
97
|
+
that originally failed.
|
|
98
|
+
|
|
99
|
+
### Step 4: Report
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
Fixed: migrations/2026_09_18_backfill_status.sql
|
|
103
|
+
- Root cause: 340 legacy rows had status = '' (not NULL) from a prior
|
|
104
|
+
import, which the backfill's WHERE status IS NULL clause missed
|
|
105
|
+
- Extended the backfill condition to also match status = ''
|
|
106
|
+
- Migration re-run clean against a full-size copy of the table
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
State the root cause in one sentence, not just "fixed the error."
|
|
110
|
+
|
|
111
|
+
## Rules
|
|
112
|
+
|
|
113
|
+
- Find and fix the smallest change that addresses the actual root cause —
|
|
114
|
+
never widen a fix beyond what the failure requires.
|
|
115
|
+
- NEVER drop or loosen a constraint (`NOT NULL`, `UNIQUE`, a foreign key)
|
|
116
|
+
to make a violation stop erroring instead of fixing the data or the
|
|
117
|
+
logic that produced the violation.
|
|
118
|
+
- NEVER delete or skip a failing migration step to reach a "successful"
|
|
119
|
+
migration run.
|
|
120
|
+
- NEVER force a query plan with a planner hint as the first fix for a
|
|
121
|
+
regression before checking whether statistics are stale or the index
|
|
122
|
+
shape no longer matches the query.
|
|
123
|
+
- NEVER raise a lock/statement timeout to make a deadlock or contention
|
|
124
|
+
error stop appearing without addressing the actual lock ordering or
|
|
125
|
+
transaction duration.
|
|
126
|
+
|
|
127
|
+
## Red Flags
|
|
128
|
+
|
|
129
|
+
| Rationalization | Why it is wrong |
|
|
130
|
+
|---|---|
|
|
131
|
+
| "I'll just drop the NOT NULL constraint so the insert succeeds" | Silences the violation without fixing why a NULL reached this point — the constraint existed to catch exactly this; find and fix the actual bad data or code path instead |
|
|
132
|
+
| "This migration keeps failing on re-run, I'll delete it and start over" | Deleting a migration that already partially applied against some environments leaves them permanently out of sync with a fresh one; add IF EXISTS/IF NOT EXISTS or fix the idempotency issue instead |
|
|
133
|
+
| "The query got slow, I'll just add a planner hint to force the old plan" | A hint papers over the actual cause (stale statistics, a changed index shape) and can go stale itself the next time data distribution shifts; fix the underlying cause first |
|
|
134
|
+
| "I'll bump the lock timeout so this migration stops timing out" | A longer timeout doesn't resolve the contention, it just waits longer before the same conflict occurs; fix the lock ordering or shrink the transaction instead |
|
|
135
|
+
|
|
136
|
+
## Verification
|
|
137
|
+
|
|
138
|
+
Do not report the fix done until all of the following hold:
|
|
139
|
+
|
|
140
|
+
- The migration re-runs clean end-to-end (up, and down if applicable)
|
|
141
|
+
against a representative dataset, not just the originally failing row.
|
|
142
|
+
- No constraint was dropped or loosened as part of the fix, unless the
|
|
143
|
+
report explicitly states the constraint itself was wrong and why.
|
|
144
|
+
- For a planner regression, `EXPLAIN`/`EXPLAIN ANALYZE` confirms the
|
|
145
|
+
expected plan after the fix, not just that the query no longer errors.
|
|
146
|
+
- The report states the root cause in one sentence, not just "it works
|
|
147
|
+
now."
|