@darkj/create-db 1.0.0 → 1.1.1
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 +24 -5
- package/dist/builders/readme.js +87 -0
- package/dist/generator.js +17 -4
- package/dist/index.js +18 -1
- package/package.json +34 -11
- package/templates/base/.claude/CLAUDE.md +44 -35
- package/templates/base/.claude/commands/analyze.md +15 -15
- package/templates/base/.claude/commands/backup.md +9 -9
- package/templates/base/.claude/commands/build.md +7 -7
- package/templates/base/.claude/commands/new-dictionary.md +29 -26
- package/templates/base/.claude/commands/new-migration.md +33 -32
- package/templates/base/.claude/rules/migrations.md +65 -58
- package/templates/base/.claude/rules/schema.md +134 -129
- package/templates/base/init.sql +2 -2
- package/templates/base/schema/init.sql +5 -5
- package/templates/base/scripts/migrate.ts +18 -16
- package/templates/base/scripts/rollback.ts +21 -21
- package/templates/base/README.md +0 -117
|
@@ -3,79 +3,81 @@ paths:
|
|
|
3
3
|
- "schema/**"
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
#
|
|
6
|
+
# Rules for working in schema/
|
|
7
7
|
|
|
8
|
-
##
|
|
8
|
+
## Fundamental rule for paths in init.sql
|
|
9
9
|
|
|
10
|
-
**
|
|
10
|
+
**All paths in every `init.sql` must be absolute from the project root.**
|
|
11
11
|
|
|
12
|
-
psql
|
|
12
|
+
psql always resolves `\i` from its working directory (CWD), not from the directory of the file
|
|
13
|
+
being read. `build.ts` also resolves from the project root (`ROOT_DIR`). Both behave the same way
|
|
14
|
+
— that's why every path must be from the root.
|
|
13
15
|
|
|
14
16
|
```sql
|
|
15
|
-
--
|
|
17
|
+
-- CORRECT — path from the root
|
|
16
18
|
\i schema/auth/roles/create.sql
|
|
17
19
|
\i schema/auth/roles/data.sql
|
|
18
20
|
|
|
19
|
-
--
|
|
21
|
+
-- WRONG — path relative to the file
|
|
20
22
|
\i create.sql
|
|
21
23
|
\i ../roles/create.sql
|
|
22
24
|
```
|
|
23
25
|
|
|
24
|
-
|
|
26
|
+
The user must always be standing at the project root when using psql:
|
|
25
27
|
```
|
|
26
|
-
\cd /
|
|
28
|
+
\cd /path/to/project
|
|
27
29
|
\i init.sql
|
|
28
30
|
```
|
|
29
31
|
|
|
30
32
|
---
|
|
31
33
|
|
|
32
|
-
##
|
|
34
|
+
## How to add a standalone table
|
|
33
35
|
|
|
34
36
|
```bash
|
|
35
|
-
mkdir schema/
|
|
36
|
-
touch schema/
|
|
37
|
+
mkdir schema/my-table
|
|
38
|
+
touch schema/my-table/create.sql schema/my-table/data.sql schema/my-table/init.sql
|
|
37
39
|
```
|
|
38
40
|
|
|
39
|
-
- `create.sql` → `CREATE TABLE`
|
|
40
|
-
- `data.sql` → INSERTs
|
|
41
|
-
- `init.sql` →
|
|
41
|
+
- `create.sql` → `CREATE TABLE` with columns, constraints, and FKs
|
|
42
|
+
- `data.sql` → INSERTs for initial data (roles, statuses, catalogs). If there's none, leave it commented out — the file must still exist
|
|
43
|
+
- `init.sql` → full paths from the project root:
|
|
42
44
|
|
|
43
45
|
```sql
|
|
44
|
-
--
|
|
45
|
-
\i schema/
|
|
46
|
-
\i schema/
|
|
47
|
-
|
|
48
|
-
--
|
|
49
|
-
\i schema/
|
|
50
|
-
\i schema/
|
|
51
|
-
\i schema/
|
|
46
|
+
-- without triggers
|
|
47
|
+
\i schema/my-table/create.sql
|
|
48
|
+
\i schema/my-table/data.sql
|
|
49
|
+
|
|
50
|
+
-- with triggers
|
|
51
|
+
\i schema/my-table/create.sql
|
|
52
|
+
\i schema/my-table/data.sql
|
|
53
|
+
\i schema/my-table/triggers.sql
|
|
52
54
|
```
|
|
53
55
|
|
|
54
|
-
-
|
|
56
|
+
- Add `\i schema/my-table/init.sql` to `schema/init.sql` in the correct position
|
|
55
57
|
|
|
56
|
-
##
|
|
58
|
+
## How to add a group
|
|
57
59
|
|
|
58
60
|
```bash
|
|
59
|
-
mkdir schema/
|
|
60
|
-
touch schema/
|
|
61
|
+
mkdir schema/my-group
|
|
62
|
+
touch schema/my-group/init.sql
|
|
61
63
|
```
|
|
62
64
|
|
|
63
|
-
|
|
65
|
+
The group's `init.sql` lists its tables with full paths from the root: `\i schema/my-group/my-table/init.sql`. Add `\i schema/my-group/init.sql` to `schema/init.sql`.
|
|
64
66
|
|
|
65
|
-
##
|
|
67
|
+
## When to create a group vs. standalone
|
|
66
68
|
|
|
67
|
-
| Standalone |
|
|
69
|
+
| Standalone | Group |
|
|
68
70
|
|---|---|
|
|
69
|
-
|
|
|
70
|
-
|
|
|
71
|
+
| Table shared across several domains (`parameters`, `tags`, `files`) | 2+ tables of the same functional domain |
|
|
72
|
+
| A single table in that domain | Tables that only make sense together |
|
|
71
73
|
|
|
72
|
-
##
|
|
74
|
+
## Domain grouping logic
|
|
73
75
|
|
|
74
|
-
|
|
76
|
+
Group by **functional responsibility**, not technical convenience.
|
|
75
77
|
|
|
76
|
-
|
|
78
|
+
Common groups:
|
|
77
79
|
|
|
78
|
-
|
|
|
80
|
+
| Group | Typical tables |
|
|
79
81
|
|---|---|
|
|
80
82
|
| `auth` | `users`, `roles`, `permissions`, `sessions` |
|
|
81
83
|
| `catalog` | `products`, `categories`, `brands` |
|
|
@@ -83,48 +85,48 @@ Grupos comunes:
|
|
|
83
85
|
| `payments` | `payments`, `payment_methods`, `invoices` |
|
|
84
86
|
| `notifications` | `notifications`, `notification_types` |
|
|
85
87
|
|
|
86
|
-
|
|
88
|
+
A table with FKs into **two different groups** probably goes standalone, after both groups in `schema/init.sql`.
|
|
87
89
|
|
|
88
|
-
##
|
|
90
|
+
## Standard CREATE TABLE format
|
|
89
91
|
|
|
90
|
-
|
|
92
|
+
This is the project's own format. Every new table must follow it without exception.
|
|
91
93
|
|
|
92
|
-
###
|
|
94
|
+
### Column order (top to bottom)
|
|
93
95
|
|
|
94
96
|
```
|
|
95
|
-
1. PK →
|
|
96
|
-
2. FKs →
|
|
97
|
-
3.
|
|
98
|
-
4. Timestamps → created_at
|
|
99
|
-
5. Constraints → PRIMARY KEY
|
|
97
|
+
1. PK → first column, always
|
|
98
|
+
2. FKs → as plain columns, not yet referencing anything, right after the PK
|
|
99
|
+
3. Attributes → the table's own fields in logical order of importance
|
|
100
|
+
4. Timestamps → created_at and updated_at at the end, always TIMESTAMPTZ
|
|
101
|
+
5. Constraints → PRIMARY KEY first, then each FOREIGN KEY with REFERENCES
|
|
100
102
|
```
|
|
101
103
|
|
|
102
104
|
### Naming
|
|
103
105
|
|
|
104
|
-
- **
|
|
105
|
-
- **PK**:
|
|
106
|
-
-
|
|
107
|
-
- **FK columns**:
|
|
108
|
-
-
|
|
109
|
-
- Triggers: `trg_{
|
|
110
|
-
-
|
|
111
|
-
-
|
|
106
|
+
- **Tables**: `snake_case`, **plural** — they represent a collection of records (`users`, `order_items`)
|
|
107
|
+
- **PK**: singular table name + `_id` → `user_id`, `order_id`, `role_id`
|
|
108
|
+
- If the name is long, use a shortened version that keeps the context
|
|
109
|
+
- **FK columns**: same format as the PK of the table they reference → `role_id`, `created_by`
|
|
110
|
+
- Indexes: `idx_{table}_{column}` (`idx_users_email`)
|
|
111
|
+
- Triggers: `trg_{table}_{description}`
|
|
112
|
+
- Trigger functions: `fn_{table}_{description}`
|
|
113
|
+
- Unique constraints: `uq_{table}_{column}` (`uq_users_email`)
|
|
112
114
|
|
|
113
|
-
###
|
|
115
|
+
### Reference example
|
|
114
116
|
|
|
115
117
|
```sql
|
|
116
118
|
CREATE TABLE users (
|
|
117
119
|
user_id SERIAL,
|
|
118
|
-
role_id INT NOT NULL, --
|
|
119
|
-
created_by INT, --
|
|
120
|
-
full_name VARCHAR(150) NOT NULL, --
|
|
121
|
-
username VARCHAR(50) NOT NULL UNIQUE, --
|
|
122
|
-
email VARCHAR(150), --
|
|
123
|
-
password_hash VARCHAR(255) NOT NULL, --
|
|
124
|
-
failed_attempts SMALLINT NOT NULL DEFAULT 0, --
|
|
125
|
-
active BOOLEAN NOT NULL DEFAULT TRUE, --
|
|
126
|
-
requires_pwd_change BOOLEAN NOT NULL DEFAULT TRUE, --
|
|
127
|
-
locked_until TIMESTAMPTZ, --
|
|
120
|
+
role_id INT NOT NULL, -- role assigned to the user
|
|
121
|
+
created_by INT, -- user who created it (NULL only for root)
|
|
122
|
+
full_name VARCHAR(150) NOT NULL, -- user's full name
|
|
123
|
+
username VARCHAR(50) NOT NULL UNIQUE, -- unique username
|
|
124
|
+
email VARCHAR(150), -- email address
|
|
125
|
+
password_hash VARCHAR(255) NOT NULL, -- password stored with a secure hash
|
|
126
|
+
failed_attempts SMALLINT NOT NULL DEFAULT 0, -- failed login attempt counter
|
|
127
|
+
active BOOLEAN NOT NULL DEFAULT TRUE, -- active/inactive account (never deleted)
|
|
128
|
+
requires_pwd_change BOOLEAN NOT NULL DEFAULT TRUE, -- forces a password change on first login
|
|
129
|
+
locked_until TIMESTAMPTZ, -- date and time until the account is locked
|
|
128
130
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
129
131
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
130
132
|
PRIMARY KEY (user_id),
|
|
@@ -133,106 +135,109 @@ CREATE TABLE users (
|
|
|
133
135
|
);
|
|
134
136
|
```
|
|
135
137
|
|
|
136
|
-
###
|
|
138
|
+
### Format rules
|
|
137
139
|
|
|
138
|
-
- **PK
|
|
139
|
-
- **FKs
|
|
140
|
-
- **
|
|
141
|
-
- **
|
|
142
|
-
-
|
|
143
|
-
- **Timestamps**:
|
|
144
|
-
- **
|
|
140
|
+
- **The PK is declared as a plain column** (`SERIAL`) without an inline `PRIMARY KEY` — the constraint goes at the end
|
|
141
|
+
- **FKs are declared as plain columns** (`INT`, `INT NOT NULL`) without an inline `REFERENCES` — the constraint goes at the end
|
|
142
|
+
- **Visual alignment**: column, type, constraints, and comment aligned with spaces for readability
|
|
143
|
+
- **Inline comments** (`--`) on every column that isn't self-explanatory from its name
|
|
144
|
+
- **Explicit `NOT NULL`** on every column that doesn't allow nulls — never assume the default
|
|
145
|
+
- **Timestamps**: always `TIMESTAMPTZ` (with timezone), never bare `TIMESTAMP`
|
|
146
|
+
- **Constraint order at the end**: `PRIMARY KEY` first, then each `FOREIGN KEY` in the same order the FK columns appear above
|
|
145
147
|
|
|
146
|
-
###
|
|
148
|
+
### Data types
|
|
147
149
|
|
|
148
|
-
|
|
|
150
|
+
| Data | Type |
|
|
149
151
|
|---|---|
|
|
150
|
-
| PK
|
|
151
|
-
| FK,
|
|
152
|
-
|
|
|
153
|
-
|
|
|
154
|
-
|
|
|
155
|
-
|
|
|
156
|
-
|
|
|
157
|
-
|
|
|
158
|
-
|
|
|
159
|
-
|
|
|
160
|
-
|
|
|
161
|
-
|
|
162
|
-
###
|
|
163
|
-
|
|
164
|
-
|
|
152
|
+
| Auto-incrementing PK | `SERIAL` |
|
|
153
|
+
| FK, reference to another table | `INT` |
|
|
154
|
+
| Text with a known length | `VARCHAR(N)` |
|
|
155
|
+
| Unbounded text | `TEXT` |
|
|
156
|
+
| Small counter | `SMALLINT` |
|
|
157
|
+
| General integer | `INT` |
|
|
158
|
+
| Money / decimal precision | `NUMERIC(10,2)` — never `FLOAT` |
|
|
159
|
+
| Boolean | `BOOLEAN` — never `INT` or `CHAR` |
|
|
160
|
+
| Date only | `DATE` |
|
|
161
|
+
| Date + time + timezone | `TIMESTAMPTZ` — always this, never bare `TIMESTAMP` |
|
|
162
|
+
| Enumerations | catalog table with FK — never Postgres `ENUM` (hard to migrate) |
|
|
163
|
+
|
|
164
|
+
### Audit columns
|
|
165
|
+
|
|
166
|
+
Every data table (not a pure catalog) closes with:
|
|
165
167
|
```sql
|
|
166
168
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
167
169
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
168
170
|
```
|
|
169
171
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
+
`updated_at` is maintained with a trigger in `triggers.sql`.
|
|
173
|
+
If soft delete applies, add `deleted_at TIMESTAMPTZ` before `created_at`.
|
|
172
174
|
|
|
173
|
-
###
|
|
175
|
+
### Referential integrity
|
|
174
176
|
|
|
175
|
-
|
|
177
|
+
Choose `ON DELETE` deliberately on every FK:
|
|
176
178
|
|
|
177
|
-
- `CASCADE` →
|
|
178
|
-
- `SET NULL` →
|
|
179
|
-
-
|
|
179
|
+
- `CASCADE` → the child makes no sense without the parent (`order_items` → `orders`)
|
|
180
|
+
- `SET NULL` → the relationship is optional (the FK column must allow NULL)
|
|
181
|
+
- No clause (default `RESTRICT`) → deleting the parent is explicitly blocked
|
|
180
182
|
|
|
181
|
-
###
|
|
183
|
+
### Indexes
|
|
182
184
|
|
|
183
|
-
-
|
|
184
|
-
-
|
|
185
|
-
-
|
|
186
|
-
-
|
|
185
|
+
- The PK already creates an index automatically
|
|
186
|
+
- Create indexes on columns frequently used in `WHERE`, `JOIN ON`, `ORDER BY`
|
|
187
|
+
- Composite index when queries filter by two columns together
|
|
188
|
+
- Don't over-index: every index has a write cost
|
|
187
189
|
|
|
188
190
|
### Triggers
|
|
189
191
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
+
Only create `triggers.sql` if the table genuinely needs it.
|
|
193
|
+
Always use `CREATE OR REPLACE FUNCTION` so it's re-runnable.
|
|
192
194
|
|
|
193
195
|
---
|
|
194
196
|
|
|
195
|
-
##
|
|
197
|
+
## Table dictionary (DICTIONARY.md)
|
|
196
198
|
|
|
197
|
-
|
|
199
|
+
Every table folder must have a `DICTIONARY.md` alongside `create.sql`, `data.sql`, and `init.sql`.
|
|
198
200
|
|
|
199
|
-
**
|
|
200
|
-
|
|
201
|
+
**When to generate it**: when creating a new table or receiving an existing `create.sql`.
|
|
202
|
+
If working on a table that doesn't have its `DICTIONARY.md`, ask whether to generate it.
|
|
201
203
|
|
|
202
|
-
|
|
204
|
+
Write its content in the language noted under "Documentation language" in `.claude/CLAUDE.md` —
|
|
205
|
+
this instruction stays in English, but the DICTIONARY.md text itself follows that setting.
|
|
206
|
+
|
|
207
|
+
### Template
|
|
203
208
|
|
|
204
209
|
```markdown
|
|
205
|
-
# {
|
|
210
|
+
# {table_name}
|
|
206
211
|
|
|
207
|
-
{
|
|
212
|
+
{1-2 line description: what this table represents in the business domain.}
|
|
208
213
|
|
|
209
|
-
##
|
|
214
|
+
## Columns
|
|
210
215
|
|
|
211
|
-
|
|
|
212
|
-
|
|
213
|
-
| {col}
|
|
216
|
+
| Column | Type | Nullable | Default | Description |
|
|
217
|
+
|--------|------|----------|---------|-------------|
|
|
218
|
+
| {col} | {type} | YES/NO | {default or —} | {description in business language} |
|
|
214
219
|
|
|
215
|
-
##
|
|
220
|
+
## Relationships
|
|
216
221
|
|
|
217
|
-
|
|
|
218
|
-
|
|
219
|
-
| {fk_col} | {
|
|
222
|
+
| Column | Reference | Description |
|
|
223
|
+
|--------|-----------|--------------|
|
|
224
|
+
| {fk_col} | {table.col} | {what this relationship means in the business} |
|
|
220
225
|
|
|
221
|
-
##
|
|
226
|
+
## Business rules
|
|
222
227
|
|
|
223
|
-
- {
|
|
224
|
-
- {
|
|
228
|
+
- {rule or constraint relevant to the business}
|
|
229
|
+
- {special case or expected behavior}
|
|
225
230
|
|
|
226
|
-
##
|
|
231
|
+
## Technical notes
|
|
227
232
|
|
|
228
|
-
- {constraints, triggers,
|
|
233
|
+
- {constraints, triggers, indexes relevant that aren't obvious from create.sql}
|
|
229
234
|
```
|
|
230
235
|
|
|
231
|
-
###
|
|
236
|
+
### Dictionary rules
|
|
232
237
|
|
|
233
|
-
-
|
|
234
|
-
-
|
|
235
|
-
-
|
|
236
|
-
-
|
|
237
|
-
-
|
|
238
|
-
-
|
|
238
|
+
- The header description explains **what the table represents**, not how it's implemented
|
|
239
|
+
- Columns: descriptions in **business language**, not technical ("User's role" not "FK to roles")
|
|
240
|
+
- Relationships: FKs only; explain the **meaning of the relationship**, not just the technical fact
|
|
241
|
+
- Business rules: expected behavior, edge cases, which operations are forbidden
|
|
242
|
+
- Technical notes: only what isn't obvious at a glance from `create.sql` (triggers, indexes, named constraints)
|
|
243
|
+
- If a section doesn't apply (e.g. a table with no FKs → no Relationships section), omit it entirely
|
package/templates/base/init.sql
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
-- ==========================================================
|
|
2
|
-
-- RESET
|
|
3
|
-
--
|
|
2
|
+
-- FULL RESET — drops everything and reloads from schema/
|
|
3
|
+
-- Use only on empty or development environments.
|
|
4
4
|
-- ==========================================================
|
|
5
5
|
|
|
6
6
|
DO $$ DECLARE
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
-- ==========================================================
|
|
2
2
|
-- SCHEMA LOAD ORDER
|
|
3
|
-
--
|
|
3
|
+
-- Paths are always from the project root. Dependencies first.
|
|
4
4
|
-- ==========================================================
|
|
5
5
|
|
|
6
|
-
--
|
|
7
|
-
-- \i schema/
|
|
6
|
+
-- Standalone table (no group):
|
|
7
|
+
-- \i schema/my-table/init.sql
|
|
8
8
|
|
|
9
|
-
--
|
|
10
|
-
-- \i schema/
|
|
9
|
+
-- Table group:
|
|
10
|
+
-- \i schema/my-group/init.sql
|
|
@@ -24,7 +24,7 @@ function loadEnv(): Record<string, string> {
|
|
|
24
24
|
|
|
25
25
|
async function ensureMigrationsTable(client: Client): Promise<void> {
|
|
26
26
|
await client.query(`
|
|
27
|
-
CREATE TABLE IF NOT EXISTS
|
|
27
|
+
CREATE TABLE IF NOT EXISTS migrations (
|
|
28
28
|
migration VARCHAR(255) PRIMARY KEY,
|
|
29
29
|
applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
30
30
|
)
|
|
@@ -33,7 +33,7 @@ async function ensureMigrationsTable(client: Client): Promise<void> {
|
|
|
33
33
|
|
|
34
34
|
async function appliedMigrations(client: Client): Promise<Set<string>> {
|
|
35
35
|
const result = await client.query<{ migration: string }>(
|
|
36
|
-
"SELECT migration FROM
|
|
36
|
+
"SELECT migration FROM migrations ORDER BY migration"
|
|
37
37
|
);
|
|
38
38
|
return new Set(result.rows.map((r) => r.migration));
|
|
39
39
|
}
|
|
@@ -75,23 +75,25 @@ async function main(): Promise<void> {
|
|
|
75
75
|
return;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
78
|
+
// The whole run is one transaction: if any migration in this batch fails, every
|
|
79
|
+
// migration applied earlier in the same run is rolled back too, not just the failing one.
|
|
80
|
+
await client.query("BEGIN");
|
|
81
|
+
try {
|
|
82
|
+
for (const name of pending) {
|
|
83
|
+
const upPath = path.join(MIGRATIONS_DIR, name, "up.sql");
|
|
84
|
+
const sql = fs.readFileSync(upPath, "utf-8");
|
|
85
|
+
console.log(`Applying: ${name}`);
|
|
84
86
|
await client.query(sql);
|
|
85
|
-
await client.query("INSERT INTO
|
|
86
|
-
await client.query("COMMIT");
|
|
87
|
+
await client.query("INSERT INTO migrations (migration) VALUES ($1)", [name]);
|
|
87
88
|
console.log(` ✓ Done`);
|
|
88
|
-
} catch (err) {
|
|
89
|
-
await client.query("ROLLBACK");
|
|
90
|
-
console.error(` ✗ Failed — rolled back`);
|
|
91
|
-
console.error(err);
|
|
92
|
-
await client.end();
|
|
93
|
-
process.exit(1);
|
|
94
89
|
}
|
|
90
|
+
await client.query("COMMIT");
|
|
91
|
+
} catch (err) {
|
|
92
|
+
await client.query("ROLLBACK");
|
|
93
|
+
console.error(` ✗ Failed — rolled back the whole batch`);
|
|
94
|
+
console.error(err);
|
|
95
|
+
await client.end();
|
|
96
|
+
process.exit(1);
|
|
95
97
|
}
|
|
96
98
|
|
|
97
99
|
console.log(`\nApplied ${pending.length} migration(s).`);
|
|
@@ -41,7 +41,7 @@ async function main(): Promise<void> {
|
|
|
41
41
|
await client.connect();
|
|
42
42
|
|
|
43
43
|
const tableCheck = await client.query<{ exists: boolean }>(
|
|
44
|
-
"SELECT EXISTS (SELECT 1 FROM pg_tables WHERE schemaname = 'public' AND tablename = '
|
|
44
|
+
"SELECT EXISTS (SELECT 1 FROM pg_tables WHERE schemaname = 'public' AND tablename = 'migrations') AS exists"
|
|
45
45
|
);
|
|
46
46
|
if (!tableCheck.rows[0].exists) {
|
|
47
47
|
console.log("No applied migrations to revert.");
|
|
@@ -50,7 +50,7 @@ async function main(): Promise<void> {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
const result = await client.query<{ migration: string }>(
|
|
53
|
-
"SELECT migration FROM
|
|
53
|
+
"SELECT migration FROM migrations ORDER BY migration DESC LIMIT $1",
|
|
54
54
|
[steps]
|
|
55
55
|
);
|
|
56
56
|
|
|
@@ -60,28 +60,28 @@ async function main(): Promise<void> {
|
|
|
60
60
|
return;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
await client.query("DELETE FROM
|
|
63
|
+
// The whole run is one transaction: if any down.sql in this batch fails, every
|
|
64
|
+
// migration reverted earlier in the same run is rolled back too.
|
|
65
|
+
await client.query("BEGIN");
|
|
66
|
+
try {
|
|
67
|
+
for (const { migration } of result.rows) {
|
|
68
|
+
const downPath = path.join(MIGRATIONS_DIR, migration, "down.sql");
|
|
69
|
+
if (!fs.existsSync(downPath)) {
|
|
70
|
+
throw new Error(`No down.sql for migration: ${migration}`);
|
|
71
|
+
}
|
|
72
|
+
const sql = fs.readFileSync(downPath, "utf-8");
|
|
73
|
+
console.log(`Reverting: ${migration}`);
|
|
74
|
+
await client.query("DELETE FROM migrations WHERE migration = $1", [migration]);
|
|
75
75
|
await client.query(sql);
|
|
76
|
-
await client.query("COMMIT");
|
|
77
76
|
console.log(` ✓ Reverted`);
|
|
78
|
-
} catch (err) {
|
|
79
|
-
await client.query("ROLLBACK");
|
|
80
|
-
console.error(` ✗ Failed — rolled back`);
|
|
81
|
-
console.error(err);
|
|
82
|
-
await client.end();
|
|
83
|
-
process.exit(1);
|
|
84
77
|
}
|
|
78
|
+
await client.query("COMMIT");
|
|
79
|
+
} catch (err) {
|
|
80
|
+
await client.query("ROLLBACK");
|
|
81
|
+
console.error(` ✗ Failed — rolled back the whole batch`);
|
|
82
|
+
console.error(err);
|
|
83
|
+
await client.end();
|
|
84
|
+
process.exit(1);
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
console.log(`\nReverted ${result.rows.length} migration(s).`);
|