ddlforge 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +326 -0
- package/dist/bin/ddlforge.d.ts +6 -0
- package/dist/bin/ddlforge.d.ts.map +1 -0
- package/dist/bin/ddlforge.js +14 -0
- package/dist/bin/ddlforge.js.map +1 -0
- package/dist/src/cli.d.ts +21 -0
- package/dist/src/cli.d.ts.map +1 -0
- package/dist/src/cli.js +262 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/engine/analyzer.d.ts +30 -0
- package/dist/src/engine/analyzer.d.ts.map +1 -0
- package/dist/src/engine/analyzer.js +76 -0
- package/dist/src/engine/analyzer.js.map +1 -0
- package/dist/src/engine/locks.d.ts +26 -0
- package/dist/src/engine/locks.d.ts.map +1 -0
- package/dist/src/engine/locks.js +136 -0
- package/dist/src/engine/locks.js.map +1 -0
- package/dist/src/index.d.ts +12 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +17 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/lexer/sqlTokenizer.d.ts +40 -0
- package/dist/src/lexer/sqlTokenizer.d.ts.map +1 -0
- package/dist/src/lexer/sqlTokenizer.js +494 -0
- package/dist/src/lexer/sqlTokenizer.js.map +1 -0
- package/dist/src/lexer/tokens.d.ts +35 -0
- package/dist/src/lexer/tokens.d.ts.map +1 -0
- package/dist/src/lexer/tokens.js +81 -0
- package/dist/src/lexer/tokens.js.map +1 -0
- package/dist/src/reporters/index.d.ts +7 -0
- package/dist/src/reporters/index.d.ts.map +1 -0
- package/dist/src/reporters/index.js +7 -0
- package/dist/src/reporters/index.js.map +1 -0
- package/dist/src/reporters/json.d.ts +20 -0
- package/dist/src/reporters/json.d.ts.map +1 -0
- package/dist/src/reporters/json.js +35 -0
- package/dist/src/reporters/json.js.map +1 -0
- package/dist/src/reporters/markdown.d.ts +6 -0
- package/dist/src/reporters/markdown.d.ts.map +1 -0
- package/dist/src/reporters/markdown.js +70 -0
- package/dist/src/reporters/markdown.js.map +1 -0
- package/dist/src/reporters/terminal.d.ts +10 -0
- package/dist/src/reporters/terminal.d.ts.map +1 -0
- package/dist/src/reporters/terminal.js +109 -0
- package/dist/src/reporters/terminal.js.map +1 -0
- package/dist/src/rules/addColumnNotNull.d.ts +9 -0
- package/dist/src/rules/addColumnNotNull.d.ts.map +1 -0
- package/dist/src/rules/addColumnNotNull.js +131 -0
- package/dist/src/rules/addColumnNotNull.js.map +1 -0
- package/dist/src/rules/foreignKeyNotValid.d.ts +9 -0
- package/dist/src/rules/foreignKeyNotValid.d.ts.map +1 -0
- package/dist/src/rules/foreignKeyNotValid.js +119 -0
- package/dist/src/rules/foreignKeyNotValid.js.map +1 -0
- package/dist/src/rules/index.d.ts +14 -0
- package/dist/src/rules/index.d.ts.map +1 -0
- package/dist/src/rules/index.js +28 -0
- package/dist/src/rules/index.js.map +1 -0
- package/dist/src/rules/indexConcurrently.d.ts +9 -0
- package/dist/src/rules/indexConcurrently.d.ts.map +1 -0
- package/dist/src/rules/indexConcurrently.js +92 -0
- package/dist/src/rules/indexConcurrently.js.map +1 -0
- package/dist/src/rules/prismaRenameDropAdd.d.ts +11 -0
- package/dist/src/rules/prismaRenameDropAdd.d.ts.map +1 -0
- package/dist/src/rules/prismaRenameDropAdd.js +144 -0
- package/dist/src/rules/prismaRenameDropAdd.js.map +1 -0
- package/dist/src/rules/setNotNullFullScan.d.ts +9 -0
- package/dist/src/rules/setNotNullFullScan.d.ts.map +1 -0
- package/dist/src/rules/setNotNullFullScan.js +93 -0
- package/dist/src/rules/setNotNullFullScan.js.map +1 -0
- package/dist/src/rules/transactionTrap.d.ts +9 -0
- package/dist/src/rules/transactionTrap.d.ts.map +1 -0
- package/dist/src/rules/transactionTrap.js +104 -0
- package/dist/src/rules/transactionTrap.js.map +1 -0
- package/dist/src/rules/types.d.ts +36 -0
- package/dist/src/rules/types.d.ts.map +1 -0
- package/dist/src/rules/types.js +5 -0
- package/dist/src/rules/types.js.map +1 -0
- package/dist/src/rules/unbatchedBackfill.d.ts +9 -0
- package/dist/src/rules/unbatchedBackfill.d.ts.map +1 -0
- package/dist/src/rules/unbatchedBackfill.js +58 -0
- package/dist/src/rules/unbatchedBackfill.js.map +1 -0
- package/package.json +44 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ddlforge Rule: Unbatched DML in Migration (Bare UPDATE/DELETE)
|
|
3
|
+
*
|
|
4
|
+
* Severity: WARNING
|
|
5
|
+
* Lock: ROW EXCLUSIVE
|
|
6
|
+
*/
|
|
7
|
+
import { PostgresLockLevel } from '../engine/locks.js';
|
|
8
|
+
export const unbatchedBackfillRule = {
|
|
9
|
+
id: 'unbatched-dml',
|
|
10
|
+
name: 'Unbatched DML in Migration',
|
|
11
|
+
description: 'Bare UPDATE and DELETE statements in migrations can lock millions of rows, bloat WAL, and block autovacuum.',
|
|
12
|
+
defaultSeverity: 'WARNING',
|
|
13
|
+
lockLevel: PostgresLockLevel.ROW_EXCLUSIVE,
|
|
14
|
+
check(context) {
|
|
15
|
+
const findings = [];
|
|
16
|
+
for (const stmt of context.statements) {
|
|
17
|
+
if (stmt.hasIgnore(this.id))
|
|
18
|
+
continue;
|
|
19
|
+
const tokens = stmt.tokens;
|
|
20
|
+
if (tokens.length < 2)
|
|
21
|
+
continue;
|
|
22
|
+
const firstVal = tokens[0].value;
|
|
23
|
+
const isUpdate = firstVal === 'UPDATE';
|
|
24
|
+
const isDelete = firstVal === 'DELETE' && tokens[1]?.value === 'FROM';
|
|
25
|
+
if (!isUpdate && !isDelete)
|
|
26
|
+
continue;
|
|
27
|
+
// Check for batching comments: -- batch, -- batched, -- batch-size
|
|
28
|
+
const hasBatchComment = stmt.comments.some(c => /batch/i.test(c));
|
|
29
|
+
if (hasBatchComment)
|
|
30
|
+
continue;
|
|
31
|
+
// Check if statement contains LIMIT inside a subquery
|
|
32
|
+
const hasLimit = tokens.some(t => t.value === 'LIMIT');
|
|
33
|
+
if (hasLimit)
|
|
34
|
+
continue;
|
|
35
|
+
// Check if WHERE clause exists
|
|
36
|
+
const hasWhere = tokens.some(t => t.value === 'WHERE');
|
|
37
|
+
const opName = isUpdate ? 'UPDATE' : 'DELETE';
|
|
38
|
+
const tableName = isUpdate ? (tokens[1]?.raw ?? 'table') : (tokens[2]?.raw ?? 'table');
|
|
39
|
+
findings.push({
|
|
40
|
+
ruleId: this.id,
|
|
41
|
+
ruleName: this.name,
|
|
42
|
+
severity: !hasWhere ? 'BLOCKER' : this.defaultSeverity,
|
|
43
|
+
lockLevel: this.lockLevel,
|
|
44
|
+
message: `Bare ${opName} statement on table "${tableName}" without batching or LIMIT.`,
|
|
45
|
+
detail: `Unbatched ${opName} statements in migrations hold ROW EXCLUSIVE locks on affected rows for the duration of the migration transaction, ` +
|
|
46
|
+
'generating massive WAL volume and risking transaction timeouts.',
|
|
47
|
+
suggestion: 'Migrate large backfills out-of-band using asynchronous background jobs in small batches (1,000–5,000 rows). ' +
|
|
48
|
+
'If this operation targets a small static/lookup table, add `-- ddlforge-ignore unbatched-dml`.',
|
|
49
|
+
file: context.filePath,
|
|
50
|
+
line: stmt.startLine,
|
|
51
|
+
column: stmt.startColumn,
|
|
52
|
+
codeSnippet: stmt.raw,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
return findings;
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
//# sourceMappingURL=unbatchedBackfill.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"unbatchedBackfill.js","sourceRoot":"","sources":["../../../src/rules/unbatchedBackfill.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAEvD,MAAM,CAAC,MAAM,qBAAqB,GAAS;IACzC,EAAE,EAAE,eAAe;IACnB,IAAI,EAAE,4BAA4B;IAClC,WAAW,EAAE,6GAA6G;IAC1H,eAAe,EAAE,SAAS;IAC1B,SAAS,EAAE,iBAAiB,CAAC,aAAa;IAE1C,KAAK,CAAC,OAAoB;QACxB,MAAM,QAAQ,GAAc,EAAE,CAAC;QAE/B,KAAK,MAAM,IAAI,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC;YACtC,IAAI,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC;gBAAE,SAAS;YAEtC,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;YAC3B,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC;gBAAE,SAAS;YAEhC,MAAM,QAAQ,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;YACjC,MAAM,QAAQ,GAAG,QAAQ,KAAK,QAAQ,CAAC;YACvC,MAAM,QAAQ,GAAG,QAAQ,KAAK,QAAQ,IAAI,MAAM,CAAC,CAAC,CAAC,EAAE,KAAK,KAAK,MAAM,CAAC;YAEtE,IAAI,CAAC,QAAQ,IAAI,CAAC,QAAQ;gBAAE,SAAS;YAErC,mEAAmE;YACnE,MAAM,eAAe,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;YAClE,IAAI,eAAe;gBAAE,SAAS;YAE9B,sDAAsD;YACtD,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,OAAO,CAAC,CAAC;YACvD,IAAI,QAAQ;gBAAE,SAAS;YAEvB,+BAA+B;YAC/B,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,OAAO,CAAC,CAAC;YACvD,MAAM,MAAM,GAAG,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC;YAE9C,MAAM,SAAS,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,GAAG,IAAI,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,GAAG,IAAI,OAAO,CAAC,CAAC;YAEvF,QAAQ,CAAC,IAAI,CAAC;gBACZ,MAAM,EAAE,IAAI,CAAC,EAAE;gBACf,QAAQ,EAAE,IAAI,CAAC,IAAI;gBACnB,QAAQ,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,eAAe;gBACtD,SAAS,EAAE,IAAI,CAAC,SAAS;gBACzB,OAAO,EAAE,QAAQ,MAAM,wBAAwB,SAAS,8BAA8B;gBACtF,MAAM,EACJ,aAAa,MAAM,qHAAqH;oBACxI,iEAAiE;gBACnE,UAAU,EACR,8GAA8G;oBAC9G,gGAAgG;gBAClG,IAAI,EAAE,OAAO,CAAC,QAAQ;gBACtB,IAAI,EAAE,IAAI,CAAC,SAAS;gBACpB,MAAM,EAAE,IAAI,CAAC,WAAW;gBACxB,WAAW,EAAE,IAAI,CAAC,GAAG;aACtB,CAAC,CAAC;QACL,CAAC;QAED,OAAO,QAAQ,CAAC;IAClB,CAAC;CACF,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ddlforge",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Ultra-fast, zero-runtime-dependency Postgres migration lock linter and data-loss prevention engine CLI in Node.js 20+ and TypeScript",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/src/index.js",
|
|
7
|
+
"types": "./dist/src/index.d.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"ddlforge": "dist/bin/ddlforge.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsc && node -e \"const fs=require('fs'); try{fs.chmodSync('dist/bin/ddlforge.js', 0o755)}catch{}\"",
|
|
18
|
+
"test:build": "tsc -p tsconfig.test.json",
|
|
19
|
+
"test": "npm run test:build && node --test dist/test/analyzer.test.js",
|
|
20
|
+
"test:watch": "node --test --watch dist/test/analyzer.test.js",
|
|
21
|
+
"prepack": "node -e \"const fs=require('fs'); try{fs.rmSync('dist/test', {recursive:true, force:true})}catch{}\"",
|
|
22
|
+
"prepublishOnly": "npm run build && node -e \"const fs=require('fs'); try{fs.rmSync('dist/test', {recursive:true, force:true})}catch{}\""
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"postgres",
|
|
26
|
+
"postgresql",
|
|
27
|
+
"migration",
|
|
28
|
+
"ddl",
|
|
29
|
+
"lock",
|
|
30
|
+
"linter",
|
|
31
|
+
"prisma",
|
|
32
|
+
"drizzle",
|
|
33
|
+
"zero-downtime"
|
|
34
|
+
],
|
|
35
|
+
"author": "",
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=20.0.0"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/node": "^22.13.10",
|
|
42
|
+
"typescript": "^5.8.2"
|
|
43
|
+
}
|
|
44
|
+
}
|