@c9up/atlas 0.2.5 → 0.2.6

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 (73) hide show
  1. package/db.win32-x64-msvc.node +0 -0
  2. package/dist/BaseEntity.d.ts +40 -27
  3. package/dist/BaseEntity.d.ts.map +1 -1
  4. package/dist/BaseEntity.js +82 -81
  5. package/dist/BaseEntity.js.map +1 -1
  6. package/dist/BaseModel.d.ts +10 -0
  7. package/dist/BaseModel.d.ts.map +1 -1
  8. package/dist/BaseModel.js +10 -0
  9. package/dist/BaseModel.js.map +1 -1
  10. package/dist/BaseRepository.d.ts.map +1 -1
  11. package/dist/BaseRepository.js +31 -5
  12. package/dist/BaseRepository.js.map +1 -1
  13. package/dist/ConnectionManager.d.ts.map +1 -1
  14. package/dist/ConnectionManager.js +22 -1
  15. package/dist/ConnectionManager.js.map +1 -1
  16. package/dist/ModelQuery.d.ts +22 -1
  17. package/dist/ModelQuery.d.ts.map +1 -1
  18. package/dist/ModelQuery.js +43 -20
  19. package/dist/ModelQuery.js.map +1 -1
  20. package/dist/Transaction.d.ts +9 -0
  21. package/dist/Transaction.d.ts.map +1 -1
  22. package/dist/Transaction.js +14 -0
  23. package/dist/Transaction.js.map +1 -1
  24. package/dist/adapters/NapiDbAdapter.d.ts.map +1 -1
  25. package/dist/adapters/NapiDbAdapter.js +7 -0
  26. package/dist/adapters/NapiDbAdapter.js.map +1 -1
  27. package/dist/decorators/entity.d.ts +4 -0
  28. package/dist/decorators/entity.d.ts.map +1 -1
  29. package/dist/decorators/entity.js +9 -1
  30. package/dist/decorators/entity.js.map +1 -1
  31. package/dist/decorators/hooks.d.ts +28 -0
  32. package/dist/decorators/hooks.d.ts.map +1 -1
  33. package/dist/decorators/hooks.js +43 -0
  34. package/dist/decorators/hooks.js.map +1 -1
  35. package/dist/index.d.ts +2 -2
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +4 -2
  38. package/dist/index.js.map +1 -1
  39. package/dist/query/DatabaseQueryBuilder.d.ts +24 -0
  40. package/dist/query/DatabaseQueryBuilder.d.ts.map +1 -1
  41. package/dist/query/DatabaseQueryBuilder.js +98 -3
  42. package/dist/query/DatabaseQueryBuilder.js.map +1 -1
  43. package/dist/schema/SchemaBuilder.d.ts +2 -1
  44. package/dist/schema/SchemaBuilder.d.ts.map +1 -1
  45. package/dist/schema/SchemaBuilder.js +2 -1
  46. package/dist/schema/SchemaBuilder.js.map +1 -1
  47. package/dist/schema/TableBuilder.d.ts +140 -132
  48. package/dist/schema/TableBuilder.d.ts.map +1 -1
  49. package/dist/schema/TableBuilder.js +232 -231
  50. package/dist/schema/TableBuilder.js.map +1 -1
  51. package/dist/services/db.d.ts +7 -0
  52. package/dist/services/db.d.ts.map +1 -1
  53. package/dist/services/db.js.map +1 -1
  54. package/index.darwin-arm64.node +0 -0
  55. package/index.darwin-x64.node +0 -0
  56. package/index.linux-arm64-gnu.node +0 -0
  57. package/index.linux-x64-gnu.node +0 -0
  58. package/index.win32-x64-msvc.node +0 -0
  59. package/package.json +3 -3
  60. package/src/BaseEntity.ts +115 -85
  61. package/src/BaseModel.ts +10 -0
  62. package/src/BaseRepository.ts +36 -4
  63. package/src/ConnectionManager.ts +26 -1
  64. package/src/ModelQuery.ts +73 -23
  65. package/src/Transaction.ts +23 -0
  66. package/src/adapters/NapiDbAdapter.ts +7 -0
  67. package/src/decorators/entity.ts +13 -1
  68. package/src/decorators/hooks.ts +67 -0
  69. package/src/index.ts +8 -1
  70. package/src/query/DatabaseQueryBuilder.ts +118 -7
  71. package/src/schema/SchemaBuilder.ts +2 -1
  72. package/src/schema/TableBuilder.ts +331 -303
  73. package/src/services/db.ts +7 -0
@@ -71,6 +71,255 @@ export class ForeignKeyBuilder {
71
71
  }
72
72
  }
73
73
 
