@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.
@@ -3,79 +3,81 @@ paths:
3
3
  - "schema/**"
4
4
  ---
5
5
 
6
- # Reglas para trabajar en schema/
6
+ # Rules for working in schema/
7
7
 
8
- ## Regla fundamental de rutas en init.sql
8
+ ## Fundamental rule for paths in init.sql
9
9
 
10
- **Todas las rutas en todos los `init.sql` deben ser absolutas desde la raíz del proyecto.**
10
+ **All paths in every `init.sql` must be absolute from the project root.**
11
11
 
12
- psql resuelve los `\i` siempre desde su directorio de trabajo (CWD), no desde el directorio del archivo que se está leyendo. `build.py` también resuelve desde la raíz del proyecto (`ROOT_DIR`). Los dos se comportan igual — por eso todas las rutas deben ser desde la raíz.
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
- -- CORRECTOruta desde la raíz
17
+ -- CORRECTpath from the root
16
18
  \i schema/auth/roles/create.sql
17
19
  \i schema/auth/roles/data.sql
18
20
 
19
- -- INCORRECTOruta relativa al archivo
21
+ -- WRONGpath relative to the file
20
22
  \i create.sql
21
23
  \i ../roles/create.sql
22
24
  ```
23
25
 
24
- El usuario siempre debe estar parado en la raíz del proyecto al usar psql:
26
+ The user must always be standing at the project root when using psql:
25
27
  ```
26
- \cd /ruta/al/proyecto
28
+ \cd /path/to/project
27
29
  \i init.sql
28
30
  ```
29
31
 
30
32
  ---
31
33
 
32
- ## Cómo agregar una tabla standalone
34
+ ## How to add a standalone table
33
35
 
34
36
  ```bash
35
- mkdir schema/mi-tabla
36
- touch schema/mi-tabla/create.sql schema/mi-tabla/data.sql schema/mi-tabla/init.sql
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` con columnas, constraints y FKs
40
- - `data.sql` → INSERTs de datos iniciales (roles, estados, catálogos). Si no hay, dejarlo comentadoel archivo debe existir igual
41
- - `init.sql` → rutas completas desde la raíz del proyecto:
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
- -- sin triggers
45
- \i schema/mi-tabla/create.sql
46
- \i schema/mi-tabla/data.sql
47
-
48
- -- con triggers
49
- \i schema/mi-tabla/create.sql
50
- \i schema/mi-tabla/data.sql
51
- \i schema/mi-tabla/triggers.sql
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
- - Agregar `\i schema/mi-tabla/init.sql` a `schema/init.sql` en la posición correcta
56
+ - Add `\i schema/my-table/init.sql` to `schema/init.sql` in the correct position
55
57
 
56
- ## Cómo agregar un grupo
58
+ ## How to add a group
57
59
 
58
60
  ```bash
59
- mkdir schema/mi-grupo
60
- touch schema/mi-grupo/init.sql
61
+ mkdir schema/my-group
62
+ touch schema/my-group/init.sql
61
63
  ```
62
64
 
63
- El `init.sql` del grupo lista sus tablas con rutas completas desde la raíz: `\i schema/mi-grupo/mi-tabla/init.sql`. Agregar `\i schema/mi-grupo/init.sql` a `schema/init.sql`.
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
- ## Cuándo crear un grupo vs standalone
67
+ ## When to create a group vs. standalone
66
68
 
67
- | Standalone | Grupo |
69
+ | Standalone | Group |
68
70
  |---|---|
69
- | Tabla transversal a varios dominios (`parameters`, `tags`, `files`) | 2+ tablas del mismo dominio funcional |
70
- | Una sola tabla en ese dominio | Tablas que solo tienen sentido juntas |
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
- ## Lógica de agrupación por dominio
74
+ ## Domain grouping logic
73
75
 
74
- Agrupar por **responsabilidad funcional**, no por conveniencia técnica.
76
+ Group by **functional responsibility**, not technical convenience.
75
77
 
76
- Grupos comunes:
78
+ Common groups:
77
79
 
78
- | Grupo | Tablas típicas |
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
- Una tabla que tiene FK hacia tablas de **dos grupos distintos** probablemente va standalone, después de ambos grupos en `schema/init.sql`.
88
+ A table with FKs into **two different groups** probably goes standalone, after both groups in `schema/init.sql`.
87
89
 
88
- ## Formato estándar de CREATE TABLE
90
+ ## Standard CREATE TABLE format
89
91
 
90
- Este es el formato propio del proyecto. Toda tabla nueva debe seguirlo sin excepción.
92
+ This is the project's own format. Every new table must follow it without exception.
91
93
 
92
- ### Orden de columnas (de arriba hacia abajo)
94
+ ### Column order (top to bottom)
93
95
 
94
96
  ```
