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.
Files changed (83) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +326 -0
  3. package/dist/bin/ddlforge.d.ts +6 -0
  4. package/dist/bin/ddlforge.d.ts.map +1 -0
  5. package/dist/bin/ddlforge.js +14 -0
  6. package/dist/bin/ddlforge.js.map +1 -0
  7. package/dist/src/cli.d.ts +21 -0
  8. package/dist/src/cli.d.ts.map +1 -0
  9. package/dist/src/cli.js +262 -0
  10. package/dist/src/cli.js.map +1 -0
  11. package/dist/src/engine/analyzer.d.ts +30 -0
  12. package/dist/src/engine/analyzer.d.ts.map +1 -0
  13. package/dist/src/engine/analyzer.js +76 -0
  14. package/dist/src/engine/analyzer.js.map +1 -0
  15. package/dist/src/engine/locks.d.ts +26 -0
  16. package/dist/src/engine/locks.d.ts.map +1 -0
  17. package/dist/src/engine/locks.js +136 -0
  18. package/dist/src/engine/locks.js.map +1 -0
  19. package/dist/src/index.d.ts +12 -0
  20. package/dist/src/index.d.ts.map +1 -0
  21. package/dist/src/index.js +17 -0
  22. package/dist/src/index.js.map +1 -0
  23. package/dist/src/lexer/sqlTokenizer.d.ts +40 -0
  24. package/dist/src/lexer/sqlTokenizer.d.ts.map +1 -0
  25. package/dist/src/lexer/sqlTokenizer.js +494 -0
  26. package/dist/src/lexer/sqlTokenizer.js.map +1 -0
  27. package/dist/src/lexer/tokens.d.ts +35 -0
  28. package/dist/src/lexer/tokens.d.ts.map +1 -0
  29. package/dist/src/lexer/tokens.js +81 -0
  30. package/dist/src/lexer/tokens.js.map +1 -0
  31. package/dist/src/reporters/index.d.ts +7 -0
  32. package/dist/src/reporters/index.d.ts.map +1 -0
  33. package/dist/src/reporters/index.js +7 -0
  34. package/dist/src/reporters/index.js.map +1 -0
  35. package/dist/src/reporters/json.d.ts +20 -0
  36. package/dist/src/reporters/json.d.ts.map +1 -0
  37. package/dist/src/reporters/json.js +35 -0
  38. package/dist/src/reporters/json.js.map +1 -0
  39. package/dist/src/reporters/markdown.d.ts +6 -0
  40. package/dist/src/reporters/markdown.d.ts.map +1 -0
  41. package/dist/src/reporters/markdown.js +70 -0
  42. package/dist/src/reporters/markdown.js.map +1 -0
  43. package/dist/src/reporters/terminal.d.ts +10 -0
  44. package/dist/src/reporters/terminal.d.ts.map +1 -0
  45. package/dist/src/reporters/terminal.js +109 -0
  46. package/dist/src/reporters/terminal.js.map +1 -0
  47. package/dist/src/rules/addColumnNotNull.d.ts +9 -0
  48. package/dist/src/rules/addColumnNotNull.d.ts.map +1 -0
  49. package/dist/src/rules/addColumnNotNull.js +131 -0
  50. package/dist/src/rules/addColumnNotNull.js.map +1 -0
  51. package/dist/src/rules/foreignKeyNotValid.d.ts +9 -0
  52. package/dist/src/rules/foreignKeyNotValid.d.ts.map +1 -0
  53. package/dist/src/rules/foreignKeyNotValid.js +119 -0
  54. package/dist/src/rules/foreignKeyNotValid.js.map +1 -0
  55. package/dist/src/rules/index.d.ts +14 -0
  56. package/dist/src/rules/index.d.ts.map +1 -0
  57. package/dist/src/rules/index.js +28 -0
  58. package/dist/src/rules/index.js.map +1 -0
  59. package/dist/src/rules/indexConcurrently.d.ts +9 -0
  60. package/dist/src/rules/indexConcurrently.d.ts.map +1 -0
  61. package/dist/src/rules/indexConcurrently.js +92 -0
  62. package/dist/src/rules/indexConcurrently.js.map +1 -0
  63. package/dist/src/rules/prismaRenameDropAdd.d.ts +11 -0
  64. package/dist/src/rules/prismaRenameDropAdd.d.ts.map +1 -0
  65. package/dist/src/rules/prismaRenameDropAdd.js +144 -0
  66. package/dist/src/rules/prismaRenameDropAdd.js.map +1 -0
  67. package/dist/src/rules/setNotNullFullScan.d.ts +9 -0
  68. package/dist/src/rules/setNotNullFullScan.d.ts.map +1 -0
  69. package/dist/src/rules/setNotNullFullScan.js +93 -0
  70. package/dist/src/rules/setNotNullFullScan.js.map +1 -0
  71. package/dist/src/rules/transactionTrap.d.ts +9 -0
  72. package/dist/src/rules/transactionTrap.d.ts.map +1 -0
  73. package/dist/src/rules/transactionTrap.js +104 -0
  74. package/dist/src/rules/transactionTrap.js.map +1 -0
  75. package/dist/src/rules/types.d.ts +36 -0
  76. package/dist/src/rules/types.d.ts.map +1 -0
  77. package/dist/src/rules/types.js +5 -0
  78. package/dist/src/rules/types.js.map +1 -0
  79. package/dist/src/rules/unbatchedBackfill.d.ts +9 -0
  80. package/dist/src/rules/unbatchedBackfill.d.ts.map +1 -0
  81. package/dist/src/rules/unbatchedBackfill.js +58 -0
  82. package/dist/src/rules/unbatchedBackfill.js.map +1 -0
  83. 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,5 @@
1
+ /**
2
+ * ddlforge - Rule engine types, Finding, and Context interfaces
3
+ */
4
+ export {};
5
+ //# sourceMappingURL=types.js.map
@@ -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"}