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
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ddlforge contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
# 🛡️ ddlforge
|
|
2
|
+
|
|
3
|
+
> **Ultra-fast, zero-runtime-dependency PostgreSQL migration lock linter & data-loss prevention engine.**
|
|
4
|
+
> Built for Node.js 20+ and TypeScript. Designed for high-traffic Prisma, Drizzle, and raw SQL backends.
|
|
5
|
+
|
|
6
|
+
[](https://nodejs.org)
|
|
7
|
+
[](https://www.typescriptlang.org)
|
|
8
|
+
[](package.json)
|
|
9
|
+
[](LICENSE)
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## ⚡ Why ddlforge?
|
|
14
|
+
|
|
15
|
+
In PostgreSQL, DDL commands take **table-level locks**. Even if a migration takes only milliseconds to run, if it requests an `ACCESS EXCLUSIVE` or `SHARE` lock while a long-running `SELECT` or transaction is open:
|
|
16
|
+
1. The migration stalls waiting for the lock.
|
|
17
|
+
2. **Every single query behind it is queued up**, including basic reads (`SELECT`).
|
|
18
|
+
3. Connection pools instantly exhaust within seconds, crashing services and taking down production.
|
|
19
|
+
|
|
20
|
+
Furthermore, ORMs like **Prisma Migrate** cannot detect column renames automatically: when you rename a field in `schema.prisma`, Prisma generates a `DROP COLUMN` followed by `ADD COLUMN`, permanently deleting all your production data!
|
|
21
|
+
|
|
22
|
+
`ddlforge` catches these architectural pitfalls in your local pre-commit hooks and CI pipelines before code ever merges to main.
|
|
23
|
+
|
|
24
|
+
### Design Principles
|
|
25
|
+
- **Zero Runtime Dependencies**: Pure native Node.js standard library (`node:fs`, `node:path`, `node:process`, `node:child_process`). Air-gap safe.
|
|
26
|
+
- **Sub-Second Performance**: Single-pass lexical scanner and linear rule evaluators. Scans 100+ migration files in **<10ms**.
|
|
27
|
+
- **Deterministic Exit Codes**: Exits `0` when clean or advisory-only; exits `1` for any blocking violation (table lock or destructive data loss).
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## 📊 PostgreSQL Lock Hierarchy Reference
|
|
32
|
+
|
|
33
|
+
| Lock Mode | Acquired By | Conflicted Locks | Blocks Reads? | Blocks Writes? | Risk Level |
|
|
34
|
+
|:---|:---|:---|:---:|:---:|:---:|
|
|
35
|
+
| `ACCESS SHARE` | `SELECT` | `ACCESS EXCLUSIVE` | ❌ | ❌ | Low |
|
|
36
|
+
| `ROW SHARE` | `SELECT FOR UPDATE / FOR SHARE` | `EXCLUSIVE`, `ACCESS EXCLUSIVE` | ❌ | ❌ | Low |
|
|
37
|
+
| `ROW EXCLUSIVE` | `UPDATE`, `DELETE`, `INSERT` | `SHARE`, `SHARE ROW EXCLUSIVE`, `EXCLUSIVE`, `ACCESS EXCLUSIVE` | ❌ | ❌ | Medium |
|
|
38
|
+
| `SHARE UPDATE EXCLUSIVE` | `CREATE INDEX CONCURRENTLY`, `VACUUM`, `ANALYZE`, `VALIDATE CONSTRAINT` | `SHARE UPDATE EXCLUSIVE`, `SHARE`, `SHARE ROW EXCLUSIVE`, `EXCLUSIVE`, `ACCESS EXCLUSIVE` | ❌ | ❌ | **Safe for Concurrency** |
|
|
39
|
+
| `SHARE` | `CREATE INDEX` (non-concurrent) | `ROW EXCLUSIVE`, `SHARE ROW EXCLUSIVE`, `EXCLUSIVE`, `ACCESS EXCLUSIVE` | ❌ | **YES** | 🛑 **BLOCKER** |
|
|
40
|
+
| `SHARE ROW EXCLUSIVE` | `ADD CONSTRAINT FOREIGN KEY` (without `NOT VALID`) | `ROW EXCLUSIVE`, `SHARE UPDATE EXCLUSIVE`, `SHARE`, `SHARE ROW EXCLUSIVE`, `EXCLUSIVE`, `ACCESS EXCLUSIVE` | ❌ | **YES** | 🛑 **BLOCKER** |
|
|
41
|
+
| `EXCLUSIVE` | `REFRESH MATERIALIZED VIEW CONCURRENTLY` | `ROW SHARE`, `ROW EXCLUSIVE`, `SHARE UPDATE EXCLUSIVE`, `SHARE`, `SHARE ROW EXCLUSIVE`, `EXCLUSIVE`, `ACCESS EXCLUSIVE` | Row Share | **YES** | 🛑 **BLOCKER** |
|
|
42
|
+
| `ACCESS EXCLUSIVE` | `ALTER TABLE`, `DROP TABLE`, `TRUNCATE`, `ALTER COLUMN SET NOT NULL` | **ALL LOCK MODES** | **YES (SELECT)** | **YES** | 🚨 **CRITICAL DOWNTIME** |
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## 룰 The 7 Core Rules & Zero-Downtime Recipes
|
|
47
|
+
|
|
48
|
+
### 1. `require-concurrent-index` (CREATE INDEX without CONCURRENTLY)
|
|
49
|
+
- **Severity**: `BLOCKER`
|
|
50
|
+
- **Lock**: `SHARE` lock (blocks all `INSERT`, `UPDATE`, `DELETE` operations on table).
|
|
51
|
+
- **Dangerous**:
|
|
52
|
+
```sql
|
|
53
|
+
CREATE INDEX idx_users_email ON users(email);
|
|
54
|
+
```
|
|
55
|
+
- **Zero-Downtime Fix**:
|
|
56
|
+
```sql
|
|
57
|
+
CREATE INDEX CONCURRENTLY idx_users_email ON users(email);
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
### 2. `concurrent-index-in-transaction` (CONCURRENTLY inside Transaction Trap)
|
|
63
|
+
- **Severity**: `BLOCKER`
|
|
64
|
+
- **Engine Behavior**: PostgreSQL aborts with `ERROR: CREATE INDEX CONCURRENTLY cannot run inside a transaction block`.
|
|
65
|
+
- **Dangerous**:
|
|
66
|
+
```sql
|
|
67
|
+
BEGIN;
|
|
68
|
+
CREATE INDEX CONCURRENTLY idx_users_email ON users(email);
|
|
69
|
+
COMMIT;
|
|
70
|
+
```
|
|
71
|
+
- **Prisma Caveat**: Prisma Migrate wraps all migrations in an implicit transaction by default. Running `CREATE INDEX CONCURRENTLY` in Prisma fails unless you add the `-- prisma:no-transaction` directive.
|
|
72
|
+
- **Zero-Downtime Fix**:
|
|
73
|
+
```sql
|
|
74
|
+
-- prisma:no-transaction
|
|
75
|
+
CREATE INDEX CONCURRENTLY "users_email_idx" ON "users"("email");
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
### 3. `add-column-not-null-without-default` (ADD COLUMN NOT NULL without DEFAULT)
|
|
81
|
+
- **Severity**: `BLOCKER`
|
|
82
|
+
- **Lock**: `ACCESS EXCLUSIVE` lock.
|
|
83
|
+
- **Dangerous**:
|
|
84
|
+
```sql
|
|
85
|
+
ALTER TABLE users ADD COLUMN role VARCHAR(50) NOT NULL;
|
|
86
|
+
```
|
|
87
|
+
- **Why**: On non-empty tables, PostgreSQL immediately aborts with `ERROR: column "role" contains null values` while holding an `ACCESS EXCLUSIVE` table lock.
|
|
88
|
+
- **Zero-Downtime Fix (PG 11+)**:
|
|
89
|
+
Provide a static non-volatile default (metadata-only instant operation):
|
|
90
|
+
```sql
|
|
91
|
+
ALTER TABLE users ADD COLUMN role VARCHAR(50) DEFAULT 'user' NOT NULL;
|
|
92
|
+
```
|
|
93
|
+
Or for dynamic/custom defaults, use the multi-step backfill pattern:
|
|
94
|
+
```sql
|
|
95
|
+
-- Step 1: Add column nullable
|
|
96
|
+
ALTER TABLE users ADD COLUMN role VARCHAR(50);
|
|
97
|
+
-- Step 2: Batched backfill
|
|
98
|
+
-- Step 3: Add CHECK constraint NOT VALID & validate
|
|
99
|
+
ALTER TABLE users ADD CONSTRAINT chk_role_not_null CHECK (role IS NOT NULL) NOT VALID;
|
|
100
|
+
ALTER TABLE users VALIDATE CONSTRAINT chk_role_not_null;
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
### 4. `foreign-key-missing-not-valid` (ADD CONSTRAINT FOREIGN KEY without NOT VALID)
|
|
106
|
+
- **Severity**: `BLOCKER`
|
|
107
|
+
- **Lock**: `SHARE ROW EXCLUSIVE` on referencing table + `SHARE` on referenced table.
|
|
108
|
+
- **Dangerous**:
|
|
109
|
+
```sql
|
|
110
|
+
ALTER TABLE orders ADD CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users(id);
|
|
111
|
+
```
|
|
112
|
+
- **Zero-Downtime Fix**:
|
|
113
|
+
Split into two non-blocking steps:
|
|
114
|
+
```sql
|
|
115
|
+
-- Step 1: Add constraint without scanning rows (instant metadata operation)
|
|
116
|
+
ALTER TABLE orders ADD CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users(id) NOT VALID;
|
|
117
|
+
|
|
118
|
+
-- Step 2: Validate concurrently (takes only SHARE UPDATE EXCLUSIVE, reads & writes proceed!)
|
|
119
|
+
ALTER TABLE orders VALIDATE CONSTRAINT fk_user;
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
### 5. `prisma-silent-rename-data-loss` (Prisma Silent Field Rename)
|
|
125
|
+
- **Severity**: `BLOCKER` (Data-Loss Prevention)
|
|
126
|
+
- **Lock**: `ACCESS EXCLUSIVE` + Table Data Loss.
|
|
127
|
+
- **Dangerous**:
|
|
128
|
+
```sql
|
|
129
|
+
ALTER TABLE "users" DROP COLUMN "old_name";
|
|
130
|
+
ALTER TABLE "users" ADD COLUMN "new_name" TEXT NOT NULL;
|
|
131
|
+
```
|
|
132
|
+
- **Why**: Prisma Migrate cannot infer renames from `schema.prisma`. It generates a column drop followed by column creation, destroying all production data.
|
|
133
|
+
- **Zero-Downtime Fix**:
|
|
134
|
+
```sql
|
|
135
|
+
ALTER TABLE "users" RENAME COLUMN "old_name" TO "new_name";
|
|
136
|
+
```
|
|
137
|
+
Or use Prisma's `@map("old_name")` in your schema to avoid database column renames.
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
### 6. `set-not-null-full-scan` (ALTER COLUMN SET NOT NULL)
|
|
142
|
+
- **Severity**: `WARNING` (or `BLOCKER` on PG < 12)
|
|
143
|
+
- **Lock**: `ACCESS EXCLUSIVE` lock while scanning entire table for nulls.
|
|
144
|
+
- **Dangerous**:
|
|
145
|
+
```sql
|
|
146
|
+
ALTER TABLE users ALTER COLUMN email SET NOT NULL;
|
|
147
|
+
```
|
|
148
|
+
- **Zero-Downtime Fix (PG 12+)**:
|
|
149
|
+
```sql
|
|
150
|
+
-- 1. Add CHECK constraint NOT VALID (instant)
|
|
151
|
+
ALTER TABLE users ADD CONSTRAINT chk_users_email_not_null CHECK (email IS NOT NULL) NOT VALID;
|
|
152
|
+
-- 2. Validate constraint without blocking writes
|
|
153
|
+
ALTER TABLE users VALIDATE CONSTRAINT chk_users_email_not_null;
|
|
154
|
+
-- 3. Set NOT NULL (Postgres 12+ skips table scan because validated CHECK constraint exists!)
|
|
155
|
+
ALTER TABLE users ALTER COLUMN email SET NOT NULL;
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
### 7. `unbatched-dml` (Unbatched DML in Migration)
|
|
161
|
+
- **Severity**: `WARNING` (or `BLOCKER` without WHERE)
|
|
162
|
+
- **Lock**: `ROW EXCLUSIVE` lock on touched rows, massive transaction WAL bloat.
|
|
163
|
+
- **Dangerous**:
|
|
164
|
+
```sql
|
|
165
|
+
UPDATE users SET active = true;
|
|
166
|
+
DELETE FROM sessions;
|
|
167
|
+
```
|
|
168
|
+
- **Zero-Downtime Fix**:
|
|
169
|
+
Perform large backfills out-of-band using batched background workers. If updating a small static/seed table, exempt with an inline comment:
|
|
170
|
+
```sql
|
|
171
|
+
-- ddlforge-ignore unbatched-dml
|
|
172
|
+
UPDATE subscription_tiers SET active = true WHERE code = 'PRO';
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## 🚀 CLI Installation & Usage
|
|
178
|
+
|
|
179
|
+
### Running via npx (Zero Install)
|
|
180
|
+
```bash
|
|
181
|
+
npx ddlforge ./prisma/migrations
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Local Installation
|
|
185
|
+
```bash
|
|
186
|
+
npm install --save-dev ddlforge
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### CLI Flags
|
|
190
|
+
```text
|
|
191
|
+
ddlforge [paths...] [flags]
|
|
192
|
+
|
|
193
|
+
ARGUMENTS:
|
|
194
|
+
paths Target migration files or directories (e.g. ./prisma/migrations, ./drizzle)
|
|
195
|
+
|
|
196
|
+
FLAGS:
|
|
197
|
+
--pg <version> Target PostgreSQL version (default: 16)
|
|
198
|
+
--format <type> Output format: terminal | json | markdown (default: terminal)
|
|
199
|
+
--quiet, -q Suppress advisories/warnings and emit blockers only
|
|
200
|
+
--changed-only Use git diff to lint only staged or branch-modified migration files
|
|
201
|
+
--version, -v Print ddlforge version and exit
|
|
202
|
+
--help, -h Print this help message and exit
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Examples
|
|
206
|
+
```bash
|
|
207
|
+
# Lint all Prisma migrations
|
|
208
|
+
npx ddlforge ./prisma/migrations
|
|
209
|
+
|
|
210
|
+
# Lint only modified migration files in current git branch / PR
|
|
211
|
+
npx ddlforge --changed-only
|
|
212
|
+
|
|
213
|
+
# Check Drizzle migrations targeting Postgres 14 with JSON output
|
|
214
|
+
npx ddlforge ./drizzle --pg 14 --format json
|
|
215
|
+
|
|
216
|
+
# CI Mode: Emit blockers only with exit code 1
|
|
217
|
+
npx ddlforge ./migrations --quiet
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
## 🤖 GitHub Actions Workflow
|
|
223
|
+
|
|
224
|
+
Add this copy-paste workflow to `.github/workflows/migration-lint.yml`:
|
|
225
|
+
|
|
226
|
+
```yaml
|
|
227
|
+
name: Migration Lock Linter
|
|
228
|
+
|
|
229
|
+
on:
|
|
230
|
+
pull_request:
|
|
231
|
+
paths:
|
|
232
|
+
- '**/migrations/**'
|
|
233
|
+
- '**/*.sql'
|
|
234
|
+
|
|
235
|
+
jobs:
|
|
236
|
+
lint-migrations:
|
|
237
|
+
runs-on: ubuntu-latest
|
|
238
|
+
steps:
|
|
239
|
+
- name: Checkout Code
|
|
240
|
+
uses: actions/checkout@v4
|
|
241
|
+
with:
|
|
242
|
+
fetch-depth: 0
|
|
243
|
+
|
|
244
|
+
- name: Setup Node.js
|
|
245
|
+
uses: actions/setup-node@v4
|
|
246
|
+
with:
|
|
247
|
+
node-version: 20
|
|
248
|
+
|
|
249
|
+
- name: Run ddlforge Lock Linter
|
|
250
|
+
run: |
|
|
251
|
+
npx ddlforge --changed-only --format terminal
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### PR Comment Workflow (with Markdown Formatter)
|
|
255
|
+
```yaml
|
|
256
|
+
- name: Generate Migration Safety Report
|
|
257
|
+
if: always()
|
|
258
|
+
run: |
|
|
259
|
+
npx ddlforge --changed-only --format markdown > report.md
|
|
260
|
+
cat report.md >> $GITHUB_STEP_SUMMARY
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
---
|
|
264
|
+
|
|
265
|
+
## 💻 Programmatic TypeScript API
|
|
266
|
+
|
|
267
|
+
You can also use `ddlforge` programmatically within your custom deployment scripts:
|
|
268
|
+
|
|
269
|
+
```typescript
|
|
270
|
+
import { analyzeSql, MigrationAnalyzer, formatTerminal } from 'ddlforge';
|
|
271
|
+
|
|
272
|
+
const sql = `
|
|
273
|
+
CREATE INDEX idx_users_email ON users(email);
|
|
274
|
+
`;
|
|
275
|
+
|
|
276
|
+
const result = analyzeSql(sql, {
|
|
277
|
+
filePath: 'migrations/001_create_index.sql',
|
|
278
|
+
pgVersion: 16,
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
if (result.hasBlockers) {
|
|
282
|
+
console.error(`Found ${result.blockersCount} blocking migration locks!`);
|
|
283
|
+
console.log(formatTerminal([result]));
|
|
284
|
+
process.exit(1);
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
---
|
|
289
|
+
|
|
290
|
+
## ⚙️ Inline Directives
|
|
291
|
+
|
|
292
|
+
You can suppress specific rules per statement using inline comments:
|
|
293
|
+
|
|
294
|
+
```sql
|
|
295
|
+
-- ddlforge-ignore require-concurrent-index
|
|
296
|
+
CREATE INDEX idx_temp ON temporary_cache(key);
|
|
297
|
+
|
|
298
|
+
-- ddlforge-ignore unbatched-dml
|
|
299
|
+
UPDATE plan_limits SET max_seats = 100 WHERE plan_id = 'enterprise';
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
For Prisma migrations containing `CONCURRENTLY`:
|
|
303
|
+
```sql
|
|
304
|
+
-- prisma:no-transaction
|
|
305
|
+
CREATE INDEX CONCURRENTLY "users_email_idx" ON "users"("email");
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
---
|
|
309
|
+
|
|
310
|
+
## 🧪 Testing & Verification
|
|
311
|
+
|
|
312
|
+
Run the native test suite:
|
|
313
|
+
```bash
|
|
314
|
+
npm test
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
Performance benchmark:
|
|
318
|
+
```text
|
|
319
|
+
✔ analyzes 100 migrations in under 100 milliseconds (9.2ms)
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
---
|
|
323
|
+
|
|
324
|
+
## 📄 License
|
|
325
|
+
|
|
326
|
+
MIT © [ddlforge](https://github.com/ddlforge/ddlforge)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ddlforge.d.ts","sourceRoot":"","sources":["../../bin/ddlforge.ts"],"names":[],"mappings":";AAEA;;GAEG"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* ddlforge - Postgres migration lock linter and data-loss prevention engine
|
|
4
|
+
*/
|
|
5
|
+
import { runCli } from '../src/cli.js';
|
|
6
|
+
runCli(process.argv.slice(2))
|
|
7
|
+
.then(exitCode => {
|
|
8
|
+
process.exit(exitCode);
|
|
9
|
+
})
|
|
10
|
+
.catch(err => {
|
|
11
|
+
console.error('Fatal ddlforge error:', err);
|
|
12
|
+
process.exit(1);
|
|
13
|
+
});
|
|
14
|
+
//# sourceMappingURL=ddlforge.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ddlforge.js","sourceRoot":"","sources":["../../bin/ddlforge.ts"],"names":[],"mappings":";AAEA;;GAEG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAEvC,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;KAC1B,IAAI,CAAC,QAAQ,CAAC,EAAE;IACf,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;AACzB,CAAC,CAAC;KACD,KAAK,CAAC,GAAG,CAAC,EAAE;IACX,OAAO,CAAC,KAAK,CAAC,uBAAuB,EAAE,GAAG,CAAC,CAAC;IAC5C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ddlforge - Zero-dependency custom CLI argument parser and execution engine
|
|
3
|
+
*/
|
|
4
|
+
export interface CliOptions {
|
|
5
|
+
targets: string[];
|
|
6
|
+
pgVersion: number;
|
|
7
|
+
format: 'terminal' | 'json' | 'markdown';
|
|
8
|
+
quiet: boolean;
|
|
9
|
+
changedOnly: boolean;
|
|
10
|
+
help: boolean;
|
|
11
|
+
version: boolean;
|
|
12
|
+
}
|
|
13
|
+
export declare const VERSION = "0.1.0";
|
|
14
|
+
export declare function parseArgs(args: string[]): CliOptions;
|
|
15
|
+
export declare function printHelp(): void;
|
|
16
|
+
/**
|
|
17
|
+
* Finds all .sql files from input paths, directories, or defaults.
|
|
18
|
+
*/
|
|
19
|
+
export declare function discoverSqlFiles(targets: string[], changedOnly: boolean, cwd?: string): string[];
|
|
20
|
+
export declare function runCli(argv?: string[]): Promise<number>;
|
|
21
|
+
//# sourceMappingURL=cli.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/cli.ts"],"names":[],"mappings":"AAAA;;GAEG;AAUH,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,UAAU,GAAG,MAAM,GAAG,UAAU,CAAC;IACzC,KAAK,EAAE,OAAO,CAAC;IACf,WAAW,EAAE,OAAO,CAAC;IACrB,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,eAAO,MAAM,OAAO,UAAU,CAAC;AAE/B,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,UAAU,CAkFpD;AAED,wBAAgB,SAAS,IAAI,IAAI,CAwBhC;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,WAAW,EAAE,OAAO,EAAE,GAAG,GAAE,MAAsB,GAAG,MAAM,EAAE,CAwB/G;AAgFD,wBAAsB,MAAM,CAAC,IAAI,GAAE,MAAM,EAA0B,GAAG,OAAO,CAAC,MAAM,CAAC,CAqDpF"}
|
package/dist/src/cli.js
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ddlforge - Zero-dependency custom CLI argument parser and execution engine
|
|
3
|
+
*/
|
|
4
|
+
import * as fs from 'node:fs';
|
|
5
|
+
import * as path from 'node:path';
|
|
6
|
+
import { execSync } from 'node:child_process';
|
|
7
|
+
import { MigrationAnalyzer } from './engine/analyzer.js';
|
|
8
|
+
import { formatTerminal } from './reporters/terminal.js';
|
|
9
|
+
import { formatJson } from './reporters/json.js';
|
|
10
|
+
import { formatMarkdown } from './reporters/markdown.js';
|
|
11
|
+
export const VERSION = '0.1.0';
|
|
12
|
+
export function parseArgs(args) {
|
|
13
|
+
const options = {
|
|
14
|
+
targets: [],
|
|
15
|
+
pgVersion: 16,
|
|
16
|
+
format: 'terminal',
|
|
17
|
+
quiet: false,
|
|
18
|
+
changedOnly: false,
|
|
19
|
+
help: false,
|
|
20
|
+
version: false,
|
|
21
|
+
};
|
|
22
|
+
let i = 0;
|
|
23
|
+
while (i < args.length) {
|
|
24
|
+
const arg = args[i];
|
|
25
|
+
if (arg === '--help' || arg === '-h') {
|
|
26
|
+
options.help = true;
|
|
27
|
+
i++;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (arg === '--version' || arg === '-v') {
|
|
31
|
+
options.version = true;
|
|
32
|
+
i++;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (arg === '--quiet' || arg === '-q') {
|
|
36
|
+
options.quiet = true;
|
|
37
|
+
i++;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (arg === '--changed-only') {
|
|
41
|
+
options.changedOnly = true;
|
|
42
|
+
i++;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (arg === '--pg') {
|
|
46
|
+
i++;
|
|
47
|
+
if (i < args.length) {
|
|
48
|
+
options.pgVersion = parseInt(args[i], 10) || 16;
|
|
49
|
+
}
|
|
50
|
+
i++;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (arg.startsWith('--pg=')) {
|
|
54
|
+
options.pgVersion = parseInt(arg.slice(5), 10) || 16;
|
|
55
|
+
i++;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (arg === '--format') {
|
|
59
|
+
i++;
|
|
60
|
+
if (i < args.length) {
|
|
61
|
+
const fmt = args[i].toLowerCase();
|
|
62
|
+
if (fmt === 'json' || fmt === 'markdown' || fmt === 'terminal') {
|
|
63
|
+
options.format = fmt;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
i++;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (arg.startsWith('--format=')) {
|
|
70
|
+
const fmt = arg.slice(9).toLowerCase();
|
|
71
|
+
if (fmt === 'json' || fmt === 'markdown' || fmt === 'terminal') {
|
|
72
|
+
options.format = fmt;
|
|
73
|
+
}
|
|
74
|
+
i++;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
// Positional target
|
|
78
|
+
if (!arg.startsWith('-')) {
|
|
79
|
+
options.targets.push(arg);
|
|
80
|
+
}
|
|
81
|
+
i++;
|
|
82
|
+
}
|
|
83
|
+
return options;
|
|
84
|
+
}
|
|
85
|
+
export function printHelp() {
|
|
86
|
+
console.log(`
|
|
87
|
+
ddlforge v${VERSION} — Ultra-fast, zero-dependency Postgres migration lock linter
|
|
88
|
+
|
|
89
|
+
USAGE:
|
|
90
|
+
ddlforge [paths...] [flags]
|
|
91
|
+
|
|
92
|
+
ARGUMENTS:
|
|
93
|
+
paths Target migration files or directories (e.g. ./prisma/migrations, ./drizzle)
|
|
94
|
+
|
|
95
|
+
FLAGS:
|
|
96
|
+
--pg <version> Target PostgreSQL version (default: 16)
|
|
97
|
+
--format <type> Output format: terminal | json | markdown (default: terminal)
|
|
98
|
+
--quiet, -q Suppress advisories/warnings and emit blockers only
|
|
99
|
+
--changed-only Use git diff to lint only staged or branch-modified migration files
|
|
100
|
+
--version, -v Print ddlforge version and exit
|
|
101
|
+
--help, -h Print this help message and exit
|
|
102
|
+
|
|
103
|
+
EXAMPLES:
|
|
104
|
+
$ ddlforge ./prisma/migrations
|
|
105
|
+
$ ddlforge ./drizzle --pg 14 --format json
|
|
106
|
+
$ ddlforge --changed-only
|
|
107
|
+
$ ddlforge ./migrations/001_add_index.sql
|
|
108
|
+
`);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Finds all .sql files from input paths, directories, or defaults.
|
|
112
|
+
*/
|
|
113
|
+
export function discoverSqlFiles(targets, changedOnly, cwd = process.cwd()) {
|
|
114
|
+
if (changedOnly) {
|
|
115
|
+
return getChangedSqlFiles(cwd);
|
|
116
|
+
}
|
|
117
|
+
const filesToScan = [];
|
|
118
|
+
const candidateTargets = targets.length > 0
|
|
119
|
+
? targets
|
|
120
|
+
: findDefaultMigrationTargets(cwd);
|
|
121
|
+
for (const target of candidateTargets) {
|
|
122
|
+
const fullPath = path.isAbsolute(target) ? target : path.resolve(cwd, target);
|
|
123
|
+
if (!fs.existsSync(fullPath))
|
|
124
|
+
continue;
|
|
125
|
+
const stat = fs.statSync(fullPath);
|
|
126
|
+
if (stat.isDirectory()) {
|
|
127
|
+
collectSqlFromDir(fullPath, filesToScan);
|
|
128
|
+
}
|
|
129
|
+
else if (stat.isFile() && (fullPath.endsWith('.sql') || targets.length > 0)) {
|
|
130
|
+
filesToScan.push(fullPath);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return filesToScan;
|
|
134
|
+
}
|
|
135
|
+
function findDefaultMigrationTargets(cwd) {
|
|
136
|
+
const commonDirs = [
|
|
137
|
+
'prisma/migrations',
|
|
138
|
+
'drizzle',
|
|
139
|
+
'migrations',
|
|
140
|
+
'sql',
|
|
141
|
+
];
|
|
142
|
+
const found = [];
|
|
143
|
+
for (const dir of commonDirs) {
|
|
144
|
+
const p = path.resolve(cwd, dir);
|
|
145
|
+
if (fs.existsSync(p) && fs.statSync(p).isDirectory()) {
|
|
146
|
+
found.push(p);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return found.length > 0 ? found : [cwd];
|
|
150
|
+
}
|
|
151
|
+
function collectSqlFromDir(dir, accumulator) {
|
|
152
|
+
const IGNORED = new Set(['node_modules', '.git', 'dist', 'build', '.next', '.turbo']);
|
|
153
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
154
|
+
for (const entry of entries) {
|
|
155
|
+
if (entry.isDirectory()) {
|
|
156
|
+
if (!IGNORED.has(entry.name)) {
|
|
157
|
+
collectSqlFromDir(path.join(dir, entry.name), accumulator);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
else if (entry.isFile() && entry.name.endsWith('.sql')) {
|
|
161
|
+
accumulator.push(path.join(dir, entry.name));
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function getChangedSqlFiles(cwd) {
|
|
166
|
+
const resultFiles = new Set();
|
|
167
|
+
// 1. Try git diff --name-only HEAD (staged and unstaged against HEAD)
|
|
168
|
+
try {
|
|
169
|
+
const diffHead = execSync('git diff --name-only HEAD', { cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
170
|
+
diffHead
|
|
171
|
+
.split(/\r?\n/)
|
|
172
|
+
.map((f) => f.trim())
|
|
173
|
+
.filter((f) => f.endsWith('.sql') && f.length > 0)
|
|
174
|
+
.forEach((f) => {
|
|
175
|
+
const full = path.resolve(cwd, f);
|
|
176
|
+
if (fs.existsSync(full))
|
|
177
|
+
resultFiles.add(full);
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
// If no commits yet / no HEAD commit
|
|
182
|
+
try {
|
|
183
|
+
const unstaged = execSync('git diff --name-only', { cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
184
|
+
unstaged
|
|
185
|
+
.split(/\r?\n/)
|
|
186
|
+
.map((f) => f.trim())
|
|
187
|
+
.filter((f) => f.endsWith('.sql') && f.length > 0)
|
|
188
|
+
.forEach((f) => {
|
|
189
|
+
const full = path.resolve(cwd, f);
|
|
190
|
+
if (fs.existsSync(full))
|
|
191
|
+
resultFiles.add(full);
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
catch { }
|
|
195
|
+
try {
|
|
196
|
+
const staged = execSync('git diff --name-only --cached', { cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
197
|
+
staged
|
|
198
|
+
.split(/\r?\n/)
|
|
199
|
+
.map((f) => f.trim())
|
|
200
|
+
.filter((f) => f.endsWith('.sql') && f.length > 0)
|
|
201
|
+
.forEach((f) => {
|
|
202
|
+
const full = path.resolve(cwd, f);
|
|
203
|
+
if (fs.existsSync(full))
|
|
204
|
+
resultFiles.add(full);
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
catch { }
|
|
208
|
+
}
|
|
209
|
+
return Array.from(resultFiles);
|
|
210
|
+
}
|
|
211
|
+
export async function runCli(argv = process.argv.slice(2)) {
|
|
212
|
+
const options = parseArgs(argv);
|
|
213
|
+
if (options.help) {
|
|
214
|
+
printHelp();
|
|
215
|
+
return 0;
|
|
216
|
+
}
|
|
217
|
+
if (options.version) {
|
|
218
|
+
console.log(`ddlforge v${VERSION}`);
|
|
219
|
+
return 0;
|
|
220
|
+
}
|
|
221
|
+
const files = discoverSqlFiles(options.targets, options.changedOnly);
|
|
222
|
+
if (files.length === 0) {
|
|
223
|
+
if (options.format === 'json') {
|
|
224
|
+
console.log(formatJson([]));
|
|
225
|
+
}
|
|
226
|
+
else if (options.format === 'markdown') {
|
|
227
|
+
console.log(formatMarkdown([]));
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
console.log('No migration .sql files found to analyze.');
|
|
231
|
+
}
|
|
232
|
+
return 0;
|
|
233
|
+
}
|
|
234
|
+
const analyzer = new MigrationAnalyzer();
|
|
235
|
+
const results = [];
|
|
236
|
+
for (const file of files) {
|
|
237
|
+
try {
|
|
238
|
+
const content = fs.readFileSync(file, 'utf-8');
|
|
239
|
+
const res = analyzer.analyze(content, {
|
|
240
|
+
filePath: file,
|
|
241
|
+
pgVersion: options.pgVersion,
|
|
242
|
+
});
|
|
243
|
+
results.push(res);
|
|
244
|
+
}
|
|
245
|
+
catch (err) {
|
|
246
|
+
console.error(`Error reading ${file}: ${err?.message ?? String(err)}`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
// Format and output results
|
|
250
|
+
if (options.format === 'json') {
|
|
251
|
+
console.log(formatJson(results));
|
|
252
|
+
}
|
|
253
|
+
else if (options.format === 'markdown') {
|
|
254
|
+
console.log(formatMarkdown(results));
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
console.log(formatTerminal(results, { quiet: options.quiet }));
|
|
258
|
+
}
|
|
259
|
+
const hasBlockers = results.some(r => r.hasBlockers);
|
|
260
|
+
return hasBlockers ? 1 : 0;
|
|
261
|
+
}
|
|
262
|
+
//# sourceMappingURL=cli.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../../src/cli.ts"],"names":[],"mappings":"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"}
|