95
- 1. PK → primera columna, siempre
96
- 2. FKs → en columnas simples, sin referenciar aún, después del PK
97
- 3. Atributos campos propios de la tabla en orden lógico de importancia
98
- 4. Timestamps → created_at y updated_at al final, siempre TIMESTAMPTZ
99
- 5. Constraints → PRIMARY KEY primero, luego cada FOREIGN KEY con REFERENCES
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
- - **Tablas**: `snake_case`, **plural** — representan una colección de registros (`users`, `order_items`)
105
- - **PK**: nombre de la tabla en singular + `_id` → `user_id`, `order_id`, `role_id`
106
- - Si el nombre es largo, usar una versión simplificada que mantenga el contexto
107
- - **FK columns**: mismo formato que el PK de la tabla que referencian → `role_id`, `created_by`
108
- - Índices: `idx_{tabla}_{columna}` (`idx_users_email`)
109
- - Triggers: `trg_{tabla}_{descripcion}`
110
- - Funciones de trigger: `fn_{tabla}_{descripcion}`
111
- - Constraints únicos: `uq_{tabla}_{columna}` (`uq_users_email`)
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
- ### Ejemplo de referencia
115
+ ### Reference example
114
116
 
115
117
  ```sql
116
118
  CREATE TABLE users (
117
119
  user_id SERIAL,
118
- role_id INT NOT NULL, -- rol asignado al usuario
119
- created_by INT, -- usuario que lo creó (NULL solo para root)
120
- full_name VARCHAR(150) NOT NULL, -- nombre completo del usuario
121
- username VARCHAR(50) NOT NULL UNIQUE, -- nombre de usuario único
122
- email VARCHAR(150), -- correo electrónico
123
- password_hash VARCHAR(255) NOT NULL, -- contraseña almacenada con hash seguro
124
- failed_attempts SMALLINT NOT NULL DEFAULT 0, -- contador de intentos de login fallidos
125
- active BOOLEAN NOT NULL DEFAULT TRUE, -- cuenta activa/inactiva (no se elimina)
126
- requires_pwd_change BOOLEAN NOT NULL DEFAULT TRUE, -- fuerza cambio de contraseña en primer ingreso
127
- locked_until TIMESTAMPTZ, -- fecha y hora hasta cuando la cuenta está bloqueada
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
- ### Reglas del formato
138
+ ### Format rules
137
139
 
138
- - **PK se declara como columna simple** (`SERIAL`) sin `PRIMARY KEY` inline la constraint va al final
139
- - **FKs se declaran como columnas simples** (`INT`, `INT NOT NULL`) sin `REFERENCES` inline la constraint va al final
140
- - **Alineación visual**: columna, tipo, constraints y comentario alineados con espacios para legibilidad
141
- - **Comentarios inline** (`--`) en toda columna que no sea autoexplicativa por su nombre
142
- - **`NOT NULL` explícito** en toda columna que no admite nulos nunca asumir por defecto
143
- - **Timestamps**: siempre `TIMESTAMPTZ` (con zona horaria), nunca `TIMESTAMP` a secas
144
- - **Orden de constraints al final**: primero `PRIMARY KEY`, luego `FOREIGN KEY` en el mismo orden que aparecen las columnas FK arriba
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 nullsnever 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
- ### Tipos de datos
148
+ ### Data types
147
149
 
148
- | Dato | Tipo |
150
+ | Data | Type |
149
151
  |---|---|
150
- | PK autoincremental | `SERIAL` |
151
- | FK, referencia a otra tabla | `INT` |
152
- | Texto con longitud conocida | `VARCHAR(N)` |
153
- | Texto sin límite | `TEXT` |
154
- | Contador pequeño | `SMALLINT` |
155
- | Número entero general | `INT` |
156
- | Dinero / precisión decimal | `NUMERIC(10,2)` — nunca `FLOAT` |
157
- | Booleano | `BOOLEAN` — nunca `INT` ni `CHAR` |
158
- | Solo fecha | `DATE` |
159
- | Fecha + hora + zona | `TIMESTAMPTZ` — siempre esta, nunca `TIMESTAMP` a secas |
160
- | Enumeraciones | tabla de catálogo con FK — nunca `ENUM` de Postgres (difícil de migrar) |
161
-
162
- ### Columnas de auditoría
163
-
164
- Toda tabla de datos (no catálogo puro) cierra con:
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
- El `updated_at` se mantiene con un trigger en `triggers.sql`.
171
- Si aplica borrado lógico, agregar `deleted_at TIMESTAMPTZ` antes de `created_at`.
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
- ### Integridad referencial
175
+ ### Referential integrity
174
176
 
175
- Elegir `ON DELETE` conscientemente en cada FK:
177
+ Choose `ON DELETE` deliberately on every FK:
176
178
 
177
- - `CASCADE` → el hijo no tiene sentido sin el padre (`order_items` → `orders`)
178
- - `SET NULL` → la relación es opcional (la columna FK debe admitir NULL)
179
- - Sin cláusula (default `RESTRICT`) → el borrado del padre se bloquea explícitamente
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
- ### Índices
183
+ ### Indexes
182
184
 
183
- - La PK ya crea índice automáticamente
184
- - Crear índices en columnas frecuentes en `WHERE`, `JOIN ON`, `ORDER BY`
185
- - Índice compuesto cuando las queries filtran por dos columnas juntas
186
- - No sobre-indexar: cada índice tiene costo en escritura
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
- Solo crear `triggers.sql` si la tabla realmente lo necesita.
191
- Siempre usar `CREATE OR REPLACE FUNCTION` para que sea re-ejecutable.
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
- ## Diccionario de tabla (DICTIONARY.md)
197
+ ## Table dictionary (DICTIONARY.md)
196
198
 
197
- Cada carpeta de tabla debe tener un `DICTIONARY.md` junto a `create.sql`, `data.sql` e `init.sql`.
199
+ Every table folder must have a `DICTIONARY.md` alongside `create.sql`, `data.sql`, and `init.sql`.
198
200
 
199
- **Cuándo generarlo**: al crear una tabla nueva o al recibir un `create.sql` existente.
200
- Si trabajo en una tabla que no tiene su `DICTIONARY.md`, preguntar si debo generarlo.
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
- ### Plantilla
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
- # {nombre_tabla}
210
+ # {table_name}
206
211
 
207
- {Descripción en 1-2 líneas: qué representa esta tabla en el dominio del negocio.}
212
+ {1-2 line description: what this table represents in the business domain.}
208
213
 
209
- ## Columnas
214
+ ## Columns
210
215
 
211
- | Columna | Tipo | Nulo | Default | Descripción |
212
- |---------|------|------|---------|-------------|
213
- | {col} | {tipo} | NO/SÍ | {default o —} | {descripción en lenguaje de negocio} |
216
+ | Column | Type | Nullable | Default | Description |
217
+ |--------|------|----------|---------|-------------|
218
+ | {col} | {type} | YES/NO | {default or —} | {description in business language} |
214
219
 
215
- ## Relaciones
220
+ ## Relationships
216
221
 
217
- | Columna | Referencia | Descripción |
218
- |---------|------------|-------------|
219
- | {fk_col} | {tabla.col} | {qué representa esta relación en el negocio} |
222
+ | Column | Reference | Description |
223
+ |--------|-----------|--------------|
224
+ | {fk_col} | {table.col} | {what this relationship means in the business} |
220
225
 
221
- ## Reglas de negocio
226
+ ## Business rules
222
227
 
223
- - {regla o restricción relevante para el negocio}
224
- - {caso especial o comportamiento esperado}
228
+ - {rule or constraint relevant to the business}
229
+ - {special case or expected behavior}
225
230
 
226
- ## Notas técnicas
231
+ ## Technical notes
227
232
 
228
- - {constraints, triggers, índices relevantes que no son obvios del create.sql}
233
+ - {constraints, triggers, indexes relevant that aren't obvious from create.sql}
229
234
  ```
