Upload index.html with huggingface_hub
Browse files- index.html +461 -17
index.html
CHANGED
|
@@ -1,19 +1,463 @@
|
|
| 1 |
<!doctype html>
|
| 2 |
-
<html>
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
</html>
|
|
|
|
| 1 |
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="utf-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
| 6 |
+
<title>Postgres Migration Lock Lint — free online check</title>
|
| 7 |
+
<style>
|
| 8 |
+
:root{color-scheme:light}body{margin:0;font:15px/1.55 system-ui,-apple-system,"Segoe UI",sans-serif;background:#faf9f5;color:#1a1512}
|
| 9 |
+
.wrap{max-width:960px;margin:0 auto;padding:28px 18px 60px}h1{font-size:26px;margin:0 0 6px}.lede{color:#4a403a;margin:0 0 16px}
|
| 10 |
+
textarea{width:100%;min-height:260px;font:13px/1.45 ui-monospace,Menlo,Consolas,monospace;padding:12px;border:1px solid #d9d0c4;border-radius:8px;box-sizing:border-box}
|
| 11 |
+
.bar{display:flex;gap:10px;align-items:center;margin:12px 0}button{font:600 15px system-ui;padding:10px 18px;border:0;border-radius:8px;background:#1a1512;color:#fff;cursor:pointer}
|
| 12 |
+
input[type=date]{font:14px system-ui;padding:8px;border:1px solid #d9d0c4;border-radius:6px}
|
| 13 |
+
table{border-collapse:collapse;width:100%;font-size:14px;margin-top:10px}th,td{text-align:left;padding:8px 10px;border-bottom:1px solid #eee;vertical-align:top}th{font-size:12px;letter-spacing:.06em;text-transform:uppercase;color:#7a6e65}
|
| 14 |
+
.sev.error{color:#a8551f;font-weight:700}.sev.warn,.sev.warning{color:#96691f}.ok{color:#2a6446;font-weight:600}.sum{font-weight:600}
|
| 15 |
+
.note{font-size:13px;color:#7a6e65;margin-top:18px}#paid{margin-top:22px;padding:14px 16px;border:1px solid #e2d8cb;border-radius:10px;background:#fffdfa}
|
| 16 |
+
</style>
|
| 17 |
+
<script data-grow-initializer="">!(function(){window.growMe||((window.growMe=function(e){window.growMe._.push(e);}),(window.growMe._=[]));var e=document.createElement("script");(e.type="text/javascript"),(e.src="https://faves.grow.me/main.js"),(e.defer=!0),e.setAttribute("data-grow-faves-site-id","U2l0ZTphZjljMzgyMC0yMmU5LTRhYjgtYTIxNS03OTRhMGY1N2ExYzM=");var t=document.getElementsByTagName("script")[0];t.parentNode.insertBefore(e,t);})();</script>
|
| 18 |
+
<!--yj-canon-s101--><link rel="canonical" href="https://getreadystack.com/tools/pg-migration-lock-lint"></head>
|
| 19 |
+
<body>
|
| 20 |
+
<div class="wrap">
|
| 21 |
+
<h1>Postgres Migration Lock Lint</h1>
|
| 22 |
+
<p class="lede">Find the statements in your Rails, Django or raw SQL migrations that take a table-blocking Postgres lock - and the safe rewrite - before you deploy. Runs entirely in your browser — nothing is uploaded.</p>
|
| 23 |
+
<textarea id="src" spellcheck="false">-- orders: add tenancy + tighten money columns (generated, reviewed by nobody)
|
| 24 |
+
BEGIN;
|
| 25 |
+
|
| 26 |
+
ALTER TABLE orders ADD COLUMN tenant_id bigint NOT NULL;
|
| 27 |
+
ALTER TABLE orders ADD COLUMN external_ref text UNIQUE;
|
| 28 |
+
ALTER TABLE orders ADD COLUMN imported_at timestamptz DEFAULT now();
|
| 29 |
+
ALTER TABLE orders ALTER COLUMN amount_cents TYPE numeric(12,2);
|
| 30 |
+
ALTER TABLE orders ALTER COLUMN currency SET NOT NULL;
|
| 31 |
+
ALTER TABLE orders ADD CONSTRAINT orders_tenant_fk FOREIGN KEY (tenant_id) REFERENCES tenants (id);
|
| 32 |
+
ALTER TABLE orders ADD CONSTRAINT orders_amount_positive CHECK (amount_cents > 0);
|
| 33 |
+
ALTER TABLE orders ADD CONSTRAINT orders_ref_uniq UNIQUE (external_ref);
|
| 34 |
+
ALTER TABLE orders RENAME COLUMN legacy_total TO total_cents;
|
| 35 |
+
ALTER TABLE orders DROP COLUMN legacy_notes;
|
| 36 |
+
|
| 37 |
+
CREATE INDEX idx_orders_tenant ON orders (tenant_id);
|
| 38 |
+
CREATE INDEX CONCURRENTLY idx_orders_created ON orders (created_at);
|
| 39 |
+
DROP INDEX idx_orders_old_ref;
|
| 40 |
+
REINDEX TABLE orders;
|
| 41 |
+
CLUSTER orders USING idx_orders_tenant;
|
| 42 |
+
|
| 43 |
+
LOCK TABLE orders;
|
| 44 |
+
UPDATE orders SET tenant_id = 1;
|
| 45 |
+
DELETE FROM order_events;
|
| 46 |
+
TRUNCATE order_archive;
|
| 47 |
+
VACUUM FULL orders;
|
| 48 |
+
|
| 49 |
+
COMMIT;</textarea>
|
| 50 |
+
<div class="bar"><button id="go">Check</button><label>as of <input type="date" id="today"></label></div>
|
| 51 |
+
<div id="out" aria-live="polite"></div>
|
| 52 |
+
<div id="paid"><strong>Free here:</strong> Audit the migration file you have open and name every statement that takes a blocking Postgres lock, with the safe rewrite for your framework.. <strong>In the editor:</strong> Audit every migration in the workspace in one pass and export the review as Markdown or JSON to attach to the change ticket, with a CI exit-code gate for the team. — that part asks for a licence key.</div>
|
| 53 |
+
<p class="note">Same engine as the VS Code extension, byte for byte.</p>
|
| 54 |
+
</div>
|
| 55 |
+
<script>var PGML_RULES = [
|
| 56 |
+
{
|
| 57 |
+
"id": "create_index_blocking",
|
| 58 |
+
"sev": "error",
|
| 59 |
+
"on": "stmt",
|
| 60 |
+
"pattern": "\\bCREATE\\s+(UNIQUE\\s+)?INDEX\\b",
|
| 61 |
+
"absent": "\\bCONCURRENTLY\\b",
|
| 62 |
+
"msg": "CREATE INDEX holds a SHARE lock: every INSERT, UPDATE and DELETE on the table waits until the index finishes. Safe: CREATE INDEX CONCURRENTLY, outside any transaction block."
|
| 63 |
+
},
|
| 64 |
+
{
|
| 65 |
+
"id": "drop_index_blocking",
|
| 66 |
+
"sev": "error",
|
| 67 |
+
"on": "stmt",
|
| 68 |
+
"pattern": "\\bDROP\\s+INDEX\\b",
|
| 69 |
+
"absent": "\\bCONCURRENTLY\\b",
|
| 70 |
+
"msg": "DROP INDEX takes ACCESS EXCLUSIVE on the table, so reads block too. Safe: DROP INDEX CONCURRENTLY."
|
| 71 |
+
},
|
| 72 |
+
{
|
| 73 |
+
"id": "concurrently_inside_transaction",
|
| 74 |
+
"sev": "error",
|
| 75 |
+
"on": "file",
|
| 76 |
+
"pattern": "\\bCONCURRENTLY\\b",
|
| 77 |
+
"require": "^\\s*BEGIN\\s*;|\\bSTART\\s+TRANSACTION\\b",
|
| 78 |
+
"msg": "CREATE/DROP INDEX CONCURRENTLY cannot run inside a transaction block and will abort with error 25001. Safe: move it out of the BEGIN/COMMIT, one statement per migration."
|
| 79 |
+
},
|
| 80 |
+
{
|
| 81 |
+
"id": "add_column_volatile_default",
|
| 82 |
+
"sev": "error",
|
| 83 |
+
"on": "stmt",
|
| 84 |
+
"pattern": "\\bADD\\s+(COLUMN\\s+)?",
|
| 85 |
+
"present": "\\bDEFAULT\\s+(now\\s*\\(|current_timestamp|clock_timestamp|random\\s*\\(|gen_random_uuid|uuid_generate|nextval)",
|
| 86 |
+
"msg": "A volatile DEFAULT rewrites every row under ACCESS EXCLUSIVE - the fast path added in PG 11 only covers constant defaults. Safe: add the column with no default, backfill in batches, then SET DEFAULT."
|
| 87 |
+
},
|
| 88 |
+
{
|
| 89 |
+
"id": "add_column_not_null_no_default",
|
| 90 |
+
"sev": "error",
|
| 91 |
+
"on": "stmt",
|
| 92 |
+
"pattern": "\\bADD\\s+(COLUMN\\s+)?(?!CONSTRAINT\\b)[\"\\w]+[^,;()]*\\bNOT\\s+NULL\\b",
|
| 93 |
+
"absent": "\\bDEFAULT\\b",
|
| 94 |
+
"msg": "ADD COLUMN ... NOT NULL with no DEFAULT fails outright on a table that already has rows (23502). Safe: give it a constant DEFAULT, or add it nullable and SET NOT NULL after a validated CHECK."
|
| 95 |
+
},
|
| 96 |
+
{
|
| 97 |
+
"id": "add_column_unique",
|
| 98 |
+
"sev": "error",
|
| 99 |
+
"on": "stmt",
|
| 100 |
+
"pattern": "\\bADD\\s+(COLUMN\\s+)?(?!CONSTRAINT\\b)[\"\\w]+[^,;()]*\\bUNIQUE\\b",
|
| 101 |
+
"msg": "UNIQUE on ADD COLUMN builds a unique index while ACCESS EXCLUSIVE is still held, so writes and reads both stop. Safe: CREATE UNIQUE INDEX CONCURRENTLY, then ADD CONSTRAINT ... USING INDEX."
|
| 102 |
+
},
|
| 103 |
+
{
|
| 104 |
+
"id": "alter_column_type",
|
| 105 |
+
"sev": "error",
|
| 106 |
+
"on": "stmt",
|
| 107 |
+
"pattern": "\\bALTER\\s+(COLUMN\\s+)?[\"\\w]+\\s+(SET\\s+DATA\\s+)?TYPE\\b",
|
| 108 |
+
"msg": "Changing a column type rewrites the whole table and every index on it under ACCESS EXCLUSIVE. Safe: add a new column, backfill in batches, swap the reads, then drop the old column in a later deploy."
|
| 109 |
+
},
|
| 110 |
+
{
|
| 111 |
+
"id": "set_not_null_scan",
|
| 112 |
+
"sev": "warn",
|
| 113 |
+
"on": "stmt",
|
| 114 |
+
"pattern": "\\bSET\\s+NOT\\s+NULL\\b",
|
| 115 |
+
"msg": "SET NOT NULL scans the whole table under ACCESS EXCLUSIVE. PG 12 and later skip that scan only when a validated CHECK (col IS NOT NULL) already exists. Safe: ADD CONSTRAINT ... CHECK (col IS NOT NULL) NOT VALID, VALIDATE CONSTRAINT, then SET NOT NULL."
|
| 116 |
+
},
|
| 117 |
+
{
|
| 118 |
+
"id": "add_foreign_key_validating",
|
| 119 |
+
"sev": "error",
|
| 120 |
+
"on": "stmt",
|
| 121 |
+
"pattern": "\\bADD\\s+(CONSTRAINT\\s+[\"\\w]+\\s+)?FOREIGN\\s+KEY\\b",
|
| 122 |
+
"absent": "\\bNOT\\s+VALID\\b",
|
| 123 |
+
"msg": "A validating FOREIGN KEY locks both tables (ACCESS EXCLUSIVE on the child, SHARE ROW EXCLUSIVE on the parent) while it scans. Safe: ADD CONSTRAINT ... NOT VALID, then VALIDATE CONSTRAINT in a second statement."
|
| 124 |
+
},
|
| 125 |
+
{
|
| 126 |
+
"id": "add_check_validating",
|
| 127 |
+
"sev": "error",
|
| 128 |
+
"on": "stmt",
|
| 129 |
+
"pattern": "\\bADD\\s+(CONSTRAINT\\s+[\"\\w]+\\s+)?CHECK\\s*\\(",
|
| 130 |
+
"absent": "\\bNOT\\s+VALID\\b",
|
| 131 |
+
"msg": "A validating CHECK constraint scans the table under ACCESS EXCLUSIVE. Safe: ADD CONSTRAINT ... CHECK (...) NOT VALID, then VALIDATE CONSTRAINT, which only needs SHARE UPDATE EXCLUSIVE."
|
| 132 |
+
},
|
| 133 |
+
{
|
| 134 |
+
"id": "add_pk_or_unique_constraint",
|
| 135 |
+
"sev": "error",
|
| 136 |
+
"on": "stmt",
|
| 137 |
+
"pattern": "\\bADD\\s+(CONSTRAINT\\s+[\"\\w]+\\s+)?(PRIMARY\\s+KEY|UNIQUE)\\s*\\(",
|
| 138 |
+
"absent": "\\bUSING\\s+INDEX\\b",
|
| 139 |
+
"msg": "This builds the backing index inside the ACCESS EXCLUSIVE lock. Safe: CREATE UNIQUE INDEX CONCURRENTLY first, then ADD CONSTRAINT ... USING INDEX, which only attaches the finished index."
|
| 140 |
+
},
|
| 141 |
+
{
|
| 142 |
+
"id": "rename_breaks_running_code",
|
| 143 |
+
"sev": "warn",
|
| 144 |
+
"on": "stmt",
|
| 145 |
+
"pattern": "\\bRENAME\\s+(COLUMN\\s+|TO\\b)",
|
| 146 |
+
"msg": "The rename itself is instant, but the old pods are still sending the old name and start erroring at 42703 the moment it commits. Safe: add the new name, write to both, migrate readers, drop the old name a deploy later."
|
| 147 |
+
},
|
| 148 |
+
{
|
| 149 |
+
"id": "drop_column_breaks_running_code",
|
| 150 |
+
"sev": "warn",
|
| 151 |
+
"on": "stmt",
|
| 152 |
+
"pattern": "\\bDROP\\s+COLUMN\\b",
|
| 153 |
+
"msg": "DROP COLUMN is metadata-only, but any SELECT * cached by the still-running old release breaks on commit. Safe: stop referencing the column in a shipped release first (Rails: ignored_columns), then drop it."
|
| 154 |
+
},
|
| 155 |
+
{
|
| 156 |
+
"id": "vacuum_full_blocking",
|
| 157 |
+
"sev": "error",
|
| 158 |
+
"on": "stmt",
|
| 159 |
+
"pattern": "\\bVACUUM\\s+FULL\\b",
|
| 160 |
+
"msg": "VACUUM FULL holds ACCESS EXCLUSIVE for the whole rewrite and needs room for a second copy of the table. Safe: leave it out of the migration; use pg_repack or plain VACUUM."
|
| 161 |
+
},
|
| 162 |
+
{
|
| 163 |
+
"id": "reindex_blocking",
|
| 164 |
+
"sev": "error",
|
| 165 |
+
"on": "stmt",
|
| 166 |
+
"pattern": "\\bREINDEX\\b",
|
| 167 |
+
"absent": "\\bCONCURRENTLY\\b",
|
| 168 |
+
"msg": "REINDEX blocks writes on the table (and reads on the index) until it finishes. Safe: REINDEX CONCURRENTLY, available since PG 12."
|
| 169 |
+
},
|
| 170 |
+
{
|
| 171 |
+
"id": "cluster_blocking",
|
| 172 |
+
"sev": "error",
|
| 173 |
+
"on": "stmt",
|
| 174 |
+
"pattern": "\\bCLUSTER\\b\\s+[\"\\w]",
|
| 175 |
+
"msg": "CLUSTER holds ACCESS EXCLUSIVE while it rewrites the table in index order - nothing reads it meanwhile. Safe: do not put CLUSTER in a migration."
|
| 176 |
+
},
|
| 177 |
+
{
|
| 178 |
+
"id": "truncate_in_migration",
|
| 179 |
+
"sev": "error",
|
| 180 |
+
"on": "stmt",
|
| 181 |
+
"pattern": "\\bTRUNCATE\\b",
|
| 182 |
+
"msg": "TRUNCATE takes ACCESS EXCLUSIVE and deletes rows no backfill can bring back. Safe: DELETE in batches with a WHERE, or move the cleanup out of the migration."
|
| 183 |
+
},
|
| 184 |
+
{
|
| 185 |
+
"id": "explicit_lock_table",
|
| 186 |
+
"sev": "warn",
|
| 187 |
+
"on": "stmt",
|
| 188 |
+
"pattern": "\\bLOCK\\s+(TABLE\\s+)?[\"\\w]",
|
| 189 |
+
"msg": "LOCK TABLE defaults to ACCESS EXCLUSIVE, the strongest mode there is. Safe: name the weakest mode you actually need (IN SHARE UPDATE EXCLUSIVE MODE) and set a lock_timeout."
|
| 190 |
+
},
|
| 191 |
+
{
|
| 192 |
+
"id": "unbounded_backfill",
|
| 193 |
+
"sev": "error",
|
| 194 |
+
"on": "stmt",
|
| 195 |
+
"pattern": "\\bUPDATE\\s+[\"\\w.]+\\s+SET\\b",
|
| 196 |
+
"absent": "\\bWHERE\\b",
|
| 197 |
+
"msg": "An UPDATE with no WHERE takes a row lock on every row in one transaction and blocks writers behind it for the whole run. Safe: loop over primary-key ranges, committing each batch."
|
| 198 |
+
},
|
| 199 |
+
{
|
| 200 |
+
"id": "delete_unbounded",
|
| 201 |
+
"sev": "warn",
|
| 202 |
+
"on": "stmt",
|
| 203 |
+
"pattern": "\\bDELETE\\s+FROM\\s+[\"\\w.]+",
|
| 204 |
+
"absent": "\\bWHERE\\b",
|
| 205 |
+
"msg": "DELETE with no WHERE writes a dead tuple for every row and holds them all in one transaction. Safe: delete in batches with a WHERE, or use a partition drop."
|
| 206 |
+
},
|
| 207 |
+
{
|
| 208 |
+
"id": "no_lock_timeout",
|
| 209 |
+
"sev": "error",
|
| 210 |
+
"on": "file",
|
| 211 |
+
"pattern": "\\b(ALTER\\s+TABLE|CREATE\\s+(UNIQUE\\s+)?INDEX)\\b",
|
| 212 |
+
"require": ";",
|
| 213 |
+
"absent": "lock_timeout",
|
| 214 |
+
"msg": "No lock_timeout in this migration. If the DDL queues behind one long SELECT, every query that arrives after it queues behind the DDL - that is the outage, not the DDL itself. Safe: SET lock_timeout = '3s'; and retry."
|
| 215 |
+
},
|
| 216 |
+
{
|
| 217 |
+
"id": "ddl_and_backfill_same_transaction",
|
| 218 |
+
"sev": "warn",
|
| 219 |
+
"on": "file",
|
| 220 |
+
"pattern": "\\b(UPDATE|INSERT\\s+INTO)\\b",
|
| 221 |
+
"require": "^\\s*BEGIN\\s*;[\\s\\S]*\\bALTER\\s+TABLE\\b",
|
| 222 |
+
"msg": "A backfill in the same transaction as the DDL keeps the ACCESS EXCLUSIVE lock for as long as the backfill runs. Safe: one transaction for the DDL, a separate batched job for the data."
|
| 223 |
+
},
|
| 224 |
+
{
|
| 225 |
+
"id": "rails_add_index_blocking",
|
| 226 |
+
"sev": "error",
|
| 227 |
+
"on": "line",
|
| 228 |
+
"pattern": "\\badd_index\\b",
|
| 229 |
+
"absent": "algorithm:\\s*:concurrently",
|
| 230 |
+
"msg": "add_index builds the index with a SHARE lock and blocks writes. Safe: add_index ..., algorithm: :concurrently, plus disable_ddl_transaction! on the migration class."
|
| 231 |
+
},
|
| 232 |
+
{
|
| 233 |
+
"id": "rails_add_reference_index",
|
| 234 |
+
"sev": "error",
|
| 235 |
+
"on": "line",
|
| 236 |
+
"pattern": "\\badd_(reference|belongs_to)\\b",
|
| 237 |
+
"absent": "index:\\s*false|algorithm:\\s*:concurrently",
|
| 238 |
+
"msg": "add_reference creates its index the blocking way by default. Safe: add_reference ..., index: false, then add_index with algorithm: :concurrently."
|
| 239 |
+
},
|
| 240 |
+
{
|
| 241 |
+
"id": "rails_change_column",
|
| 242 |
+
"sev": "error",
|
| 243 |
+
"on": "line",
|
| 244 |
+
"pattern": "\\bchange_column\\s*[( ]",
|
| 245 |
+
"msg": "change_column emits ALTER COLUMN TYPE, which rewrites the table under ACCESS EXCLUSIVE. Safe: new column, batched backfill, swap readers, drop the old one later."
|
| 246 |
+
},
|
| 247 |
+
{
|
| 248 |
+
"id": "rails_change_column_null",
|
| 249 |
+
"sev": "warn",
|
| 250 |
+
"on": "line",
|
| 251 |
+
"pattern": "\\bchange_column_null\\b",
|
| 252 |
+
"msg": "change_column_null(false) emits SET NOT NULL and scans the table. Safe: add a CHECK ... NOT VALID, validate it, then set the column NOT NULL."
|
| 253 |
+
},
|
| 254 |
+
{
|
| 255 |
+
"id": "rails_remove_column",
|
| 256 |
+
"sev": "warn",
|
| 257 |
+
"on": "line",
|
| 258 |
+
"pattern": "\\bremove_columns?\\b",
|
| 259 |
+
"msg": "Removing a column breaks the release that is still running and still selecting it. Safe: ship self.ignored_columns += [...] first, then remove the column in the next deploy."
|
| 260 |
+
},
|
| 261 |
+
{
|
| 262 |
+
"id": "rails_rename",
|
| 263 |
+
"sev": "warn",
|
| 264 |
+
"on": "line",
|
| 265 |
+
"pattern": "\\brename_(column|table)\\b",
|
| 266 |
+
"msg": "A rename breaks every pod of the old release the instant it commits. Safe: add the new name, write to both, move readers, drop the old name in a later migration."
|
| 267 |
+
},
|
| 268 |
+
{
|
| 269 |
+
"id": "rails_add_foreign_key",
|
| 270 |
+
"sev": "error",
|
| 271 |
+
"on": "line",
|
| 272 |
+
"pattern": "\\badd_foreign_key\\b",
|
| 273 |
+
"absent": "validate:\\s*false",
|
| 274 |
+
"msg": "add_foreign_key validates existing rows while holding locks on both tables. Safe: add_foreign_key ..., validate: false, then validate_foreign_key in a second migration."
|
| 275 |
+
},
|
| 276 |
+
{
|
| 277 |
+
"id": "rails_concurrently_needs_disable_ddl",
|
| 278 |
+
"sev": "error",
|
| 279 |
+
"on": "file",
|
| 280 |
+
"pattern": "algorithm:\\s*:concurrently",
|
| 281 |
+
"absent": "disable_ddl_transaction!",
|
| 282 |
+
"msg": "algorithm: :concurrently inside Rails' automatic transaction aborts with 25001. Safe: add disable_ddl_transaction! at the top of the migration class."
|
| 283 |
+
},
|
| 284 |
+
{
|
| 285 |
+
"id": "django_add_index_blocking",
|
| 286 |
+
"sev": "error",
|
| 287 |
+
"on": "line",
|
| 288 |
+
"pattern": "migrations\\.AddIndex\\(",
|
| 289 |
+
"msg": "AddIndex emits a plain CREATE INDEX and blocks writes. Safe: AddIndexConcurrently from django.contrib.postgres.operations, with atomic = False on the Migration class."
|
| 290 |
+
},
|
| 291 |
+
{
|
| 292 |
+
"id": "django_alter_field",
|
| 293 |
+
"sev": "error",
|
| 294 |
+
"on": "line",
|
| 295 |
+
"pattern": "migrations\\.AlterField\\(",
|
| 296 |
+
"msg": "AlterField can emit ALTER COLUMN TYPE or SET NOT NULL - both scan or rewrite the table under ACCESS EXCLUSIVE. Safe: run sqlmigrate on it first and split the change across two deploys."
|
| 297 |
+
},
|
| 298 |
+
{
|
| 299 |
+
"id": "django_rename",
|
| 300 |
+
"sev": "warn",
|
| 301 |
+
"on": "line",
|
| 302 |
+
"pattern": "migrations\\.Rename(Field|Model)\\(",
|
| 303 |
+
"msg": "A rename breaks the web workers still running the old code. Safe: add the new field, write to both, move readers, then remove the old field."
|
| 304 |
+
},
|
| 305 |
+
{
|
| 306 |
+
"id": "django_remove",
|
| 307 |
+
"sev": "warn",
|
| 308 |
+
"on": "line",
|
| 309 |
+
"pattern": "migrations\\.Remove(Field|Model)\\(",
|
| 310 |
+
"msg": "Removing a field breaks the old release that is still selecting it. Safe: stop referencing it in a shipped release, then remove it in the next migration."
|
| 311 |
+
},
|
| 312 |
+
{
|
| 313 |
+
"id": "django_runpython_atomic",
|
| 314 |
+
"sev": "warn",
|
| 315 |
+
"on": "file",
|
| 316 |
+
"pattern": "migrations\\.RunPython\\(",
|
| 317 |
+
"absent": "atomic\\s*=\\s*False",
|
| 318 |
+
"msg": "RunPython inside an atomic migration holds every lock the schema operations took for the whole data pass. Safe: atomic = False on the Migration class and commit the backfill in batches."
|
| 319 |
+
},
|
| 320 |
+
{
|
| 321 |
+
"id": "django_concurrently_needs_non_atomic",
|
| 322 |
+
"sev": "error",
|
| 323 |
+
"on": "file",
|
| 324 |
+
"pattern": "(AddIndexConcurrently|RemoveIndexConcurrently)\\(",
|
| 325 |
+
"absent": "atomic\\s*=\\s*False",
|
| 326 |
+
"msg": "AddIndexConcurrently in an atomic migration aborts with 25001. Safe: set atomic = False on the Migration class."
|
| 327 |
+
}
|
| 328 |
+
];</script>
|
| 329 |
+
<script>'use strict';
|
| 330 |
+
// Postgres Migration Lock Lint - one brain, used by the VS Code extension and by the free web page.
|
| 331 |
+
// Rules live in rules.json so the same file can be read by node or injected into the browser page.
|
| 332 |
+
var PGML_DOC = (typeof module !== 'undefined') ? require('./rules.json') : window.PGML_RULES;
|
| 333 |
+
var PGML_RULE_LIST = Array.isArray(PGML_DOC) ? PGML_DOC : PGML_DOC.rules;
|
| 334 |
+
var PGML_TARGET_PG = 'PostgreSQL 12-17';
|
| 335 |
+
|
| 336 |
+
// Blank out comments without moving any character, so reported line numbers stay exact.
|
| 337 |
+
function pgmlStrip(text) {
|
| 338 |
+
function blank(m) { return m.replace(/[^\n]/g, ' '); }
|
| 339 |
+
return String(text)
|
| 340 |
+
.replace(/\/\*[\s\S]*?\*\//g, blank)
|
| 341 |
+
.replace(/--[^\n]*/g, blank)
|
| 342 |
+
.replace(/^[ \t]*#[^\n]*/gm, blank);
|
| 343 |
+
}
|
| 344 |
+
|
| 345 |
+
function pgmlLineAt(text, offset) {
|
| 346 |
+
var n = 1;
|
| 347 |
+
for (var i = 0; i < offset && i < text.length; i++) {
|
| 348 |
+
if (text.charCodeAt(i) === 10) { n++; }
|
| 349 |
+
}
|
| 350 |
+
return n;
|
| 351 |
+
}
|
| 352 |
+
|
| 353 |
+
// SQL is graded per statement (a statement can span many lines); frameworks are graded per line.
|
| 354 |
+
function pgmlStatements(text) {
|
| 355 |
+
var out = [], start = 0;
|
| 356 |
+
for (var i = 0; i <= text.length; i++) {
|
| 357 |
+
if (i === text.length || text.charAt(i) === ';') {
|
| 358 |
+
var raw = text.slice(start, i);
|
| 359 |
+
if (raw.trim()) { out.push({ text: raw, offset: start }); }
|
| 360 |
+
start = i + 1;
|
| 361 |
+
}
|
| 362 |
+
}
|
| 363 |
+
return out;
|
| 364 |
+
}
|
| 365 |
+
|
| 366 |
+
function pgmlCheck(text, opts) {
|
| 367 |
+
opts = opts || {};
|
| 368 |
+
var src = pgmlStrip(text == null ? '' : text);
|
| 369 |
+
var stmts = pgmlStatements(src);
|
| 370 |
+
var lines = src.split(/\r?\n/);
|
| 371 |
+
var findings = [], seen = {};
|
| 372 |
+
|
| 373 |
+
function push(rule, line) {
|
| 374 |
+
var key = rule.id + '@' + line;
|
| 375 |
+
if (seen[key]) { return; }
|
| 376 |
+
seen[key] = 1;
|
| 377 |
+
findings.push({ check: rule.id, sev: rule.sev, msg: rule.msg, line: line });
|
| 378 |
+
}
|
| 379 |
+
function rx(pattern, flags) { return new RegExp(pattern, flags); }
|
| 380 |
+
|
| 381 |
+
for (var r = 0; r < PGML_RULE_LIST.length; r++) {
|
| 382 |
+
var rule = PGML_RULE_LIST[r];
|
| 383 |
+
var pf = rule.flags || 'i', af = 'im';
|
| 384 |
+
if (rule.on === 'file') {
|
| 385 |
+
if (rule.require && !rx(rule.require, af).test(src)) { continue; }
|
| 386 |
+
var fm = rx(rule.pattern, pf).exec(src);
|
| 387 |
+
if (!fm) { continue; }
|
| 388 |
+
if (rule.absent && rx(rule.absent, af).test(src)) { continue; }
|
| 389 |
+
push(rule, pgmlLineAt(src, fm.index));
|
| 390 |
+
} else if (rule.on === 'line') {
|
| 391 |
+
for (var i = 0; i < lines.length; i++) {
|
| 392 |
+
var lm = rx(rule.pattern, pf).exec(lines[i]);
|
| 393 |
+
if (!lm) { continue; }
|
| 394 |
+
if (rule.absent && rx(rule.absent, af).test(lines[i])) { continue; }
|
| 395 |
+
if (rule.present && !rx(rule.present, af).test(lines[i])) { continue; }
|
| 396 |
+
push(rule, i + 1);
|
| 397 |
+
}
|
| 398 |
+
} else {
|
| 399 |
+
for (var s = 0; s < stmts.length; s++) {
|
| 400 |
+
var st = stmts[s];
|
| 401 |
+
var sm = rx(rule.pattern, pf).exec(st.text);
|
| 402 |
+
if (!sm) { continue; }
|
| 403 |
+
if (rule.absent && rx(rule.absent, af).test(st.text)) { continue; }
|
| 404 |
+
if (rule.present && !rx(rule.present, af).test(st.text)) { continue; }
|
| 405 |
+
push(rule, pgmlLineAt(src, st.offset + sm.index));
|
| 406 |
+
}
|
| 407 |
+
}
|
| 408 |
+
}
|
| 409 |
+
findings.sort(function (a, b) {
|
| 410 |
+
return (a.line - b.line) || (a.check < b.check ? -1 : a.check > b.check ? 1 : 0);
|
| 411 |
+
});
|
| 412 |
+
return { findings: findings, rule_count: PGML_RULE_LIST.length, target_pg: PGML_TARGET_PG };
|
| 413 |
+
}
|
| 414 |
+
|
| 415 |
+
var PGML_API = { engine: { check: pgmlCheck }, RULES: PGML_RULE_LIST, RULE_COUNT: PGML_RULE_LIST.length };
|
| 416 |
+
if (typeof module !== 'undefined' && module.exports) { module.exports = PGML_API; }
|
| 417 |
+
if (typeof window !== 'undefined') { window.PGMLENGINE = PGML_API; }
|
| 418 |
+
</script>
|
| 419 |
+
<script>// ⛔뼈대 — kit/scaffold.py 가 찍는다 (s143). findings [{check,sev,msg,line?}] → 글/HTML. 같은 파일이 Node 와 브라우저에서 산다.
|
| 420 |
+
'use strict';
|
| 421 |
+
function escapeHtml(s) { return String(s == null ? '' : s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'); }
|
| 422 |
+
function summaryLine(res) {
|
| 423 |
+
const f = (res && res.findings) || []; const e = f.filter(function (x) { return String(x.sev || 'error').toLowerCase().startsWith('err'); }).length;
|
| 424 |
+
return f.length ? (f.length + ' finding' + (f.length === 1 ? '' : 's') + ' (' + e + ' error' + (e === 1 ? '' : 's') + ')') : 'Clean — no findings.';
|
| 425 |
+
}
|
| 426 |
+
function toText(res, label) {
|
| 427 |
+
const f = (res && res.findings) || [];
|
| 428 |
+
const lines = ['== ' + (label || '') + ' — ' + summaryLine(res)];
|
| 429 |
+
f.forEach(function (x) { lines.push(' ' + String(x.sev || 'error').toUpperCase().slice(0, 4) + ' ' + (x.line ? 'L' + x.line + ' ' : '') + (x.check || x.id || '') + ' ' + (x.msg || x.message || '')); });
|
| 430 |
+
return lines.join('\n');
|
| 431 |
+
}
|
| 432 |
+
function toHtml(res) {
|
| 433 |
+
const f = (res && res.findings) || [];
|
| 434 |
+
if (!f.length) return '<p class="ok">' + escapeHtml(summaryLine(res)) + '</p>';
|
| 435 |
+
const rows = f.map(function (x) { return '<tr><td class="sev ' + escapeHtml(String(x.sev || 'error')) + '">' + escapeHtml(String(x.sev || 'error')) + '</td><td>' + (x.line ? 'L' + escapeHtml(x.line) : '') + '</td><td><code>' + escapeHtml(x.check || x.id || '') + '</code></td><td>' + escapeHtml(x.msg || x.message || '') + '</td></tr>'; }).join('');
|
| 436 |
+
return '<p class="sum">' + escapeHtml(summaryLine(res)) + '</p><table><thead><tr><th>Level</th><th>Line</th><th>Check</th><th>What</th></tr></thead><tbody>' + rows + '</tbody></table>';
|
| 437 |
+
}
|
| 438 |
+
var PGMLREPORT = { escapeHtml: escapeHtml, summaryLine: summaryLine, toText: toText, toHtml: toHtml };
|
| 439 |
+
if (typeof module !== 'undefined' && module.exports) module.exports = PGMLREPORT;
|
| 440 |
+
else window.PGMLREPORT = PGMLREPORT;
|
| 441 |
+
</script>
|
| 442 |
+
<script>
|
| 443 |
+
(function(){
|
| 444 |
+
var src=document.getElementById('src'), out=document.getElementById('out'), go=document.getElementById('go'), today=document.getElementById('today');
|
| 445 |
+
today.value=new Date().toISOString().slice(0,10);
|
| 446 |
+
function run(){ try { var res=window.PGMLENGINE.engine.check(src.value,{today:today.value}); out.innerHTML=window.PGMLREPORT.toHtml(res); } catch(e){ out.innerHTML='<p class="sev error">Could not run: '+String(e.message||e)+'</p>'; } }
|
| 447 |
+
go.onclick=run; if (src.value.trim()) run();
|
| 448 |
+
})();
|
| 449 |
+
</script>
|
| 450 |
+
|
| 451 |
+
|
| 452 |
+
<section data-yj-install="free" style="margin:2rem auto 1rem;max-width:46rem;padding:1.25rem 1.25rem;border:1px solid #cfe3d6;border-radius:12px;background:#f6fbf8;font:16px/1.6 system-ui,-apple-system,Segoe UI,sans-serif"><h2 style="margin:0 0 .4rem;font-size:1.2rem">Install free</h2><p style="margin:0 0 .8rem;color:#41505f">Free for the file open in your editor - no key, no limit. The workspace sweep and the report ask for a key.</p><a href="https://marketplace.visualstudio.com/items?itemName=ReadyStack.pg-migration-lock-lint" rel="noopener" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">VS Code Marketplace</a><a href="https://open-vsx.org/extension/readystack/pg-migration-lock-lint" rel="noopener" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">Open VSX (Cursor, VSCodium)</a><a href="https://www.npmjs.com/package/@readystack/pg-migration-lock-lint" rel="noopener" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">npm</a><a href="https://hub.docker.com/r/getreadystack/pg-migration-lock-lint" rel="noopener" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">Docker Hub</a><pre style="margin:.5rem 0 0;padding:.6rem .8rem;background:#f3f5f8;border-radius:8px;font-size:.9rem;overflow:auto">ext install ReadyStack.pg-migration-lock-lint
|
| 453 |
+
npx @readystack/pg-migration-lock-lint <file>
|
| 454 |
+
docker run --rm -v "$PWD:/w" getreadystack/pg-migration-lock-lint /w</pre><p style="margin:.6rem 0 0;font-size:.92rem;color:#41505f"><a href="https://buy.polar.sh/polar_cl_l6iN1uWt0FwWu7tBsczD0jWpP2vxFM54Wdwqb3KPi1G" rel="nofollow" style="color:#0f7b3f;text-decoration:underline">Team? One key for every ReadyStack linter, 5 seats, $149 once</a></p></section>
|
| 455 |
+
<section data-yj-alert="rule" style="margin:1rem auto 1.5rem;max-width:46rem;padding:1.1rem 1.25rem;border:1px solid #d9dee6;border-radius:12px;background:#fbfcfd;font:15px/1.55 system-ui,-apple-system,Segoe UI,sans-serif"><b style="font-size:1.05rem">Get one email when this rule changes</b><div style="opacity:.78;margin:.35rem 0 .8rem">We watch the regulation and vendor sources behind Postgres Migration Lock Lint every day. When a rule changes, you get a single email with what changed and the updated check. No newsletter.</div><div style="display:flex;gap:8px;flex-wrap:wrap"><input id="yjae" type="email" placeholder="you@example.com" style="flex:1;min-width:210px;padding:11px 13px;border:1px solid rgba(140,150,165,.34);border-radius:8px;background:#fff;color:inherit"><button id="yjab" style="padding:11px 18px;border-radius:8px;border:0;cursor:pointer;background:#0f7b3f;color:#fff;font:600 15px system-ui,sans-serif">Notify me</button></div><div id="yjao" hidden style="margin-top:.6rem;color:#0f7b3f">You're on the list. One email when it changes.</div><script>(function(){var b=document.getElementById("yjab"),e=document.getElementById("yjae");if(!b||!e)return;b.addEventListener("click",function(){var v=e.value.trim();if(!/.+@.+\..+/.test(v))return;try{localStorage.setItem("yj_em","1");}catch(_){}fetch(location.origin+"/api/subscribe",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({email:v,slug:"pg-migration-lock-lint",lang:"en",source:"rule_alert"})}).then(function(r){var o=document.getElementById("yjao");o.hidden=false;if(!r.ok){o.textContent="Could not save - please try again.";}}).catch(function(){var o=document.getElementById("yjao");o.hidden=false;o.textContent="Could not save - please try again.";});});})();</script></section>
|
| 456 |
+
|
| 457 |
+
<section data-yj-cta="paid" style="margin:3rem auto;max-width:46rem;padding:1.5rem 1.25rem;border:1px solid #d8dee9;border-radius:12px;background:#fbfcfe;font:16px/1.6 system-ui,-apple-system,Segoe UI,sans-serif"><h2 style="margin:0 0 .5rem;font-size:1.25rem">Get the complete version <span class="yj-price">$29</span></h2><p style="margin:.25rem 0 1rem;color:#41505f">This page is the working piece. The full pack has everything below.</p><p style="margin:0 0 1rem;color:#41505f">Names every statement in a Rails, Django or raw SQL migration that takes a table-blocking Postgres lock, and prints the non-blocking rewrite. 36 rules, PostgreSQL 12-17.</p><p style="margin:0 0 1rem;color:#5b6b7c;font-size:.94rem">Independent Postgres consultancies bill migration and lock review at $150/hour and up; this is one review, priced once.</p><a href="https://buy.polar.sh/polar_cl_zaJ9pwhlczNQZUXiIKP3wGCzfzxe2LYQ6mMD02xD1j5" rel="nofollow" style="display:inline-block;padding:.7rem 1.3rem;border-radius:8px;background:#1b64f2;color:#fff;text-decoration:none;font-weight:600">Buy the full version — $29</a></section>
|
| 458 |
+
<section data-yj="yj-facts-v1" style="max-width:920px;margin:48px auto 0;padding:0 20px;font-family:system-ui,sans-serif"><style>.yj-article p{font:400 15.5px/1.7 system-ui,sans-serif;color:#26313b;margin:0 0 12px;max-width:72ch}.yj-article h2,.yj-article h3,.yj-article h4{font:600 17px/1.35 system-ui,sans-serif;color:#1b1f24;margin:22px 0 8px}.yj-article table{border-collapse:collapse;margin:10px 0 16px;font:14px/1.45 system-ui,sans-serif}.yj-article th,.yj-article td{border:1px solid #e2e6ea;padding:6px 10px;text-align:left}.yj-article th{background:#f4f6f8}.yj-article pre{background:#0f1720;color:#e6edf3;padding:12px 14px;border-radius:8px;overflow:auto;font:13px/1.5 ui-monospace,Menlo,monospace}.yj-article code{font:13px ui-monospace,Menlo,monospace;background:#f1f3f5;padding:1px 4px;border-radius:3px}.yj-article pre code{background:none;padding:0}.yj-article ul,.yj-article ol{font:15px/1.6 system-ui,sans-serif;color:#26313b}</style><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:34px 0 6px;color:#1b1f24">Questions people ask</h2><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What does Postgres Migration Lock Lint actually do?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">It reads a migration file and names every statement that takes a lock Postgres will not share, such as ACCESS EXCLUSIVE on a table rewrite or SHARE on a plain CREATE INDEX. 36 rules, PostgreSQL 12-17. On the bundled 20-statement sample migration it returns 22 findings, 16 of them at error severity; the hand-split version of the same schema change returns 0 findings.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">Who is this for?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Rails and Django engineers who deploy to a Postgres database that has real traffic, and anyone reviewing raw .sql migrations written by an assistant. It is aimed at the person who runs the deploy and gets paged when a table stops answering, not at a greenfield project where every table is empty.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">Why do free tools not cover this?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">strong_migrations reads only the Rails DSL, squawk reads only raw SQL, and both run in CI after the migration is already in a pull request. Neither reads a Django operations list. This runs on the file in your editor, across all three formats, before the branch exists, and it knows which forms became safe in PostgreSQL 11 and 12.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What is free and what needs the licence key?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Free: the migration file you have open, checked against all 36 rules, with the full message and the safe rewrite. No key, no limit on how often. The full version adds scope and ownership: every migration in the workspace in one pass, the review exported as Markdown or JSON for the change ticket, and an exit code for CI.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What does the alternative cost?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Independent Postgres consultancies bill migration and lock review at $150 an hour and up, and an incident review costs more than that once on-call time is counted. This is $29 once, with a 7-day full refund. The free tier covers the file you are editing right now at no cost at all.</p></details><script type="application/ld+json" data-yj="yj-faq-ld">{"@context": "https://schema.org", "@type": "FAQPage", "mainEntity": [{"@type": "Question", "name": "What does Postgres Migration Lock Lint actually do?", "acceptedAnswer": {"@type": "Answer", "text": "It reads a migration file and names every statement that takes a lock Postgres will not share, such as ACCESS EXCLUSIVE on a table rewrite or SHARE on a plain CREATE INDEX. 36 rules, PostgreSQL 12-17. On the bundled 20-statement sample migration it returns 22 findings, 16 of them at error severity; the hand-split version of the same schema change returns 0 findings."}}, {"@type": "Question", "name": "Who is this for?", "acceptedAnswer": {"@type": "Answer", "text": "Rails and Django engineers who deploy to a Postgres database that has real traffic, and anyone reviewing raw .sql migrations written by an assistant. It is aimed at the person who runs the deploy and gets paged when a table stops answering, not at a greenfield project where every table is empty."}}, {"@type": "Question", "name": "Why do free tools not cover this?", "acceptedAnswer": {"@type": "Answer", "text": "strong_migrations reads only the Rails DSL, squawk reads only raw SQL, and both run in CI after the migration is already in a pull request. Neither reads a Django operations list. This runs on the file in your editor, across all three formats, before the branch exists, and it knows which forms became safe in PostgreSQL 11 and 12."}}, {"@type": "Question", "name": "What is free and what needs the licence key?", "acceptedAnswer": {"@type": "Answer", "text": "Free: the migration file you have open, checked against all 36 rules, with the full message and the safe rewrite. No key, no limit on how often. The full version adds scope and ownership: every migration in the workspace in one pass, the review exported as Markdown or JSON for the change ticket, and an exit code for CI."}}, {"@type": "Question", "name": "What does the alternative cost?", "acceptedAnswer": {"@type": "Answer", "text": "Independent Postgres consultancies bill migration and lock review at $150 an hour and up, and an incident review costs more than that once on-call time is counted. This is $29 once, with a 7-day full refund. The free tier covers the file you are editing right now at no cost at all."}}]}</script><div id="yj-ask" style="margin:36px 0 0;padding:18px 18px 16px;border:1px solid #e2e6ea;border-radius:10px;background:#fbfbfc"><h2 style="font:600 19px/1.3 system-ui,sans-serif;margin:0 0 4px;color:#1b1f24">Ask about this tool</h2><p style="font:400 13.5px/1.5 system-ui,sans-serif;margin:0 0 12px;color:#5b6570">One question, answered by the person who built it. Your email only if you want the answer sent.</p><form onsubmit="return yjAsk(event)" style="display:grid;gap:8px"><textarea name="question" required minlength="10" maxlength="2000" rows="3" placeholder="Your question" style="font:14px/1.5 system-ui,sans-serif;padding:10px;border:1px solid #cfd6dd;border-radius:6px"></textarea><div style="display:flex;gap:8px;flex-wrap:wrap"><input type="email" name="email" placeholder="Email (optional)" style="flex:1;min-width:200px;font:14px system-ui,sans-serif;padding:9px 10px;border:1px solid #cfd6dd;border-radius:6px"><button type="submit" style="font:600 14px system-ui,sans-serif;padding:9px 18px;border:0;border-radius:6px;background:#1b1f24;color:#fff;cursor:pointer">Send</button></div><p id="yj-ask-msg" style="font:13px system-ui,sans-serif;margin:2px 0 0;color:#0a6238;min-height:1em"></p></form><script>function yjAsk(e){e.preventDefault();var f=e.target,m=document.getElementById("yj-ask-msg");m.style.color="#0a6238";fetch(location.origin+"/api/ask",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({sku:0,slug:"pg-migration-lock-lint",lang:"en",question:f.question.value,email:f.email.value,page:location.pathname})}).then(function(r){if(!r.ok)throw 0;m.textContent="Thanks \u2014 noted. Answers land on this page and, if you left an email, in your inbox.";f.question.value="";}).catch(function(){m.style.color="#a8551f";m.textContent="Could not send. Please try again in a minute.";});return false;}</script></div></section><section class="yj-related" style="max-width:920px;margin:56px auto 40px;padding:0 20px"><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:0 0 6px;color:#1b1f24">Same job, next tool</h2><p style="font:400 14.5px/1.5 system-ui,sans-serif;margin:0 0 20px;color:#5b6570">Free in the browser. The full version is a one-time licence, 7-day refund.</p><div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:14px"><a class="yj-rel-c" href="https://getreadystack.com/tools/robots-txt-ai-crawler-audit"><span class="yj-rel-t">AI Crawler Rules - robots.txt Audit for AI Search</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/bulk-sender-lint"><span class="yj-rel-t">Bulk Sender Lint - the Gmail, Yahoo and Outlook.com rules, checked in your mai</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/devicetree-config-language-pack"><span class="yj-rel-t">DeviceTree Config Audit: 23 dtc-Style Rules, 50 Snippets</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/en301549-lint"><span class="yj-rel-t">EN 301 549 Lint — European Accessibility Act rules, with the clause number, in</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/erb-escape-audit"><span class="yj-rel-t">ERB Escape Audit - Rails XSS lint</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/firestore-rules-guard"><span class="yj-rel-t">Firestore Rules Guard</span><span class="yj-rel-p">$29</span></a></div><style>.yj-rel-c{display:flex;flex-direction:column;gap:8px;padding:15px 16px;border:1px solid #e2e6ea;border-radius:6px;text-decoration:none;background:#fff}.yj-rel-c:hover{border-color:#b9c2cb;background:#fafbfc}.yj-rel-t{font:500 14.5px/1.42 system-ui,sans-serif;color:#1b1f24}.yj-rel-p{font:600 13px system-ui,sans-serif;color:#0a6238}</style></section><!--yj-share-s101--><div style="margin:28px 0 8px;text-align:center"><button id="yjcp" type="button" style="font:inherit;padding:10px 18px;border:1px solid #8884;border-radius:10px;background:transparent;cursor:pointer">Copy link</button><button id="yjimg" type="button" style="font:inherit;padding:10px 18px;margin-left:8px;border:1px solid #8884;border-radius:10px;background:transparent;cursor:pointer">Save result as image</button></div><script>(function(){var b=document.getElementById("yjimg");if(!b)return;b.addEventListener("click",function(){var o=document.getElementById("out");var txt=(o&&(o.innerText||o.textContent)||"").trim();if(!txt){b.textContent="(no result yet)";setTimeout(function(){b.textContent="Save result as image";},1500);return;}var lines=txt.split(String.fromCharCode(10)).filter(function(x){return x.trim();}).slice(0,10);var W=1200,P=56,c=document.createElement("canvas");c.width=W;var lh=38,H=P*2+70+lines.length*lh+60;c.height=H;var x=c.getContext("2d");x.fillStyle="#1a1512";x.fillRect(0,0,W,H);x.fillStyle="#f3e9d2";x.font="600 34px system-ui,Segoe UI,Arial";var t=(document.title||"").split("|")[0].trim();x.fillText(t.slice(0,54),P,P+34);x.font="22px ui-monospace,Menlo,Consolas,monospace";x.fillStyle="#e8dcc4";for(var i=0;i<lines.length;i++){x.fillText(lines[i].slice(0,88),P,P+70+i*lh+22);}x.font="600 22px system-ui,Segoe UI,Arial";x.fillStyle="#c9a227";var d=location.hostname.replace("www.","");x.fillText(d,W-P-x.measureText(d).width,H-P+6);c.toBlob(function(bl){var u=URL.createObjectURL(bl),a=document.createElement("a");a.href=u;a.download=(t||"result").replace(/[^a-z0-9]+/gi,"-").toLowerCase()+"-"+d+".png";document.body.appendChild(a);a.click();setTimeout(function(){URL.revokeObjectURL(u);a.remove();},2000);},"image/png");});})();</script><script>(function(){var b=document.getElementById("yjcp");if(!b)return;var t=b.textContent;b.addEventListener("click",function(){var u=location.href.split("#")[0];function d(){b.textContent="Link copied";setTimeout(function(){b.textContent=t;},1800);}if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(u).then(d,function(){prompt("",u);});}else{prompt("",u);}});})();</script><div class="yj-home-find"><div class="yj-lgbar"><a href="/" class="yj-lg on">EN</a><a href="/de/" class="yj-lg">DE</a><a href="/ja/" class="yj-lg">JA</a><a href="/es/" class="yj-lg">ES</a><a href="/pt/" class="yj-lg">PT</a></div><section class="yj-find"><h2>Find a tool</h2><input id="yjq" type="search" autocomplete="off" placeholder="Type a product name, format or task"><div id="yjr"></div></section><style>.yj-lgbar{position:absolute;top:14px;right:18px;display:flex;gap:6px;z-index:60}.yj-lg{font:600 12px/1 system-ui,sans-serif;padding:6px 9px;border-radius:4px;text-decoration:none;color:#8b96a3;border:1px solid rgba(140,150,165,.28)}.yj-lg:hover{color:#e8eaed;border-color:rgba(160,175,195,.6)}.yj-lg.on{color:#0e1116;background:#cfd6de;border-color:#cfd6de}.yj-find{max-width:760px;margin:34px auto 8px;padding:0 20px}.yj-find h2{font:600 20px/1.3 system-ui,sans-serif;margin:0 0 10px}#yjq{width:100%;box-sizing:border-box;padding:13px 15px;font:400 15.5px system-ui,sans-serif;border:1px solid rgba(140,150,165,.34);border-radius:8px;background:rgba(255,255,255,.04);color:inherit}#yjq:focus{outline:none;border-color:#7FD1C4}#yjr{margin-top:10px;display:flex;flex-direction:column;gap:6px}#yjr a{display:flex;justify-content:space-between;gap:12px;padding:11px 14px;border:1px solid rgba(140,150,165,.22);border-radius:6px;text-decoration:none;font:400 14.5px/1.4 system-ui,sans-serif;color:inherit}#yjr a:hover{border-color:#7FD1C4}#yjr .pr{font:600 13px system-ui,sans-serif;color:#5fbf9a;white-space:nowrap}</style><script>(function(){var q=document.getElementById("yjq"),r=document.getElementById("yjr"),D=null;function load(){if(D)return Promise.resolve(D);return fetch("/search-index.json").then(function(x){return x.json();}).then(function(j){D=j;return j;}).catch(function(){D=[];return D;});}function esc(t){return String(t).replace(/[&<>"]/g,function(c){return {"&":"&","<":"<",">":">",'"':"""}[c];});}function run(){var v=q.value.trim().toLowerCase();if(v.length<2){r.innerHTML="";return;}load().then(function(j){var w=v.split(/\s+/),o=[];for(var i=0;i<j.length&&o.length<12;i++){var t=j[i].t.toLowerCase();var ok=true;for(var k=0;k<w.length;k++){if(t.indexOf(w[k])<0){ok=false;break;}}if(ok)o.push(j[i]);}r.innerHTML=o.length?o.map(function(x){return "<a href=\""+esc(x.u)+"\">"+"<span>"+esc(x.t)+"</span>"+(x.p?"<span class=\"pr\">$"+x.p+"</span>":"")+"</a>";}).join(""):"";});}q.addEventListener("input",run);q.addEventListener("focus",load);})();</script></div>
|
| 459 |
+
<script data-yj="yj-view-beacon">(function(){try{var p=location.pathname;var k='yjv:'+p;if(sessionStorage.getItem(k))return;sessionStorage.setItem(k,'1');fetch('/api/ev',{method:'POST',keepalive:true,headers:{'Content-Type':'application/json'},body:JSON.stringify({path:p,lang:"en",ref:(document.referrer||'')})}).catch(function(){});}catch(e){}})();</script>
|
| 460 |
+
|
| 461 |
+
<script data-yj="yj-use-beacon">(function(){try{var o=document.getElementById('out');if(!o)return;var p=location.pathname,k='yju:'+p,sent=false;function fire(){if(sent||sessionStorage.getItem(k))return;if(!(o.textContent||'').replace(/\s+/g,'').length)return;sent=true;sessionStorage.setItem(k,'1');fetch('/api/ev',{method:'POST',keepalive:true,headers:{'Content-Type':'application/json'},body:JSON.stringify({path:p,lang:"en",t:'use'})}).catch(function(){});}var mo=new MutationObserver(function(){fire();if(sent)mo.disconnect();});mo.observe(o,{childList:true,characterData:true,subtree:true});}catch(e){}})();</script>
|
| 462 |
+
</body>
|
| 463 |
</html>
|