74
+ /**
75
+ * The chainable a column-type method hands back — Knex's `ColumnBuilder`.
76
+ *
77
+ * It is bound to ITS OWN column, which is what lets `table.comment()` mean the
78
+ * table comment and `table.string('x').comment()` the column one. A builder
79
+ * flattened onto the table cannot tell those apart, and Knex has both.
80
+ */
81
+ export class ColumnBuilder {
82
+ readonly #table: TableBuilder;
83
+ readonly #column: ColumnDefinition;
84
+
85
+ constructor(table: TableBuilder, column: ColumnDefinition) {
86
+ this.#table = table;
87
+ this.#column = column;
88
+ }
89
+
90
+ notNullable(): this {
91
+ this.#column.nullable = false;
92
+ this.#table.markNullabilityTouched();
93
+ return this;
94
+ }
95
+
96
+ nullable(): this {
97
+ this.#column.nullable = true;
98
+ this.#table.markNullabilityTouched();
99
+ return this;
100
+ }
101
+
102
+ /**
103
+ * Set a column default. JS literals are quoted/escaped (`'x'`, `123`,
104
+ * `true` — Lucid/Knex semantics); wrap SQL expressions in {@link raw} (or
105
+ * use `Migration.now()`) to emit them verbatim.
106
+ */
107
+ defaultTo(value: DefaultValue): this {
108
+ this.#column.defaultValue = renderDefaultValue(value);
109
+ return this;
110
+ }
111
+
112
+ /** MySQL `UNSIGNED` numeric modifier (Lucid `unsigned()`). No-op on pg/sqlite. */
113
+ unsigned(): this {
114
+ this.#column.unsigned = true;
115
+ return this;
116
+ }
117
+
118
+ /**
119
+ * Declare the current column a foreign key.
120
+ *
121
+ * - `references('users', 'id')` — atlas form `(table, column='id')`.
122
+ * - `references('users.id')` — Lucid/Knex dotted `'table.column'` shorthand, so
123
+ * a migration copied from Lucid resolves the target the same way. A single
124
+ * argument without a dot is treated as the table name (column defaults to
125
+ * `id`), preserving the atlas one-arg behaviour.
126
+ */
127
+ references(tableOrPath: string, column?: string): this {
128
+ let table = tableOrPath;
129
+ let col = column ?? "id";
130
+ const dot = tableOrPath.indexOf(".");
131
+ // Dotted shorthand only when no explicit column was passed — an explicit
132
+ // second arg always wins, so `references('a.b', 'c')` stays (table 'a.b').
133
+ if (dot !== -1 && column === undefined) {
134
+ table = tableOrPath.slice(0, dot);
135
+ col = tableOrPath.slice(dot + 1);
136
+ }
137
+ this.#column.references = { table, column: col };
138
+ return this;
139
+ }
140
+
141
+ /**
142
+ * Referential action for the current column's foreign key `ON DELETE`
143
+ * (Lucid parity). Must follow {@link references}.
144
+ */
145
+ onDelete(action: ReferentialAction): this {
146
+ if (this.#column.references) {
147
+ this.#column.references.onDelete = action;
148
+ }
149
+ return this;
150
+ }
151
+
152
+ /** Referential action for the current column's foreign key `ON UPDATE`. Must follow {@link references}. */
153
+ onUpdate(action: ReferentialAction): this {
154
+ if (this.#column.references) {
155
+ this.#column.references.onUpdate = action;
156
+ }
157
+ return this;
158
+ }
159
+
160
+ /**
161
+ * Comment this column (Lucid/Knex column `comment()`). Inline on MySQL, a
162
+ * separate `COMMENT ON COLUMN` on Postgres, dropped on SQLite. The TABLE
163
+ * comment is `table.comment()` — the receiver tells them apart, as in Knex.
164
+ */
165
+ comment(text: string): this {
166
+ this.#column.comment = text;
167
+ return this;
168
+ }
169
+
170
+ /** Collate this column (Lucid/Knex column `collate()`). The table collation is `table.collate()`. */
171
+ collate(collation: string): this {
172
+ this.#column.collate = collation;
173
+ return this;
174
+ }
175
+
176
+ /**
177
+ * Place an added column first (Lucid/Knex `first()`). MySQL-only —
178
+ * Postgres and SQLite always append, and the Rust compiler raises
179
+ * `E_UNSUPPORTED` rather than dropping the instruction silently.
180
+ */
181
+ first(): this {
182
+ this.#column.position = { at: "first" };
183
+ return this;
184
+ }
185
+
186
+ /** Place an added column after `column` (Lucid/Knex `after()`). MySQL-only — see {@link first}. */
187
+ after(column: string): this {
188
+ this.#column.position = { at: "after", column };
189
+ return this;
190
+ }
191
+
192
+ // ─── CHECK constraints ────────────────────────────────────
193
+
194
+ /** `CHECK (col > 0)` on this column (Lucid/Knex `checkPositive`). */
195
+ checkPositive(constraintName?: string): this {
196
+ this.#table.addColumnCheck(
197
+ this.#column.name,
198
+ (column) => ({ check: "positive", column }),
199
+ constraintName,
200
+ );
201
+ return this;
202
+ }
203
+
204
+ /** `CHECK (col < 0)` on this column (Lucid/Knex `checkNegative`). */
205
+ checkNegative(constraintName?: string): this {
206
+ this.#table.addColumnCheck(
207
+ this.#column.name,
208
+ (column) => ({ check: "negative", column }),
209
+ constraintName,
210
+ );
211
+ return this;
212
+ }
213
+
214
+ /** `CHECK (col IN (…))` on this column (Lucid/Knex `checkIn`). Values are quoted, never interpolated raw. */
215
+ checkIn(values: readonly CheckValue[], constraintName?: string): this {
216
+ this.#table.addColumnCheck(
217
+ this.#column.name,
218
+ (column) => ({ check: "in", column, values: [...values] }),
219
+ constraintName,
220
+ );
221
+ return this;
222
+ }
223
+
224
+ /** `CHECK (col NOT IN (…))` on this column (Lucid/Knex `checkNotIn`). */
225
+ checkNotIn(values: readonly CheckValue[], constraintName?: string): this {
226
+ this.#table.addColumnCheck(
227
+ this.#column.name,
228
+ (column) => ({ check: "notIn", column, values: [...values] }),
229
+ constraintName,
230
+ );
231
+ return this;
232
+ }
233
+
234
+ /**
235
+ * `CHECK (col BETWEEN lo AND hi)` on this column (Lucid/Knex
236
+ * `checkBetween`). Accepts one `[min, max]` interval or a list of them —
237
+ * several intervals are OR'd together, as in Knex.
238
+ */
239
+ checkBetween(
240
+ range: readonly CheckValue[] | readonly (readonly CheckValue[])[],
241
+ constraintName?: string,
242
+ ): this {
243
+ // A single [min, max] vs a list of intervals: the first element of a
244
+ // list-of-intervals is itself an array.
245
+ const ranges = Array.isArray(range[0])
246
+ ? (range as readonly (readonly CheckValue[])[]).map((r) => [...r])
247
+ : [[...(range as readonly CheckValue[])]];
248
+ this.#table.addColumnCheck(
249
+ this.#column.name,
250
+ (column) => ({ check: "between", column, ranges }),
251
+ constraintName,
252
+ );
253
+ return this;
254
+ }
255
+
256
+ /** `CHECK (LENGTH(col) <op> n)` on this column (Lucid/Knex `checkLength`). The operator is allow-listed by the Rust compiler. */
257
+ checkLength(
258
+ operator: CheckOperator,
259
+ length: number,
260
+ constraintName?: string,
261
+ ): this {
262
+ this.#table.addColumnCheck(
263
+ this.#column.name,
264
+ (column) => ({ check: "length", column, operator, length }),
265
+ constraintName,
266
+ );
267
+ return this;
268
+ }
269
+
270
+ /**
271
+ * `CHECK (col ~ 'pattern')` on this column (Lucid/Knex `checkRegex`).
272
+ * Postgres spells it `~`; MySQL and SQLite use `REGEXP`.
273
+ *
274
+ * SQLite parses `REGEXP` but ships no implementation — the constraint only
275
+ * works if the connection registers a `regexp` function. Knex behaves the
276
+ * same way, so this is parity rather than a new trap, but it is worth
277
+ * knowing before you rely on it there.
278
+ */
279
+ checkRegex(pattern: string, constraintName?: string): this {
280
+ this.#table.addColumnCheck(
281
+ this.#column.name,
282
+ (column) => ({ check: "regex", column, pattern }),
283
+ constraintName,
284
+ );
285
+ return this;
286
+ }
287
+
288
+ /** Mark this column the primary key (Lucid/Knex column `primary()`). */
289
+ primary(): this {
290
+ this.#column.primary = true;
291
+ return this;
292
+ }
293
+
294
+ /** Mark this column `UNIQUE` (Lucid/Knex column `unique()`). */
295
+ unique(): this {
296
+ this.#column.unique = true;
297
+ return this;
298
+ }
299
+
300
+ /** Index this column (Lucid/Knex column `index()`). */
301
+ index(name?: string): this {
302
+ this.#table.index(this.#column.name, name);
303
+ return this;
304
+ }
305
+
306
+ /**
307
+ * Apply this definition as a type change instead of an `ADD COLUMN`
308
+ * (Lucid/Knex `alter()`).
309
+ *
310
+ * Nullability moves only if `.nullable()` / `.notNullable()` was called
311
+ * before this — a bare `t.string('x').alter()` changes the type and leaves
312
+ * the NOT NULL constraint exactly as it is.
313
+ *
314
+ * SQLite cannot alter a column in place; the Rust compiler rejects it with
315
+ * `E_UNSUPPORTED` rather than emitting a table rebuild behind your back.
316
+ */
317
+ alter(): this {
318
+ this.#table.alterPendingColumn();
319
+ return this;
320
+ }
321
+ }
322
+
74
323
  /**
75
324
  * Whether the builder is filling a `CREATE TABLE` or an `ALTER TABLE`. In
76
325
  * `alter` mode a column-type method (`t.string('x')`) becomes `ADD COLUMN`,
@@ -107,7 +356,7 @@ export class TableBuilder {
107
356
 
108
357
  // ─── Column types ─────────────────────────────────────────
109
358
 
110
- uuid(name: string): this {
359
+ uuid(name: string): ColumnBuilder {
111
360
  return this.#addColumn(name, "uuid");
112
361
  }
113
362
 
@@ -117,19 +366,19 @@ export class TableBuilder {
117
366
  * `AUTOINCREMENT`, Postgres `GENERATED ... AS IDENTITY`, MySQL
118
367
  * `AUTO_INCREMENT`). For a 64-bit key use `bigIncrements()`.
119
368
  */