230
235
 
231
- ### Reglas del diccionario
236
+ ### Dictionary rules
232
237
 
233
- - La descripción del header explica **qué representa** la tabla, no cómo está implementada
234
- - Columnas: descripciones en **lenguaje de negocio**, no técnico ("Rol del usuario" no "FK a roles")
235
- - Relaciones: solo las FK; explicar el **significado de la relación**, no solo el dato técnico
236
- - Reglas de negocio: comportamiento esperado, casos borde, qué operaciones están prohibidas
237
- - Notas técnicas: solo lo que no es visible a simple vista en el `create.sql` (triggers, índices, constraints nombrados)
238
- - Si una sección no aplica (ej. tabla sin FKsin sección Relaciones), omitirla completamente
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
@@ -1,6 +1,6 @@
1
1
  -- ==========================================================
2
- -- RESET COMPLETO elimina todo y recarga desde schema/
3
- -- Usar solo en entornos vacíos o de desarrollo.
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
- -- Rutas siempre desde la raíz del proyecto. Dependencias primero.
3
+ -- Paths are always from the project root. Dependencies first.
4
4
  -- ==========================================================
5
5
 
6
- -- Tabla standalone (sin grupo):
7
- -- \i schema/mi-tabla/init.sql
6
+ -- Standalone table (no group):
7
+ -- \i schema/my-table/init.sql
8
8
 
9
- -- Grupo de tablas:
10
- -- \i schema/mi-grupo/init.sql
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 _migrations (
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 _migrations ORDER BY migration"
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
- for (const name of pending) {
79
- const upPath = path.join(MIGRATIONS_DIR, name, "up.sql");
80
- const sql = fs.readFileSync(upPath, "utf-8");
81
- console.log(`Applying: ${name}`);
82
- await client.query("BEGIN");
83
- try {
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 _migrations (migration) VALUES ($1)", [name]);
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 = '_migrations') AS exists"
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 _migrations ORDER BY migration DESC LIMIT $1",
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
- for (const { migration } of result.rows) {
64
- const downPath = path.join(MIGRATIONS_DIR, migration, "down.sql");
65
- if (!fs.existsSync(downPath)) {
66
- console.error(`No down.sql for migration: ${migration}`);
67
- await client.end();
68
- process.exit(1);
69
- }
70
- const sql = fs.readFileSync(downPath, "utf-8");
71
- console.log(`Reverting: ${migration}`);
72
- await client.query("BEGIN");
73
- try {
74
- await client.query("DELETE FROM _migrations WHERE migration = $1", [migration]);
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).`);