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,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ddlforge Rule: CREATE INDEX missing CONCURRENTLY
|
|
3
|
+
*
|
|
4
|
+
* Severity: BLOCKER
|
|
5
|
+
* Lock: SHARE (blocks all INSERT, UPDATE, DELETE)
|
|
6
|
+
*/
|
|
7
|
+
import { Rule } from './types.js';
|
|
8
|
+
export declare const indexConcurrentlyRule: Rule;
|
|
9
|
+
//# sourceMappingURL=indexConcurrently.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"indexConcurrently.d.ts","sourceRoot":"","sources":["../../../src/rules/indexConcurrently.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,IAAI,EAAwB,MAAM,YAAY,CAAC;AAIxD,eAAO,MAAM,qBAAqB,EAAE,IA2FnC,CAAC"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ddlforge Rule: CREATE INDEX missing CONCURRENTLY
|
|
3
|
+
*
|
|
4
|
+
* Severity: BLOCKER
|
|
5
|
+
* Lock: SHARE (blocks all INSERT, UPDATE, DELETE)
|
|
6
|
+
*/
|
|
7
|
+
import { PostgresLockLevel } from '../engine/locks.js';
|
|
8
|
+
import { TokenType } from '../lexer/tokens.js';
|
|
9
|
+
export const indexConcurrentlyRule = {
|
|
10
|
+
id: 'require-concurrent-index',
|
|
11
|
+
name: 'CREATE INDEX without CONCURRENTLY',
|
|
12
|
+
description: 'CREATE INDEX acquires a SHARE lock, blocking all concurrent INSERT, UPDATE, and DELETE operations.',
|
|
13
|
+
defaultSeverity: 'BLOCKER',
|
|
14
|
+
lockLevel: PostgresLockLevel.SHARE,
|
|
15
|
+
check(context) {
|
|
16
|
+
const findings = [];
|
|
17
|
+
for (const stmt of context.statements) {
|
|
18
|
+
if (stmt.hasIgnore(this.id))
|
|
19
|
+
continue;
|
|
20
|
+
const tokens = stmt.tokens;
|
|
21
|
+
if (tokens.length < 3)
|
|
22
|
+
continue;
|
|
23
|
+
let idx = 0;
|
|
24
|
+
if (tokens[idx].value !== 'CREATE')
|
|
25
|
+
continue;
|
|
26
|
+
idx++;
|
|
27
|
+
let isUnique = false;
|
|
28
|
+
if (tokens[idx]?.value === 'UNIQUE') {
|
|
29
|
+
isUnique = true;
|
|
30
|
+
idx++;
|
|
31
|
+
}
|
|
32
|
+
if (tokens[idx]?.value !== 'INDEX')
|
|
33
|
+
continue;
|
|
34
|
+
idx++;
|
|
35
|
+
// Check if CONCURRENTLY is present before the 'ON' keyword
|
|
36
|
+
let hasConcurrently = false;
|
|
37
|
+
let onIndex = -1;
|
|
38
|
+
let indexName = '';
|
|
39
|
+
for (let i = idx; i < tokens.length; i++) {
|
|
40
|
+
if (tokens[i].value === 'CONCURRENTLY') {
|
|
41
|
+
hasConcurrently = true;
|
|
42
|
+
}
|
|
43
|
+
if (tokens[i].value === 'ON') {
|
|
44
|
+
onIndex = i;
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
if (!hasConcurrently && tokens[i].value !== 'IF' && tokens[i].value !== 'NOT' && tokens[i].value !== 'EXISTS') {
|
|
48
|
+
if (!indexName && (tokens[i].type === TokenType.IDENTIFIER || tokens[i].type === TokenType.KEYWORD)) {
|
|
49
|
+
indexName = tokens[i].raw;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// If there is no 'ON', it's not a valid standard CREATE INDEX statement
|
|
54
|
+
if (onIndex === -1)
|
|
55
|
+
continue;
|
|
56
|
+
if (!hasConcurrently) {
|
|
57
|
+
// Find table name after ON
|
|
58
|
+
let tableName = '';
|
|
59
|
+
if (onIndex + 1 < tokens.length) {
|
|
60
|
+
const tableToken = tokens[onIndex + 1];
|
|
61
|
+
if (tableToken.value === 'ONLY' && onIndex + 2 < tokens.length) {
|
|
62
|
+
tableName = tokens[onIndex + 2].raw;
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
tableName = tableToken.raw;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const prefix = isUnique ? 'CREATE UNIQUE INDEX CONCURRENTLY' : 'CREATE INDEX CONCURRENTLY';
|
|
69
|
+
const restOfStatement = stmt.raw
|
|
70
|
+
.replace(/^CREATE\s+(UNIQUE\s+)?INDEX\s+/i, '')
|
|
71
|
+
.trim();
|
|
72
|
+
const suggestedFix = `${prefix} ${restOfStatement};`;
|
|
73
|
+
findings.push({
|
|
74
|
+
ruleId: this.id,
|
|
75
|
+
ruleName: this.name,
|
|
76
|
+
severity: this.defaultSeverity,
|
|
77
|
+
lockLevel: this.lockLevel,
|
|
78
|
+
message: `CREATE ${isUnique ? 'UNIQUE ' : ''}INDEX missing CONCURRENTLY keyword.`,
|
|
79
|
+
detail: 'Building an index without CONCURRENTLY acquires a SHARE lock on the target table. ' +
|
|
80
|
+
'This blocks all concurrent write operations (INSERT, UPDATE, DELETE) for the entire duration of the build.',
|
|
81
|
+
suggestion: `Add the CONCURRENTLY modifier: \`${suggestedFix}\` (Note: Run outside of an explicit transaction).`,
|
|
82
|
+
file: context.filePath,
|
|
83
|
+
line: stmt.startLine,
|
|
84
|
+
column: stmt.startColumn,
|
|
85
|
+
codeSnippet: stmt.raw,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return findings;
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
//# sourceMappingURL=indexConcurrently.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"indexConcurrently.js","sourceRoot":"","sources":["../../../src/rules/indexConcurrently.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ddlforge Rule: Prisma Silent Field Rename (Destructive Data Loss)
|
|
3
|
+
*
|
|
4
|
+
* Detects paired DROP COLUMN + ADD COLUMN on the same table, which Prisma
|
|
5
|
+
* generates for renamed fields, causing irreversible production data wipe.
|
|
6
|
+
*
|
|
7
|
+
* Severity: BLOCKER
|
|
8
|
+
*/
|
|
9
|
+
import { Rule } from './types.js';
|
|
10
|
+
export declare const prismaRenameDropAddRule: Rule;
|
|
11
|
+
//# sourceMappingURL=prismaRenameDropAdd.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prismaRenameDropAdd.d.ts","sourceRoot":"","sources":["../../../src/rules/prismaRenameDropAdd.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,IAAI,EAAwB,MAAM,YAAY,CAAC;AA0ExD,eAAO,MAAM,uBAAuB,EAAE,IAuFrC,CAAC"}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ddlforge Rule: Prisma Silent Field Rename (Destructive Data Loss)
|
|
3
|
+
*
|
|
4
|
+
* Detects paired DROP COLUMN + ADD COLUMN on the same table, which Prisma
|
|
5
|
+
* generates for renamed fields, causing irreversible production data wipe.
|
|
6
|
+
*
|
|
7
|
+
* Severity: BLOCKER
|
|
8
|
+
*/
|
|
9
|
+
import { PostgresLockLevel } from '../engine/locks.js';
|
|
10
|
+
function parseAlterSubcommands(tokens) {
|
|
11
|
+
const results = [];
|
|
12
|
+
let parenDepth = 0;
|
|
13
|
+
let i = 0;
|
|
14
|
+
while (i < tokens.length) {
|
|
15
|
+
if (tokens[i].value === '(') {
|
|
16
|
+
parenDepth++;
|
|
17
|
+
i++;
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
if (tokens[i].value === ')') {
|
|
21
|
+
parenDepth--;
|
|
22
|
+
i++;
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
if (parenDepth === 0) {
|
|
26
|
+
// Match DROP [COLUMN] [IF EXISTS] <col>
|
|
27
|
+
if (tokens[i].value === 'DROP') {
|
|
28
|
+
let nextIdx = i + 1;
|
|
29
|
+
if (tokens[nextIdx]?.value === 'COLUMN')
|
|
30
|
+
nextIdx++;
|
|
31
|
+
if (tokens[nextIdx]?.value === 'IF' && tokens[nextIdx + 1]?.value === 'EXISTS')
|
|
32
|
+
nextIdx += 2;
|
|
33
|
+
// Make sure it's not DROP CONSTRAINT or DROP INDEX
|
|
34
|
+
if (tokens[i + 1]?.value !== 'CONSTRAINT' && tokens[i + 1]?.value !== 'INDEX') {
|
|
35
|
+
const colName = tokens[nextIdx]?.raw;
|
|
36
|
+
if (colName) {
|
|
37
|
+
results.push({ type: 'DROP_COLUMN', columnName: colName });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// Match ADD [COLUMN] [IF NOT EXISTS] <col>
|
|
42
|
+
if (tokens[i].value === 'ADD') {
|
|
43
|
+
let nextIdx = i + 1;
|
|
44
|
+
if (tokens[nextIdx]?.value === 'COLUMN')
|
|
45
|
+
nextIdx++;
|
|
46
|
+
if (tokens[nextIdx]?.value === 'IF' && tokens[nextIdx + 1]?.value === 'NOT' && tokens[nextIdx + 2]?.value === 'EXISTS')
|
|
47
|
+
nextIdx += 3;
|
|
48
|
+
if (tokens[i + 1]?.value !== 'CONSTRAINT' && tokens[nextIdx]?.value !== 'CONSTRAINT') {
|
|
49
|
+
const colName = tokens[nextIdx]?.raw;
|
|
50
|
+
if (colName) {
|
|
51
|
+
results.push({ type: 'ADD_COLUMN', columnName: colName });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
i++;
|
|
57
|
+
}
|
|
58
|
+
return results;
|
|
59
|
+
}
|
|
60
|
+
export const prismaRenameDropAddRule = {
|
|
61
|
+
id: 'prisma-silent-rename-data-loss',
|
|
62
|
+
name: 'Prisma Silent Field Rename (Destructive Data Loss)',
|
|
63
|
+
description: 'Detects paired DROP COLUMN and ADD COLUMN on the same table, preventing irreversible data destruction caused by Prisma field renames.',
|
|
64
|
+
defaultSeverity: 'BLOCKER',
|
|
65
|
+
lockLevel: PostgresLockLevel.ACCESS_EXCLUSIVE,
|
|
66
|
+
check(context) {
|
|
67
|
+
const findings = [];
|
|
68
|
+
const dropColumnsByTable = new Map();
|
|
69
|
+
const addColumnsByTable = new Map();
|
|
70
|
+
for (const stmt of context.statements) {
|
|
71
|
+
if (stmt.hasIgnore(this.id))
|
|
72
|
+
continue;
|
|
73
|
+
const tokens = stmt.tokens;
|
|
74
|
+
if (tokens.length < 5)
|
|
75
|
+
continue;
|
|
76
|
+
if (tokens[0].value !== 'ALTER' || tokens[1].value !== 'TABLE')
|
|
77
|
+
continue;
|
|
78
|
+
let idx = 2;
|
|
79
|
+
if (tokens[idx]?.value === 'ONLY')
|
|
80
|
+
idx++;
|
|
81
|
+
if (tokens[idx]?.value === 'IF' && tokens[idx + 1]?.value === 'EXISTS')
|
|
82
|
+
idx += 2;
|
|
83
|
+
const tableName = tokens[idx]?.raw ?? '';
|
|
84
|
+
if (!tableName)
|
|
85
|
+
continue;
|
|
86
|
+
const normalizedTable = tableName.replace(/^"|"$/g, '').toLowerCase();
|
|
87
|
+
idx++;
|
|
88
|
+
const subTokens = tokens.slice(idx);
|
|
89
|
+
const actions = parseAlterSubcommands(subTokens);
|
|
90
|
+
for (const action of actions) {
|
|
91
|
+
if (action.type === 'DROP_COLUMN') {
|
|
92
|
+
const list = dropColumnsByTable.get(normalizedTable) ?? [];
|
|
93
|
+
list.push({
|
|
94
|
+
tableName,
|
|
95
|
+
columnName: action.columnName,
|
|
96
|
+
line: stmt.startLine,
|
|
97
|
+
column: stmt.startColumn,
|
|
98
|
+
raw: stmt.raw,
|
|
99
|
+
});
|
|
100
|
+
dropColumnsByTable.set(normalizedTable, list);
|
|
101
|
+
}
|
|
102
|
+
else if (action.type === 'ADD_COLUMN') {
|
|
103
|
+
const list = addColumnsByTable.get(normalizedTable) ?? [];
|
|
104
|
+
list.push({
|
|
105
|
+
tableName,
|
|
106
|
+
columnName: action.columnName,
|
|
107
|
+
line: stmt.startLine,
|
|
108
|
+
column: stmt.startColumn,
|
|
109
|
+
raw: stmt.raw,
|
|
110
|
+
});
|
|
111
|
+
addColumnsByTable.set(normalizedTable, list);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// Correlate DROP and ADD on the same table
|
|
116
|
+
for (const [tableKey, dropList] of dropColumnsByTable.entries()) {
|
|
117
|
+
const addList = addColumnsByTable.get(tableKey);
|
|
118
|
+
if (!addList || addList.length === 0)
|
|
119
|
+
continue;
|
|
120
|
+
for (const drop of dropList) {
|
|
121
|
+
for (const add of addList) {
|
|
122
|
+
findings.push({
|
|
123
|
+
ruleId: this.id,
|
|
124
|
+
ruleName: this.name,
|
|
125
|
+
severity: this.defaultSeverity,
|
|
126
|
+
lockLevel: this.lockLevel,
|
|
127
|
+
message: `Destructive column replacement on "${drop.tableName}": dropped "${drop.columnName}" and added "${add.columnName}".`,
|
|
128
|
+
detail: `Detected DROP COLUMN "${drop.columnName}" paired with ADD COLUMN "${add.columnName}" on table "${drop.tableName}". ` +
|
|
129
|
+
'Prisma Migrate generates this pattern when a model field is renamed, which results in permanent, irreversible data destruction in production!',
|
|
130
|
+
suggestion: `If this is a rename, replace the DROP/ADD pair with a non-destructive rename:\n` +
|
|
131
|
+
` ALTER TABLE ${drop.tableName} RENAME COLUMN ${drop.columnName} TO ${add.columnName};\n` +
|
|
132
|
+
`Or use Prisma's @map attribute to map the Prisma field name without modifying the database column name.`,
|
|
133
|
+
file: context.filePath,
|
|
134
|
+
line: drop.line,
|
|
135
|
+
column: drop.column,
|
|
136
|
+
codeSnippet: `${drop.raw}\n-- paired with --\n${add.raw}`,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return findings;
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
//# sourceMappingURL=prismaRenameDropAdd.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prismaRenameDropAdd.js","sourceRoot":"","sources":["../../../src/rules/prismaRenameDropAdd.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ddlforge Rule: ALTER COLUMN SET NOT NULL Full Scan Lock
|
|
3
|
+
*
|
|
4
|
+
* Severity: WARNING (or BLOCKER for PG < 12)
|
|
5
|
+
* Lock: ACCESS EXCLUSIVE
|
|
6
|
+
*/
|
|
7
|
+
import { Rule } from './types.js';
|
|
8
|
+
export declare const setNotNullFullScanRule: Rule;
|
|
9
|
+
//# sourceMappingURL=setNotNullFullScan.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"setNotNullFullScan.d.ts","sourceRoot":"","sources":["../../../src/rules/setNotNullFullScan.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,IAAI,EAAkC,MAAM,YAAY,CAAC;AA2ClE,eAAO,MAAM,sBAAsB,EAAE,IAwDpC,CAAC"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ddlforge Rule: ALTER COLUMN SET NOT NULL Full Scan Lock
|
|
3
|
+
*
|
|
4
|
+
* Severity: WARNING (or BLOCKER for PG < 12)
|
|
5
|
+
* Lock: ACCESS EXCLUSIVE
|
|
6
|
+
*/
|
|
7
|
+
import { PostgresLockLevel } from '../engine/locks.js';
|
|
8
|
+
function extractSetNotNullClauses(tokens) {
|
|
9
|
+
const results = [];
|
|
10
|
+
let parenDepth = 0;
|
|
11
|
+
let i = 0;
|
|
12
|
+
while (i < tokens.length) {
|
|
13
|
+
if (tokens[i].value === '(') {
|
|
14
|
+
parenDepth++;
|
|
15
|
+
i++;
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
if (tokens[i].value === ')') {
|
|
19
|
+
parenDepth--;
|
|
20
|
+
i++;
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
if (parenDepth === 0 && tokens[i].value === 'ALTER') {
|
|
24
|
+
let currentIdx = i + 1;
|
|
25
|
+
if (tokens[currentIdx]?.value === 'COLUMN')
|
|
26
|
+
currentIdx++;
|
|
27
|
+
const columnName = tokens[currentIdx]?.raw;
|
|
28
|
+
currentIdx++;
|
|
29
|
+
if (tokens[currentIdx]?.value === 'SET' && tokens[currentIdx + 1]?.value === 'NOT' && tokens[currentIdx + 2]?.value === 'NULL') {
|
|
30
|
+
if (columnName) {
|
|
31
|
+
results.push({ columnName });
|
|
32
|
+
}
|
|
33
|
+
i = currentIdx + 3;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
i++;
|
|
38
|
+
}
|
|
39
|
+
return results;
|
|
40
|
+
}
|
|
41
|
+
export const setNotNullFullScanRule = {
|
|
42
|
+
id: 'set-not-null-full-scan',
|
|
43
|
+
name: 'ALTER COLUMN SET NOT NULL Full Scan Lock',
|
|
44
|
+
description: 'ALTER COLUMN SET NOT NULL scans the entire table under an ACCESS EXCLUSIVE lock to verify non-nullability.',
|
|
45
|
+
defaultSeverity: 'WARNING',
|
|
46
|
+
lockLevel: PostgresLockLevel.ACCESS_EXCLUSIVE,
|
|
47
|
+
check(context) {
|
|
48
|
+
const findings = [];
|
|
49
|
+
const severity = context.pgVersion < 12 ? 'BLOCKER' : this.defaultSeverity;
|
|
50
|
+
for (const stmt of context.statements) {
|
|
51
|
+
if (stmt.hasIgnore(this.id))
|
|
52
|
+
continue;
|
|
53
|
+
const tokens = stmt.tokens;
|
|
54
|
+
if (tokens.length < 6)
|
|
55
|
+
continue;
|
|
56
|
+
if (tokens[0].value !== 'ALTER' || tokens[1].value !== 'TABLE')
|
|
57
|
+
continue;
|
|
58
|
+
let idx = 2;
|
|
59
|
+
if (tokens[idx]?.value === 'ONLY')
|
|
60
|
+
idx++;
|
|
61
|
+
if (tokens[idx]?.value === 'IF' && tokens[idx + 1]?.value === 'EXISTS')
|
|
62
|
+
idx += 2;
|
|
63
|
+
const tableName = tokens[idx]?.raw ?? 'table';
|
|
64
|
+
idx++;
|
|
65
|
+
// Scan subcommands
|
|
66
|
+
const targets = extractSetNotNullClauses(tokens.slice(idx));
|
|
67
|
+
for (const target of targets) {
|
|
68
|
+
findings.push({
|
|
69
|
+
ruleId: this.id,
|
|
70
|
+
ruleName: this.name,
|
|
71
|
+
severity,
|
|
72
|
+
lockLevel: this.lockLevel,
|
|
73
|
+
message: `Direct ALTER COLUMN "${target.columnName}" SET NOT NULL on table "${tableName}".`,
|
|
74
|
+
detail: `Setting NOT NULL directly forces a full table scan while holding an ACCESS EXCLUSIVE lock. ` +
|
|
75
|
+
`This blocks all reads and writes to "${tableName}" for the duration of the scan.`,
|
|
76
|
+
suggestion: `Safe zero-downtime 3-step pattern:\n` +
|
|
77
|
+
` 1. Add check constraint NOT VALID:\n` +
|
|
78
|
+
` ALTER TABLE ${tableName} ADD CONSTRAINT chk_${target.columnName}_not_null CHECK (${target.columnName} IS NOT NULL) NOT VALID;\n` +
|
|
79
|
+
` 2. Validate constraint without blocking writes:\n` +
|
|
80
|
+
` ALTER TABLE ${tableName} VALIDATE CONSTRAINT chk_${target.columnName}_not_null;\n` +
|
|
81
|
+
` 3. Apply SET NOT NULL (instant in PG 12+ due to validated constraint):\n` +
|
|
82
|
+
` ALTER TABLE ${tableName} ALTER COLUMN ${target.columnName} SET NOT NULL;`,
|
|
83
|
+
file: context.filePath,
|
|
84
|
+
line: stmt.startLine,
|
|
85
|
+
column: stmt.startColumn,
|
|
86
|
+
codeSnippet: stmt.raw,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return findings;
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
//# sourceMappingURL=setNotNullFullScan.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"setNotNullFullScan.js","sourceRoot":"","sources":["../../../src/rules/setNotNullFullScan.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ddlforge Rule: CONCURRENTLY inside Transaction Trap
|
|
3
|
+
*
|
|
4
|
+
* Severity: BLOCKER
|
|
5
|
+
* Engine behavior: Postgres aborts execution with `ERROR: CREATE INDEX CONCURRENTLY cannot run inside a transaction block`
|
|
6
|
+
*/
|
|
7
|
+
import { Rule } from './types.js';
|
|
8
|
+
export declare const transactionTrapRule: Rule;
|
|
9
|
+
//# sourceMappingURL=transactionTrap.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transactionTrap.d.ts","sourceRoot":"","sources":["../../../src/rules/transactionTrap.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,IAAI,EAAwB,MAAM,YAAY,CAAC;AA4BxD,eAAO,MAAM,mBAAmB,EAAE,IA+EjC,CAAC"}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ddlforge Rule: CONCURRENTLY inside Transaction Trap
|
|
3
|
+
*
|
|
4
|
+
* Severity: BLOCKER
|
|
5
|
+
* Engine behavior: Postgres aborts execution with `ERROR: CREATE INDEX CONCURRENTLY cannot run inside a transaction block`
|
|
6
|
+
*/
|
|
7
|
+
import { PostgresLockLevel } from '../engine/locks.js';
|
|
8
|
+
function isConcurrentIndexStatement(tokens) {
|
|
9
|
+
if (tokens.length < 3)
|
|
10
|
+
return false;
|
|
11
|
+
// CREATE [UNIQUE] INDEX CONCURRENTLY
|
|
12
|
+
if (tokens[0].value === 'CREATE') {
|
|
13
|
+
let idx = 1;
|
|
14
|
+
if (tokens[idx]?.value === 'UNIQUE')
|
|
15
|
+
idx++;
|
|
16
|
+
if (tokens[idx]?.value === 'INDEX') {
|
|
17
|
+
for (let i = idx + 1; i < tokens.length; i++) {
|
|
18
|
+
if (tokens[i].value === 'ON')
|
|
19
|
+
break;
|
|
20
|
+
if (tokens[i].value === 'CONCURRENTLY')
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
// DROP INDEX CONCURRENTLY
|
|
26
|
+
if (tokens[0].value === 'DROP' && tokens[1]?.value === 'INDEX') {
|
|
27
|
+
for (let i = 2; i < tokens.length; i++) {
|
|
28
|
+
if (tokens[i].value === 'CONCURRENTLY')
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
export const transactionTrapRule = {
|
|
35
|
+
id: 'concurrent-index-in-transaction',
|
|
36
|
+
name: 'CONCURRENTLY inside Transaction Trap',
|
|
37
|
+
description: 'PostgreSQL aborts transactions containing concurrent index builds or drops.',
|
|
38
|
+
defaultSeverity: 'BLOCKER',
|
|
39
|
+
lockLevel: PostgresLockLevel.NONE,
|
|
40
|
+
check(context) {
|
|
41
|
+
const findings = [];
|
|
42
|
+
let inExplicitTransaction = false;
|
|
43
|
+
let transactionStartLine = 1;
|
|
44
|
+
for (const stmt of context.statements) {
|
|
45
|
+
const tokens = stmt.tokens;
|
|
46
|
+
if (tokens.length === 0)
|
|
47
|
+
continue;
|
|
48
|
+
const firstVal = tokens[0].value;
|
|
49
|
+
// Detect transaction start
|
|
50
|
+
if (firstVal === 'BEGIN' || (firstVal === 'START' && tokens[1]?.value === 'TRANSACTION')) {
|
|
51
|
+
inExplicitTransaction = true;
|
|
52
|
+
transactionStartLine = stmt.startLine;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
// Detect transaction end
|
|
56
|
+
if (firstVal === 'COMMIT' || firstVal === 'ROLLBACK' || firstVal === 'END') {
|
|
57
|
+
inExplicitTransaction = false;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
// Check if this statement is CREATE INDEX CONCURRENTLY or DROP INDEX CONCURRENTLY
|
|
61
|
+
const isConcurrentIndexOp = isConcurrentIndexStatement(tokens);
|
|
62
|
+
if (isConcurrentIndexOp) {
|
|
63
|
+
if (stmt.hasIgnore(this.id))
|
|
64
|
+
continue;
|
|
65
|
+
// Scenario 1: Inside an explicit transaction block
|
|
66
|
+
if (inExplicitTransaction) {
|
|
67
|
+
findings.push({
|
|
68
|
+
ruleId: this.id,
|
|
69
|
+
ruleName: this.name,
|
|
70
|
+
severity: this.defaultSeverity,
|
|
71
|
+
lockLevel: this.lockLevel,
|
|
72
|
+
message: 'CONCURRENTLY index operation cannot run inside an explicit transaction block.',
|
|
73
|
+
detail: `Statement at line ${stmt.startLine} is inside a transaction block started at line ${transactionStartLine}. ` +
|
|
74
|
+
'PostgreSQL rejects concurrent index operations with "ERROR: CREATE/DROP INDEX CONCURRENTLY cannot run inside a transaction block".',
|
|
75
|
+
suggestion: 'Remove the enclosing BEGIN ... COMMIT block and execute the CONCURRENTLY statement standalone.',
|
|
76
|
+
file: context.filePath,
|
|
77
|
+
line: stmt.startLine,
|
|
78
|
+
column: stmt.startColumn,
|
|
79
|
+
codeSnippet: stmt.raw,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
// Scenario 2: Prisma migration without -- prisma:no-transaction
|
|
83
|
+
if (context.isPrismaMigration && !context.hasFilePrismaNoTransaction) {
|
|
84
|
+
findings.push({
|
|
85
|
+
ruleId: this.id,
|
|
86
|
+
ruleName: this.name,
|
|
87
|
+
severity: this.defaultSeverity,
|
|
88
|
+
lockLevel: this.lockLevel,
|
|
89
|
+
message: 'Concurrent index operation in Prisma migration requires "-- prisma:no-transaction".',
|
|
90
|
+
detail: 'Prisma Migrate executes every migration inside an implicit transaction block. ' +
|
|
91
|
+
'Without the "-- prisma:no-transaction" directive, PostgreSQL will abort migration application.',
|
|
92
|
+
suggestion: 'Add `-- prisma:no-transaction` at the very top of this migration file.',
|
|
93
|
+
file: context.filePath,
|
|
94
|
+
line: stmt.startLine,
|
|
95
|
+
column: stmt.startColumn,
|
|
96
|
+
codeSnippet: stmt.raw,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return findings;
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
//# sourceMappingURL=transactionTrap.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transactionTrap.js","sourceRoot":"","sources":["../../../src/rules/transactionTrap.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ddlforge - Rule engine types, Finding, and Context interfaces
|
|
3
|
+
*/
|
|
4
|
+
import { Statement } from '../lexer/tokens.js';
|
|
5
|
+
import { PostgresLockLevel } from '../engine/locks.js';
|
|
6
|
+
export type Severity = 'BLOCKER' | 'WARNING' | 'ADVISORY';
|
|
7
|
+
export interface Finding {
|
|
8
|
+
ruleId: string;
|
|
9
|
+
ruleName: string;
|
|
10
|
+
severity: Severity;
|
|
11
|
+
lockLevel: PostgresLockLevel;
|
|
12
|
+
message: string;
|
|
13
|
+
detail: string;
|
|
14
|
+
suggestion: string;
|
|
15
|
+
file: string;
|
|
16
|
+
line: number;
|
|
17
|
+
column: number;
|
|
18
|
+
codeSnippet: string;
|
|
19
|
+
}
|
|
20
|
+
export interface RuleContext {
|
|
21
|
+
filePath: string;
|
|
22
|
+
fileContent: string;
|
|
23
|
+
pgVersion: number;
|
|
24
|
+
statements: Statement[];
|
|
25
|
+
isPrismaMigration: boolean;
|
|
26
|
+
hasFilePrismaNoTransaction: boolean;
|
|
27
|
+
}
|
|
28
|
+
export interface Rule {
|
|
29
|
+
id: string;
|
|
30
|
+
name: string;
|
|
31
|
+
description: string;
|
|
32
|
+
defaultSeverity: Severity;
|
|
33
|
+
lockLevel: PostgresLockLevel;
|
|
34
|
+
check(context: RuleContext): Finding[];
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/rules/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAC/C,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAEvD,MAAM,MAAM,QAAQ,GAAG,SAAS,GAAG,SAAS,GAAG,UAAU,CAAC;AAE1D,MAAM,WAAW,OAAO;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,QAAQ,CAAC;IACnB,SAAS,EAAE,iBAAiB,CAAC;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,SAAS,EAAE,CAAC;IACxB,iBAAiB,EAAE,OAAO,CAAC;IAC3B,0BAA0B,EAAE,OAAO,CAAC;CACrC;AAED,MAAM,WAAW,IAAI;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,QAAQ,CAAC;IAC1B,SAAS,EAAE,iBAAiB,CAAC;IAC7B,KAAK,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,EAAE,CAAC;CACxC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/rules/types.ts"],"names":[],"mappings":"AAAA;;GAEG"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ddlforge Rule: Unbatched DML in Migration (Bare UPDATE/DELETE)
|
|
3
|
+
*
|
|
4
|
+
* Severity: WARNING
|
|
5
|
+
* Lock: ROW EXCLUSIVE
|
|
6
|
+
*/
|
|
7
|
+
import { Rule } from './types.js';
|
|
8
|
+
export declare const unbatchedBackfillRule: Rule;
|
|
9
|
+
//# sourceMappingURL=unbatchedBackfill.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"unbatchedBackfill.d.ts","sourceRoot":"","sources":["../../../src/rules/unbatchedBackfill.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,IAAI,EAAwB,MAAM,YAAY,CAAC;AAGxD,eAAO,MAAM,qBAAqB,EAAE,IAyDnC,CAAC"}
|