120
- increments(name = "id"): this {
369
+ increments(name = "id"): ColumnBuilder {
121
370
  return this.#addIncrements(name, "integer");
122
371
  }
123
372
 
124
373
  /** Auto-incrementing 64-bit primary key (Lucid `bigIncrements()`). */
125
- bigIncrements(name = "id"): this {
374
+ bigIncrements(name = "id"): ColumnBuilder {
126
375
  return this.#addIncrements(name, "bigInteger");
127
376
  }
128
377
 
129
- string(name: string, length = 255): this {
130
- this.#addColumn(name, "string");
378
+ string(name: string, length = 255): ColumnBuilder {
379
+ const column = this.#addColumn(name, "string");
131
380
  if (this.#currentColumn) this.#currentColumn.length = length;
132
- return this;
381
+ return column;
133
382
  }
134
383
 
135
384
  /**
@@ -137,35 +386,35 @@ export class TableBuilder {
137
386
  * MySQL type (`MEDIUMTEXT` / `LONGTEXT`); Postgres and SQLite have a single
138
387
  * unbounded `TEXT` and ignore it.
139
388
  */
140
- text(name: string, textType: TextVariant = "text"): this {
389
+ text(name: string, textType: TextVariant = "text"): ColumnBuilder {
141
390
  return this.#addColumn(name, textType);
142
391
  }
143
- integer(name: string): this {
392
+ integer(name: string): ColumnBuilder {
144
393
  return this.#addColumn(name, "integer");
145
394
  }
146
395
  /** 24-bit integer (Lucid/Knex `mediumint`). MySQL `MEDIUMINT`; pg/SQLite widen to `INTEGER`. */
147
- mediumint(name: string): this {
396
+ mediumint(name: string): ColumnBuilder {
148
397
  return this.#addColumn(name, "mediumint");
149
398
  }
150
399
  /** 8-bit integer (Lucid `tinyint`). MySQL `TINYINT`; Postgres widens to `SMALLINT`; SQLite `INTEGER`. */
151
- tinyint(name: string): this {
400
+ tinyint(name: string): ColumnBuilder {
152
401
  return this.#addColumn(name, "tinyint");
153
402
  }
154
403
  /** 16-bit integer (Lucid `smallint`). `SMALLINT` on pg/mysql, `INTEGER` on SQLite. */
155
- smallint(name: string): this {
404
+ smallint(name: string): ColumnBuilder {
156
405
  return this.#addColumn(name, "smallint");
157
406
  }
158
- bigInteger(name: string): this {
407
+ bigInteger(name: string): ColumnBuilder {
159
408
  return this.#addColumn(name, "bigInteger");
160
409
  }
161
410
 
162
- decimal(name: string, precision = 10, scale = 2): this {
163
- this.#addColumn(name, "decimal");
411
+ decimal(name: string, precision = 10, scale = 2): ColumnBuilder {
412
+ const column = this.#addColumn(name, "decimal");
164
413
  if (this.#currentColumn) {
165
414
  this.#currentColumn.precision = precision;
166
415
  this.#currentColumn.scale = scale;
167
416
  }
168
- return this;
417
+ return column;
169
418
  }
170
419
 
171
420
  /**
@@ -173,18 +422,18 @@ export class TableBuilder {
173
422
  * MySQL. `precision`/`scale` render `FLOAT(p, s)` on MySQL only — pg and
174
423
  * SQLite have fixed-width floats and ignore them.
175
424
  */
176
- float(name: string, precision?: number, scale?: number): this {
425
+ float(name: string, precision?: number, scale?: number): ColumnBuilder {
177
426
  return this.#addFloat(name, "float", precision, scale);
178
427
  }
179
428
  /** Double-precision float (Lucid `double`). `DOUBLE PRECISION` on pg, `REAL` on SQLite, `DOUBLE` on MySQL. See {@link float} for precision/scale. */
180
- double(name: string, precision?: number, scale?: number): this {
429
+ double(name: string, precision?: number, scale?: number): ColumnBuilder {
181
430
  return this.#addFloat(name, "double", precision, scale);
182
431
  }
183
432
 
184
- boolean(name: string): this {
433
+ boolean(name: string): ColumnBuilder {
185
434
  return this.#addColumn(name, "boolean");
186
435
  }
187
- date(name: string): this {
436
+ date(name: string): ColumnBuilder {
188
437
  return this.#addColumn(name, "date");
189
438
  }
190
439
  /**
@@ -192,10 +441,10 @@ export class TableBuilder {
192
441
  * `precision` renders `TIME(p)` fractional seconds (ignored on SQLite,
193
442
  * which has no time type to carry it).
194
443
  */
195
- time(name: string, precision?: number): this {
196
- this.#addColumn(name, "time");
444
+ time(name: string, precision?: number): ColumnBuilder {
445
+ const column = this.#addColumn(name, "time");
197
446
  if (this.#currentColumn) this.#currentColumn.precision = precision;
198
- return this;
447
+ return column;
199
448
  }
200
449
  /**
201
450
  * Timestamp column (Lucid `timestamp(name, options)`).
@@ -208,16 +457,19 @@ export class TableBuilder {
208
457
  timestamp(
209
458
  name: string,
210
459
  options: { useTz?: boolean; precision?: number } = {},
211
- ): this {
212
- this.#addColumn(name, options.useTz ? "timestamptz" : "timestamp");
460
+ ): ColumnBuilder {
461
+ const column = this.#addColumn(
462
+ name,
463
+ options.useTz ? "timestamptz" : "timestamp",
464
+ );
213
465
  if (this.#currentColumn) this.#currentColumn.precision = options.precision;
214
- return this;
466
+ return column;
215
467
  }
216
468
  /** Alias of {@link timestamp} (Lucid `dateTime`). Use `{ useTz: true }` or {@link timestamptz} for a tz-aware column. */
217
469
  dateTime(
218
470
  name: string,
219
471
  options: { useTz?: boolean; precision?: number } = {},
220
- ): this {
472
+ ): ColumnBuilder {
221
473
  return this.timestamp(name, options);
222
474
  }
223
475
  /**
@@ -229,33 +481,27 @@ export class TableBuilder {
229
481
  * from the SQL type). On MySQL/SQLite (no real tz type) it degrades to the
230
482
  * plain timestamp mapping.
231
483
  */
232
- timestamptz(name: string): this {
484
+ timestamptz(name: string): ColumnBuilder {
233
485
  return this.#addColumn(name, "timestamptz");
234
486
  }
235
- json(name: string): this {
487
+ json(name: string): ColumnBuilder {
236
488
  return this.#addColumn(name, "json");
237
489
  }
238
490
  /**
239
491
  * Binary JSON (Lucid/Knex `jsonb`). `JSONB` on pg, `JSON` on MySQL, `TEXT`
240
492
  * on SQLite.
241
- *
242
- * Deviation, named: atlas's {@link json} already maps to `JSONB` on
243
- * Postgres (it predates this method), where Lucid's `json()` maps to
244
- * `json`. Leaving `json()` alone avoids silently rewriting the physical
245
- * type of existing columns and desyncing `SchemaCheck`, so on Postgres the
246
- * two spellings coincide.
247
493
  */
248
- jsonb(name: string): this {
494
+ jsonb(name: string): ColumnBuilder {
249
495
  return this.#addColumn(name, "jsonb");
250
496
  }
251
497
  /**
252
498
  * Binary blob (Lucid/Knex `binary(name, length)`). `BYTEA` on pg, `BLOB` on
253
499
  * SQLite; on MySQL `length` selects `VARBINARY(n)` over `BLOB`.
254
500
  */
255
- binary(name: string, length?: number): this {
256
- this.#addColumn(name, "binary");
501
+ binary(name: string, length?: number): ColumnBuilder {
502
+ const column = this.#addColumn(name, "binary");
257
503
  if (this.#currentColumn) this.#currentColumn.length = length;
258
- return this;
504
+ return column;
259
505
  }
260
506
 
261
507
  /**
@@ -268,10 +514,10 @@ export class TableBuilder {
268
514
  * narrow grammar (letters, digits, spaces, `_`, and one parenthesised
269
515
  * argument list) and rejects anything else with `E_UNSAFE_SQL`.
270
516
  */
271
- specificType(name: string, type: string): this {
272
- this.#addColumn(name, "specificType");
517
+ specificType(name: string, type: string): ColumnBuilder {
518
+ const column = this.#addColumn(name, "specificType");
273
519
  if (this.#currentColumn) this.#currentColumn.rawType = type;
274
- return this;
520
+ return column;
275
521
  }
276
522
 
277
523
  /**
@@ -279,10 +525,10 @@ export class TableBuilder {
279
525
  * Postgres and SQLite render `TEXT` plus a `CHECK (col IN (...))` that pins the
280
526
  * value set. At least one value is required.
281
527
  */
282
- enum(name: string, values: string[]): this {
283
- this.#addColumn(name, "enum");
528
+ enum(name: string, values: string[]): ColumnBuilder {
529
+ const column = this.#addColumn(name, "enum");
284
530
  if (this.#currentColumn) this.#currentColumn.values = values;
285
- return this;
531
+ return column;
286
532
  }
287
533
 
288
534
  // ─── Shortcuts ────────────────────────────────────────────
@@ -307,7 +553,7 @@ export class TableBuilder {
307
553
  * (A dialect-aware escape hatch can be added later if a future story needs
308
554
  * per-dialect PK defaults.)
309
555
  */
310
- id(): this {
556
+ id(): ColumnBuilder {
311
557
  return this.uuid("id").primary().defaultTo(new RawSql("gen_random_uuid()"));
312
558
  }
313
559
 
@@ -347,10 +593,9 @@ export class TableBuilder {
347
593
  */
348
594
  timestamps(useTimestamps = true, defaultToNow = true): this {
349
595
  const add = (name: string): void => {
350
- if (useTimestamps) this.timestamp(name);
351
- else this.dateTime(name);
596
+ const column = useTimestamps ? this.timestamp(name) : this.dateTime(name);
352
597
  if (defaultToNow) {
353
- this.notNullable().defaultTo(new RawSql("CURRENT_TIMESTAMP"));
598
+ column.notNullable().defaultTo(new RawSql("CURRENT_TIMESTAMP"));
354
599
  }
355
600
  };
356
601
  add("created_at");
@@ -358,209 +603,13 @@ export class TableBuilder {
358
603
  return this;
359
604
  }
360
605
 
361
- // ─── Column modifiers ─────────────────────────────────────
362
-
363
- notNullable(): this {
364
- if (this.#currentColumn) this.#currentColumn.nullable = false;
365
- this.#nullabilityTouched = true;
366
- return this;
367
- }
368
-
369
- nullable(): this {
370
- if (this.#currentColumn) this.#currentColumn.nullable = true;
371
- this.#nullabilityTouched = true;
372
- return this;
373
- }
374
-
375
- /**
376
- * Set a column default. JS literals are quoted/escaped (`'x'`, `123`,
377
- * `true` — Lucid/Knex semantics); wrap SQL expressions in {@link raw} (or
378
- * use `Migration.now()`) to emit them verbatim.
379
- */
380
- defaultTo(value: DefaultValue): this {
381
- if (this.#currentColumn) {
382
- this.#currentColumn.defaultValue = renderDefaultValue(value);
383
- }
384
- return this;
385
- }
386
-
387
- /** MySQL `UNSIGNED` numeric modifier (Lucid `unsigned()`). No-op on pg/sqlite. */
388
- unsigned(): this {
389
- if (this.#currentColumn) this.#currentColumn.unsigned = true;
390
- return this;
391
- }
392
-
393
- /**
394
- * Declare the current column a foreign key.
395
- *
396
- * - `references('users', 'id')` — atlas form `(table, column='id')`.
397
- * - `references('users.id')` — Lucid/Knex dotted `'table.column'` shorthand, so
398
- * a migration copied from Lucid resolves the target the same way. A single
399
- * argument without a dot is treated as the table name (column defaults to
400
- * `id`), preserving the atlas one-arg behaviour.
401
- */
402
- references(tableOrPath: string, column?: string): this {
403
- let table = tableOrPath;
404
- let col = column ?? "id";
405
- const dot = tableOrPath.indexOf(".");
406
- // Dotted shorthand only when no explicit column was passed — an explicit
407
- // second arg always wins, so `references('a.b', 'c')` stays (table 'a.b').
408
- if (dot !== -1 && column === undefined) {
409
- table = tableOrPath.slice(0, dot);
410
- col = tableOrPath.slice(dot + 1);
411
- }
412
- if (this.#currentColumn) {
413
- this.#currentColumn.references = { table, column: col };
414
- }
415
- return this;
416
- }
417
-
418
- /**
419
- * Referential action for the current column's foreign key `ON DELETE`
420
- * (Lucid parity). Must follow {@link references}.
421
- */
422
- onDelete(action: ReferentialAction): this {
423
- if (this.#currentColumn?.references) {
424
- this.#currentColumn.references.onDelete = action;
425
- }
426
- return this;
427
- }
428
-
429
- /** Referential action for the current column's foreign key `ON UPDATE`. Must follow {@link references}. */
430
- onUpdate(action: ReferentialAction): this {
431
- if (this.#currentColumn?.references) {
432
- this.#currentColumn.references.onUpdate = action;
433
- }
434
- return this;
435
- }
436
-
437
- /**
438
- * Comment the current **column** (Lucid/Knex column `comment()`). Inline on
439
- * MySQL, a separate `COMMENT ON COLUMN` on Postgres, dropped on SQLite.
440
- *
441
- * Deviation, named: Knex's `table.comment()` is the TABLE comment, because
442
- * its column methods return a separate column builder. Atlas flattens the
443
- * column modifiers onto the table builder (`.notNullable()`, `.unique()`,
444
- * `.defaultTo()` all work this way), so `comment()` follows that same rule
445
- * and the table comment is {@link tableComment}. Resolving it by "is a
446
- * column pending?" would be exactly the kind of guessing that bites later.
447
- */
448
- comment(text: string): this {
449
- if (this.#currentColumn) this.#currentColumn.comment = text;
450
- return this;
451
- }
452
-
453
- /** Collate the current **column** (Lucid/Knex column `collate()`). See {@link comment} for why the table form is {@link tableCollate}. */
454
- collate(collation: string): this {
455
- if (this.#currentColumn) this.#currentColumn.collate = collation;
456
- return this;
457
- }
458
-
459
- /**
460
- * Place an added column first (Lucid/Knex `first()`). MySQL-only —
461
- * Postgres and SQLite always append, and the Rust compiler raises
462
- * `E_UNSUPPORTED` rather than dropping the instruction silently.
463
- */
464
- first(): this {
465
- if (this.#currentColumn) this.#currentColumn.position = { at: "first" };
466
- return this;
467
- }
468
-
469
- /** Place an added column after `column` (Lucid/Knex `after()`). MySQL-only — see {@link first}. */
470
- after(column: string): this {
471
- if (this.#currentColumn) {
472
- this.#currentColumn.position = { at: "after", column };
473
- }
474
- return this;
475
- }
476
-
477
606
  // ─── CHECK constraints ────────────────────────────────────
478
607
 
479
- /** `CHECK (col > 0)` on the current column (Lucid/Knex `checkPositive`). */
480
- checkPositive(constraintName?: string): this {
481
- return this.#addCheck(
482
- (column) => ({ check: "positive", column }),
483
- constraintName,
484
- );
485
- }
486
-
487
- /** `CHECK (col < 0)` on the current column (Lucid/Knex `checkNegative`). */
488
- checkNegative(constraintName?: string): this {
489
- return this.#addCheck(
490
- (column) => ({ check: "negative", column }),
491
- constraintName,
492
- );
493
- }
494
-
495
- /** `CHECK (col IN (…))` on the current column (Lucid/Knex `checkIn`). Values are quoted, never interpolated raw. */
496
- checkIn(values: readonly CheckValue[], constraintName?: string): this {
497
- return this.#addCheck(
498
- (column) => ({ check: "in", column, values: [...values] }),
499
- constraintName,
500
- );
501
- }
502
-
503
- /** `CHECK (col NOT IN (…))` on the current column (Lucid/Knex `checkNotIn`). */
504
- checkNotIn(values: readonly CheckValue[], constraintName?: string): this {
505
- return this.#addCheck(
506
- (column) => ({ check: "notIn", column, values: [...values] }),
507
- constraintName,
508
- );
509
- }
510
-
511
- /**
512
- * `CHECK (col BETWEEN lo AND hi)` on the current column (Lucid/Knex
513
- * `checkBetween`). Accepts one `[min, max]` interval or a list of them —
514
- * several intervals are OR'd together, as in Knex.
515
- */
516
- checkBetween(
517
- range: readonly CheckValue[] | readonly (readonly CheckValue[])[],
518
- constraintName?: string,
519
- ): this {
520
- // A single [min, max] vs a list of intervals: the first element of a
521
- // list-of-intervals is itself an array.
522
- const ranges = Array.isArray(range[0])
523
- ? (range as readonly (readonly CheckValue[])[]).map((r) => [...r])
524
- : [[...(range as readonly CheckValue[])]];
525
- return this.#addCheck(
526
- (column) => ({ check: "between", column, ranges }),
527
- constraintName,
528
- );
529
- }
530
-
531
- /** `CHECK (LENGTH(col) <op> n)` on the current column (Lucid/Knex `checkLength`). The operator is allow-listed by the Rust compiler. */
532
- checkLength(
533
- operator: CheckOperator,
534
- length: number,
535
- constraintName?: string,
536
- ): this {
537
- return this.#addCheck(
538
- (column) => ({ check: "length", column, operator, length }),
539
- constraintName,
540
- );
541
- }
542
-
543
- /**
544
- * `CHECK (col ~ 'pattern')` on the current column (Lucid/Knex `checkRegex`).
545
- * Postgres spells it `~`; MySQL and SQLite use `REGEXP`.
546
- *
547
- * SQLite parses `REGEXP` but ships no implementation — the constraint only
548
- * works if the connection registers a `regexp` function. Knex behaves the
549
- * same way, so this is parity rather than a new trap, but it is worth
550
- * knowing before you rely on it there.
551
- */
552
- checkRegex(pattern: string, constraintName?: string): this {
553
- return this.#addCheck(
554
- (column) => ({ check: "regex", column, pattern }),
555
- constraintName,
556
- );
557
- }
558
-
559
608
  /**
560
609
  * A free-form `CHECK (predicate)` (Lucid/Knex `check`). The predicate is
561
610
  * emitted verbatim — exactly as trusted as {@link Schema.raw}, so never
562
- * build it from user input. Prefer the typed `check*` helpers, which are
563
- * safe by construction.
611
+ * build it from user input. Prefer the typed `check*` helpers on the column
612
+ * builder, which are safe by construction.
564
613
  */
565
614
  check(predicate: string, constraintName?: string): this {
566
615
  this.#pushConstraint({
@@ -582,16 +631,8 @@ export class TableBuilder {
582
631
 
583
632
  // ─── Table-level constraints ──────────────────────────────
584
633
 
585
- /**
586
- * With no argument, mark the current column as the primary key (the
587
- * existing column modifier). With a column list, declare a composite
588
- * `PRIMARY KEY (…)` table constraint (Lucid/Knex `primary([...])`).
589
- */
590
- primary(columns?: readonly string[], constraintName?: string): this {
591
- if (columns === undefined) {
592
- if (this.#currentColumn) this.#currentColumn.primary = true;
593
- return this;
594
- }
634
+ /** Composite `PRIMARY KEY (…)` table constraint (Lucid/Knex `primary([...])`). */
635
+ primary(columns: readonly string[], constraintName?: string): this {
595
636
  this.#pushConstraint({
596
637
  constraint: "primary",
597
638
  name: constraintName,
@@ -601,18 +642,12 @@ export class TableBuilder {
601
642
  }
602
643
 
603
644
  /**
604
- * With no argument, mark the current column `UNIQUE` (the existing column
605
- * modifier). With a column list, declare a composite `UNIQUE (…)` table
606
- * constraint (Lucid/Knex `unique([...])`).
645
+ * Composite `UNIQUE (…)` table constraint (Lucid/Knex `unique([...])`).
607
646
  *
608
647
  * Note this is a real constraint, unlike {@link uniqueIndex}, which creates
609
648
  * a separate `CREATE UNIQUE INDEX`.
610
649
  */
611
- unique(columns?: readonly string[], constraintName?: string): this {
612
- if (columns === undefined) {
613
- if (this.#currentColumn) this.#currentColumn.unique = true;
614
- return this;
615
- }
650
+ unique(columns: readonly string[], constraintName?: string): this {
616
651
  this.#pushConstraint({
617
652
  constraint: "unique",
618
653
  name: constraintName ?? this.#constraintName(columns, "unique"),
@@ -698,14 +733,14 @@ export class TableBuilder {
698
733
  return this;
699
734
  }
700
735
 
701
- /** MySQL default collation for the table. Named `tableCollate` because {@link collate} is the column modifier — see {@link comment}. */
702
- tableCollate(name: string): this {
736
+ /** MySQL default collation for the table (Lucid/Knex `collate`). The column form is `table.<type>(…).collate()`. */
737
+ collate(name: string): this {
703
738
  this.#options.collate = name;
704
739
  return this;
705
740
  }
706
741
 
707
- /** Table comment. Named `tableComment` because {@link comment} is the column modifier — see there for why. */
708
- tableComment(text: string): this {
742
+ /** Table comment (Lucid/Knex `comment`). The column form is `table.<type>(…).comment()`. */
743
+ comment(text: string): this {
709
744
  this.#options.comment = text;
710
745
  return this;
711
746
  }
@@ -736,16 +771,9 @@ export class TableBuilder {
736
771
 
737
772
  /**
738
773
  * Apply the pending column definition as a type change instead of an
739
- * `ADD COLUMN` (Lucid/Knex `alter()`). Must follow a column-type method.
740
- *
741
- * Nullability moves only if `.nullable()` / `.notNullable()` was called
742
- * before this — a bare `t.string('x').alter()` changes the type and leaves
743
- * the NOT NULL constraint exactly as it is.
744
- *
745
- * SQLite cannot alter a column in place; the Rust compiler rejects it with
746
- * `E_UNSUPPORTED` rather than emitting a table rebuild behind your back.
774
+ * @internal Backs `ColumnBuilder.alter()`.
747
775
  */
748
- alter(): this {
776
+ alterPendingColumn(): void {
749
777
  this.#assertAlterMode("alter()");
750
778
  const pending = this.#currentOp;
751
779
  if (!pending) {
@@ -764,7 +792,24 @@ export class TableBuilder {
764
792
  this.#operations[this.#operations.indexOf(pending)] = converted;
765
793
  this.#currentOp = converted;
766
794
  }
767
- return this;
795
+ }
796
+
797
+ /** @internal Backs `ColumnBuilder.nullable()` / `.notNullable()`. */
798
+ markNullabilityTouched(): void {
799
+ this.#nullabilityTouched = true;
800
+ }
801
+
802
+ /** @internal Backs the `ColumnBuilder.check*()` helpers, for a named column. */
803
+ addColumnCheck(
804
+ column: string,
805
+ build: (column: string) => CheckExpression,
806
+ constraintName?: string,
807
+ ): void {
808
+ this.#pushConstraint({
809
+ constraint: "check",
810
+ name: constraintName,
811
+ expr: build(column),
812
+ });
768
813
  }
769
814
 
770
815
  /** Drop a column (Lucid/Knex `dropColumn`). */
@@ -920,23 +965,6 @@ export class TableBuilder {
920
965
  }
921
966
 
922
967
  /** Build a CHECK against the pending column. */
923
- #addCheck(
924
- build: (column: string) => CheckExpression,
925
- constraintName?: string,
926
- ): this {
927
- const column = this.#currentColumn?.name;
928
- if (!column) {
929
- throw new Error(
930
- "E_CHECK_MISUSE: a check* helper must follow a column definition, e.g. table.integer('qty').checkPositive()",
931
- );
932
- }
933
- this.#pushConstraint({
934
- constraint: "check",
935
- name: constraintName,
936
- expr: build(column),
937
- });
938
- return this;
939
- }
940
968
 
941
969
  /**
942
970
  * Default constraint name, following Knex's `<table>_<columns>_<suffix>`
@@ -968,7 +996,7 @@ export class TableBuilder {
968
996
  this.#nullabilityTouched = false;
969
997
  }
970
998
 
971
- #addColumn(name: string, type: ColumnType): this {
999
+ #addColumn(name: string, type: ColumnType): ColumnBuilder {
972
1000
  const col: ColumnDefinition = {
973
1001
  name,
974
1002
  type,
@@ -987,7 +1015,7 @@ export class TableBuilder {
987
1015
  this.#operations.push(op);
988
1016
  this.#currentOp = op;
989
1017
  }
990
- return this;
1018
+ return new ColumnBuilder(this, col);
991
1019
  }
992
1020
 
993
1021
  #addFloat(
@@ -995,22 +1023,22 @@ export class TableBuilder {
995
1023
  type: "float" | "double",
996
1024
  precision?: number,
997
1025
  scale?: number,
998
- ): this {
999
- this.#addColumn(name, type);
1026
+ ): ColumnBuilder {
1027
+ const column = this.#addColumn(name, type);
1000
1028
  if (this.#currentColumn) {
1001
1029
  this.#currentColumn.precision = precision;
1002
1030
  this.#currentColumn.scale = scale;
1003
1031
  }
1004
- return this;
1032
+ return column;
1005
1033
  }
1006
1034
 
1007
- #addIncrements(name: string, type: "integer" | "bigInteger"): this {
1008
- this.#addColumn(name, type);
1035
+ #addIncrements(name: string, type: "integer" | "bigInteger"): ColumnBuilder {
1036
+ const column = this.#addColumn(name, type);
1009
1037
  if (this.#currentColumn) {
1010
1038
  this.#currentColumn.autoIncrement = true;
1011
1039
  this.#currentColumn.primary = true;
1012
1040
  this.#currentColumn.nullable = false;
1013
1041
  }
1014
- return this;
1042
+ return column;
1015
1043
  }
1016
1044
  }