@c9up/atlas 0.1.19 → 0.2.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.
Files changed (250) hide show
  1. package/README.md +63 -14
  2. package/db.darwin-arm64.node +0 -0
  3. package/db.darwin-x64.node +0 -0
  4. package/db.linux-arm64-gnu.node +0 -0
  5. package/db.linux-x64-gnu.node +0 -0
  6. package/db.win32-x64-msvc.node +0 -0
  7. package/dist/AtlasProvider.d.ts +66 -7
  8. package/dist/AtlasProvider.d.ts.map +1 -1
  9. package/dist/AtlasProvider.js +205 -39
  10. package/dist/AtlasProvider.js.map +1 -1
  11. package/dist/BaseEntity.d.ts +181 -5
  12. package/dist/BaseEntity.d.ts.map +1 -1
  13. package/dist/BaseEntity.js +339 -33
  14. package/dist/BaseEntity.js.map +1 -1
  15. package/dist/BaseModel.d.ts +127 -0
  16. package/dist/BaseModel.d.ts.map +1 -0
  17. package/dist/BaseModel.js +250 -0
  18. package/dist/BaseModel.js.map +1 -0
  19. package/dist/BaseRepository.d.ts +81 -15
  20. package/dist/BaseRepository.d.ts.map +1 -1
  21. package/dist/BaseRepository.js +1433 -387
  22. package/dist/BaseRepository.js.map +1 -1
  23. package/dist/ConnectionManager.d.ts +106 -0
  24. package/dist/ConnectionManager.d.ts.map +1 -0
  25. package/dist/ConnectionManager.js +228 -0
  26. package/dist/ConnectionManager.js.map +1 -0
  27. package/dist/ModelQuery.d.ts +577 -41
  28. package/dist/ModelQuery.d.ts.map +1 -1
  29. package/dist/ModelQuery.js +2188 -291
  30. package/dist/ModelQuery.js.map +1 -1
  31. package/dist/Transaction.d.ts +71 -1
  32. package/dist/Transaction.d.ts.map +1 -1
  33. package/dist/Transaction.js +138 -24
  34. package/dist/Transaction.js.map +1 -1
  35. package/dist/adapters/NapiDbAdapter.d.ts +44 -4
  36. package/dist/adapters/NapiDbAdapter.d.ts.map +1 -1
  37. package/dist/adapters/NapiDbAdapter.js +131 -13
  38. package/dist/adapters/NapiDbAdapter.js.map +1 -1
  39. package/dist/console/contract.d.ts +62 -0
  40. package/dist/console/contract.d.ts.map +1 -0
  41. package/dist/console/contract.js +38 -0
  42. package/dist/console/contract.js.map +1 -0
  43. package/dist/console/factoryCommands.d.ts +23 -0
  44. package/dist/console/factoryCommands.d.ts.map +1 -0
  45. package/dist/console/factoryCommands.js +62 -0
  46. package/dist/console/factoryCommands.js.map +1 -0
  47. package/dist/console/migrationCommands.d.ts +78 -0
  48. package/dist/console/migrationCommands.d.ts.map +1 -0
  49. package/dist/console/migrationCommands.js +327 -0
  50. package/dist/console/migrationCommands.js.map +1 -0
  51. package/dist/console/schemaCheckCommand.d.ts +11 -17
  52. package/dist/console/schemaCheckCommand.d.ts.map +1 -1
  53. package/dist/console/schemaCheckCommand.js +21 -15
  54. package/dist/console/schemaCheckCommand.js.map +1 -1
  55. package/dist/console/schemaDumpCommand.d.ts +30 -0
  56. package/dist/console/schemaDumpCommand.d.ts.map +1 -0
  57. package/dist/console/schemaDumpCommand.js +69 -0
  58. package/dist/console/schemaDumpCommand.js.map +1 -0
  59. package/dist/console/schemaGenerateCommand.d.ts +100 -0
  60. package/dist/console/schemaGenerateCommand.d.ts.map +1 -0
  61. package/dist/console/schemaGenerateCommand.js +246 -0
  62. package/dist/console/schemaGenerateCommand.js.map +1 -0
  63. package/dist/console/seederCommands.d.ts +46 -0
  64. package/dist/console/seederCommands.d.ts.map +1 -0
  65. package/dist/console/seederCommands.js +136 -0
  66. package/dist/console/seederCommands.js.map +1 -0
  67. package/dist/decorators/entity.d.ts +50 -8
  68. package/dist/decorators/entity.d.ts.map +1 -1
  69. package/dist/decorators/entity.js +42 -2
  70. package/dist/decorators/entity.js.map +1 -1
  71. package/dist/decorators/hooks.d.ts +3 -3
  72. package/dist/decorators/hooks.d.ts.map +1 -1
  73. package/dist/decorators/hooks.js.map +1 -1
  74. package/dist/events.d.ts +69 -0
  75. package/dist/events.d.ts.map +1 -0
  76. package/dist/events.js +82 -0
  77. package/dist/events.js.map +1 -0
  78. package/dist/factories.d.ts +8 -0
  79. package/dist/factories.d.ts.map +1 -0
  80. package/dist/factories.js +8 -0
  81. package/dist/factories.js.map +1 -0
  82. package/dist/index.d.ts +15 -5
  83. package/dist/index.d.ts.map +1 -1
  84. package/dist/index.js +14 -3
  85. package/dist/index.js.map +1 -1
  86. package/dist/lucid-schema.d.ts +15 -0
  87. package/dist/lucid-schema.d.ts.map +1 -0
  88. package/dist/lucid-schema.js +16 -0
  89. package/dist/lucid-schema.js.map +1 -0
  90. package/dist/metadata-keys.d.ts +3 -2
  91. package/dist/metadata-keys.d.ts.map +1 -1
  92. package/dist/naming/NamingStrategy.d.ts +7 -0
  93. package/dist/naming/NamingStrategy.d.ts.map +1 -1
  94. package/dist/naming/NamingStrategy.js +16 -0
  95. package/dist/naming/NamingStrategy.js.map +1 -1
  96. package/dist/orm.d.ts +14 -0
  97. package/dist/orm.d.ts.map +1 -0
  98. package/dist/orm.js +14 -0
  99. package/dist/orm.js.map +1 -0
  100. package/dist/query/DatabaseQueryBuilder.d.ts +557 -0
  101. package/dist/query/DatabaseQueryBuilder.d.ts.map +1 -0
  102. package/dist/query/DatabaseQueryBuilder.js +1798 -0
  103. package/dist/query/DatabaseQueryBuilder.js.map +1 -0
  104. package/dist/query/DmlBuilder.d.ts +62 -0
  105. package/dist/query/DmlBuilder.d.ts.map +1 -0
  106. package/dist/query/DmlBuilder.js +84 -0
  107. package/dist/query/DmlBuilder.js.map +1 -0
  108. package/dist/query/QueryBuilder.d.ts +5 -0
  109. package/dist/query/QueryBuilder.d.ts.map +1 -1
  110. package/dist/query/QueryBuilder.js +7 -0
  111. package/dist/query/QueryBuilder.js.map +1 -1
  112. package/dist/query/RawQueryBuilder.d.ts +47 -0
  113. package/dist/query/RawQueryBuilder.d.ts.map +1 -0
  114. package/dist/query/RawQueryBuilder.js +138 -0
  115. package/dist/query/RawQueryBuilder.js.map +1 -0
  116. package/dist/query/interpolate.d.ts +45 -0
  117. package/dist/query/interpolate.d.ts.map +1 -0
  118. package/dist/query/interpolate.js +51 -0
  119. package/dist/query/interpolate.js.map +1 -0
  120. package/dist/query/native.d.ts +8 -0
  121. package/dist/query/native.d.ts.map +1 -1
  122. package/dist/query/native.js +14 -0
  123. package/dist/query/native.js.map +1 -1
  124. package/dist/query/operators.d.ts +4 -0
  125. package/dist/query/operators.d.ts.map +1 -0
  126. package/dist/query/operators.js +27 -0
  127. package/dist/query/operators.js.map +1 -0
  128. package/dist/schema/Migration.d.ts +50 -3
  129. package/dist/schema/Migration.d.ts.map +1 -1
  130. package/dist/schema/Migration.js +60 -24
  131. package/dist/schema/Migration.js.map +1 -1
  132. package/dist/schema/MigrationRunner.d.ts +95 -34
  133. package/dist/schema/MigrationRunner.d.ts.map +1 -1
  134. package/dist/schema/MigrationRunner.js +523 -68
  135. package/dist/schema/MigrationRunner.js.map +1 -1
  136. package/dist/schema/Schema.d.ts +114 -0
  137. package/dist/schema/Schema.d.ts.map +1 -1
  138. package/dist/schema/Schema.js +272 -3
  139. package/dist/schema/Schema.js.map +1 -1
  140. package/dist/schema/SchemaDumper.d.ts +121 -0
  141. package/dist/schema/SchemaDumper.d.ts.map +1 -0
  142. package/dist/schema/SchemaDumper.js +365 -0
  143. package/dist/schema/SchemaDumper.js.map +1 -0
  144. package/dist/schema/Seeder.d.ts +13 -0
  145. package/dist/schema/Seeder.d.ts.map +1 -1
  146. package/dist/schema/Seeder.js +28 -5
  147. package/dist/schema/Seeder.js.map +1 -1
  148. package/dist/schema/TableBuilder.d.ts +274 -16
  149. package/dist/schema/TableBuilder.d.ts.map +1 -1
  150. package/dist/schema/TableBuilder.js +656 -52
  151. package/dist/schema/TableBuilder.js.map +1 -1
  152. package/dist/schema/catalog.d.ts +71 -0
  153. package/dist/schema/catalog.d.ts.map +1 -0
  154. package/dist/schema/catalog.js +158 -0
  155. package/dist/schema/catalog.js.map +1 -0
  156. package/dist/schema/introspect.d.ts +1 -1
  157. package/dist/schema/introspect.d.ts.map +1 -1
  158. package/dist/schema/introspect.js +26 -11
  159. package/dist/schema/introspect.js.map +1 -1
  160. package/dist/schema/types.d.ts +150 -1
  161. package/dist/schema/types.d.ts.map +1 -1
  162. package/dist/schema/types.js +11 -0
  163. package/dist/schema/types.js.map +1 -1
  164. package/dist/seeders.d.ts +7 -0
  165. package/dist/seeders.d.ts.map +1 -0
  166. package/dist/seeders.js +7 -0
  167. package/dist/seeders.js.map +1 -0
  168. package/dist/services/db.d.ts +122 -5
  169. package/dist/services/db.d.ts.map +1 -1
  170. package/dist/services/db.js +187 -14
  171. package/dist/services/db.js.map +1 -1
  172. package/dist/testing/DatabaseCleanup.d.ts +33 -7
  173. package/dist/testing/DatabaseCleanup.d.ts.map +1 -1
  174. package/dist/testing/DatabaseCleanup.js +54 -24
  175. package/dist/testing/DatabaseCleanup.js.map +1 -1
  176. package/dist/testing/DbAssertions.d.ts +48 -0
  177. package/dist/testing/DbAssertions.d.ts.map +1 -0
  178. package/dist/testing/DbAssertions.js +80 -0
  179. package/dist/testing/DbAssertions.js.map +1 -0
  180. package/dist/testing/Factory.d.ts +166 -15
  181. package/dist/testing/Factory.d.ts.map +1 -1
  182. package/dist/testing/Factory.js +497 -23
  183. package/dist/testing/Factory.js.map +1 -1
  184. package/dist/testing/TestUtils.d.ts +49 -0
  185. package/dist/testing/TestUtils.d.ts.map +1 -0
  186. package/dist/testing/TestUtils.js +79 -0
  187. package/dist/testing/TestUtils.js.map +1 -0
  188. package/dist/testing/index.d.ts +3 -1
  189. package/dist/testing/index.d.ts.map +1 -1
  190. package/dist/testing/index.js +3 -1
  191. package/dist/testing/index.js.map +1 -1
  192. package/dist/types/relations.d.ts +31 -0
  193. package/dist/types/relations.d.ts.map +1 -0
  194. package/dist/types/relations.js +20 -0
  195. package/dist/types/relations.js.map +1 -0
  196. package/index.darwin-arm64.node +0 -0
  197. package/index.darwin-x64.node +0 -0
  198. package/index.linux-arm64-gnu.node +0 -0
  199. package/index.linux-x64-gnu.node +0 -0
  200. package/index.win32-x64-msvc.node +0 -0
  201. package/package.json +32 -2
  202. package/scripts/guard-publish.mjs +15 -0
  203. package/src/AtlasProvider.ts +305 -52
  204. package/src/BaseEntity.ts +460 -41
  205. package/src/BaseModel.ts +461 -0
  206. package/src/BaseRepository.ts +1710 -421
  207. package/src/ConnectionManager.ts +298 -0
  208. package/src/ModelQuery.ts +2998 -335
  209. package/src/Transaction.ts +227 -30
  210. package/src/adapters/NapiDbAdapter.ts +224 -17
  211. package/src/console/contract.ts +96 -0
  212. package/src/console/factoryCommands.ts +73 -0
  213. package/src/console/migrationCommands.ts +448 -0
  214. package/src/console/schemaCheckCommand.ts +26 -22
  215. package/src/console/schemaDumpCommand.ts +96 -0
  216. package/src/console/schemaGenerateCommand.ts +380 -0
  217. package/src/console/seederCommands.ts +174 -0
  218. package/src/decorators/entity.ts +71 -8
  219. package/src/decorators/hooks.ts +5 -3
  220. package/src/events.ts +117 -0
  221. package/src/factories.ts +10 -0
  222. package/src/index.ts +69 -3
  223. package/src/lucid-schema.ts +22 -0
  224. package/src/metadata-keys.ts +3 -2
  225. package/src/naming/NamingStrategy.ts +23 -0
  226. package/src/orm.ts +13 -0
  227. package/src/query/DatabaseQueryBuilder.ts +2724 -0
  228. package/src/query/DmlBuilder.ts +131 -0
  229. package/src/query/QueryBuilder.ts +8 -0
  230. package/src/query/RawQueryBuilder.ts +173 -0
  231. package/src/query/interpolate.ts +73 -0
  232. package/src/query/native.ts +29 -0
  233. package/src/query/operators.ts +27 -0
  234. package/src/schema/Migration.ts +71 -3
  235. package/src/schema/MigrationRunner.ts +676 -69
  236. package/src/schema/Schema.ts +359 -3
  237. package/src/schema/SchemaDumper.ts +518 -0
  238. package/src/schema/Seeder.ts +47 -10
  239. package/src/schema/TableBuilder.ts +783 -52
  240. package/src/schema/catalog.ts +236 -0
  241. package/src/schema/introspect.ts +26 -9
  242. package/src/schema/types.ts +137 -2
  243. package/src/seeders.ts +16 -0
  244. package/src/services/db.ts +361 -21
  245. package/src/testing/DatabaseCleanup.ts +78 -25
  246. package/src/testing/DbAssertions.ts +127 -0
  247. package/src/testing/Factory.ts +837 -40
  248. package/src/testing/TestUtils.ts +123 -0
  249. package/src/testing/index.ts +12 -1
  250. package/src/types/relations.ts +43 -0
@@ -10,14 +10,65 @@ import { compileStatementNative, getAtlasDialect, } from "../query/native.js";
10
10
  import { RawSql } from "../query/QueryBuilder.js";
11
11
  import { renderDefaultValue } from "./raw.js";
12
12
  import { TYPE_KIND_MAP, } from "./types.js";
13
+ /**
14
+ * The chainable returned by `table.foreign([...])`, so the target reads in
15
+ * Knex order: `.references([...]).inTable('other').onDelete('cascade')`. It
16
+ * mutates the already-recorded constraint in place.
17
+ */
18
+ export class ForeignKeyBuilder {
19
+ #references;
20
+ constructor(references) {
21
+ this.#references = references;
22
+ }
23
+ /** Target column(s) on the referenced table. */
24
+ references(columns) {
25
+ this.#references.columns =
26
+ typeof columns === "string" ? [columns] : [...columns];
27
+ return this;
28
+ }
29
+ /**
30
+ * Referenced table. Keeps returning this builder so `.onDelete()` /
31
+ * `.onUpdate()` can follow, as in Knex — the constraint was already
32
+ * recorded on the table when `foreign()` was called, so there is nothing to
33
+ * hand back to.
34
+ */
35
+ inTable(table) {
36
+ this.#references.table = table;
37
+ return this;
38
+ }
39
+ onDelete(action) {
40
+ this.#references.onDelete = action;
41
+ return this;
42
+ }
43
+ onUpdate(action) {
44
+ this.#references.onUpdate = action;
45
+ return this;
46
+ }
47
+ }
13
48
  /** Table builder — used inside `schema.createTable(name, callback)`. */
14
49
  export class TableBuilder {
15
50
  tableName;
51
+ mode;
16
52
  #columns = [];
17
53
  #indexes = [];
18
54
  #currentColumn;
19
- constructor(tableName) {
55
+ /** Ordered ALTER TABLE ops. Empty (and unused) in `create` mode. */
56
+ #operations = [];
57
+ /** Table-level constraints. In `alter` mode these become `addConstraint` ops instead. */
58
+ #constraints = [];
59
+ #options = {};
60
+ /** The op the pending column modifiers apply to, in `alter` mode. */
61
+ #currentOp;
62
+ /**
63
+ * Whether `.nullable()` / `.notNullable()` was called on the current column.
64
+ * `ColumnDefinition.nullable` defaults to `true`, so without this flag
65
+ * `.alter()` could not tell "leave nullability alone" from "make it
66
+ * nullable" — see `AlterOperation.setNullable`.
67
+ */
68
+ #nullabilityTouched = false;
69
+ constructor(tableName, mode = "create") {
20
70
  this.tableName = tableName;
71
+ this.mode = mode;
21
72
  }
22
73
  // ─── Column types ─────────────────────────────────────────
23
74
  uuid(name) {
@@ -42,12 +93,29 @@ export class TableBuilder {
42
93
  this.#currentColumn.length = length;
43
94
  return this;
44
95
  }
45
- text(name) {
46
- return this.#addColumn(name, "text");
96
+ /**
97
+ * Text column (Lucid/Knex `text(name, textType)`). `textType` widens the
98
+ * MySQL type (`MEDIUMTEXT` / `LONGTEXT`); Postgres and SQLite have a single
99
+ * unbounded `TEXT` and ignore it.
100
+ */
101
+ text(name, textType = "text") {
102
+ return this.#addColumn(name, textType);
47
103
  }
48
104
  integer(name) {
49
105
  return this.#addColumn(name, "integer");
50
106
  }
107
+ /** 24-bit integer (Lucid/Knex `mediumint`). MySQL `MEDIUMINT`; pg/SQLite widen to `INTEGER`. */
108
+ mediumint(name) {
109
+ return this.#addColumn(name, "mediumint");
110
+ }
111
+ /** 8-bit integer (Lucid `tinyint`). MySQL `TINYINT`; Postgres widens to `SMALLINT`; SQLite `INTEGER`. */
112
+ tinyint(name) {
113
+ return this.#addColumn(name, "tinyint");
114
+ }
115
+ /** 16-bit integer (Lucid `smallint`). `SMALLINT` on pg/mysql, `INTEGER` on SQLite. */
116
+ smallint(name) {
117
+ return this.#addColumn(name, "smallint");
118
+ }
51
119
  bigInteger(name) {
52
120
  return this.#addColumn(name, "bigInteger");
53
121
  }
@@ -59,14 +127,52 @@ export class TableBuilder {
59
127
  }
60
128
  return this;
61
129
  }
130
+ /**
131
+ * Single-precision float (Lucid `float`). `REAL` on pg/sqlite, `FLOAT` on
132
+ * MySQL. `precision`/`scale` render `FLOAT(p, s)` on MySQL only — pg and
133
+ * SQLite have fixed-width floats and ignore them.
134
+ */
135
+ float(name, precision, scale) {
136
+ return this.#addFloat(name, "float", precision, scale);
137
+ }
138
+ /** Double-precision float (Lucid `double`). `DOUBLE PRECISION` on pg, `REAL` on SQLite, `DOUBLE` on MySQL. See {@link float} for precision/scale. */
139
+ double(name, precision, scale) {
140
+ return this.#addFloat(name, "double", precision, scale);
141
+ }
62
142
  boolean(name) {
63
143
  return this.#addColumn(name, "boolean");
64
144
  }
65
145
  date(name) {
66
146
  return this.#addColumn(name, "date");
67
147
  }
68
- timestamp(name) {
69
- return this.#addColumn(name, "timestamp");
148
+ /**
149
+ * Time of day (Lucid `time`). `TIME` on pg/mysql, `TEXT` on SQLite.
150
+ * `precision` renders `TIME(p)` fractional seconds (ignored on SQLite,
151
+ * which has no time type to carry it).
152
+ */
153
+ time(name, precision) {
154
+ this.#addColumn(name, "time");
155
+ if (this.#currentColumn)
156
+ this.#currentColumn.precision = precision;
157
+ return this;
158
+ }
159
+ /**
160
+ * Timestamp column (Lucid `timestamp(name, options)`).
161
+ *
162
+ * `useTz: true` selects the tz-aware type — the same thing
163
+ * {@link timestamptz} does, exposed here for Lucid's option spelling.
164
+ * `precision` renders `TIMESTAMP(p)` (ignored on SQLite, which stores
165
+ * timestamps as TEXT).
166
+ */
167
+ timestamp(name, options = {}) {
168
+ this.#addColumn(name, options.useTz ? "timestamptz" : "timestamp");
169
+ if (this.#currentColumn)
170
+ this.#currentColumn.precision = options.precision;
171
+ return this;
172
+ }
173
+ /** Alias of {@link timestamp} (Lucid `dateTime`). Use `{ useTz: true }` or {@link timestamptz} for a tz-aware column. */
174
+ dateTime(name, options = {}) {
175
+ return this.timestamp(name, options);
70
176
  }
71
177
  /**
72
178
  * `timestamp WITH time zone` — Postgres normalises every writer (atlas,
@@ -83,8 +189,55 @@ export class TableBuilder {
83
189
  json(name) {
84
190
  return this.#addColumn(name, "json");
85
191
  }
86
- binary(name) {
87
- return this.#addColumn(name, "binary");
192
+ /**
193
+ * Binary JSON (Lucid/Knex `jsonb`). `JSONB` on pg, `JSON` on MySQL, `TEXT`
194
+ * on SQLite.
195
+ *
196
+ * Deviation, named: atlas's {@link json} already maps to `JSONB` on
197
+ * Postgres (it predates this method), where Lucid's `json()` maps to
198
+ * `json`. Leaving `json()` alone avoids silently rewriting the physical
199
+ * type of existing columns and desyncing `SchemaCheck`, so on Postgres the
200
+ * two spellings coincide.
201
+ */
202
+ jsonb(name) {
203
+ return this.#addColumn(name, "jsonb");
204
+ }
205
+ /**
206
+ * Binary blob (Lucid/Knex `binary(name, length)`). `BYTEA` on pg, `BLOB` on
207
+ * SQLite; on MySQL `length` selects `VARBINARY(n)` over `BLOB`.
208
+ */
209
+ binary(name, length) {
210
+ this.#addColumn(name, "binary");
211
+ if (this.#currentColumn)
212
+ this.#currentColumn.length = length;
213
+ return this;
214
+ }
215
+ /**
216
+ * A column typed with a verbatim dialect type (Lucid/Knex `specificType`) —
217
+ * the escape hatch for types atlas has no method for (`inet`, `tsvector`,
218
+ * `geometry(Point, 4326)`…).
219
+ *
220
+ * Deviation, named: Knex passes the string straight through. Atlas cannot —
221
+ * it lands verbatim in DDL, so the Rust compiler validates it against a
222
+ * narrow grammar (letters, digits, spaces, `_`, and one parenthesised
223
+ * argument list) and rejects anything else with `E_UNSAFE_SQL`.
224
+ */
225
+ specificType(name, type) {
226
+ this.#addColumn(name, "specificType");
227
+ if (this.#currentColumn)
228
+ this.#currentColumn.rawType = type;
229
+ return this;
230
+ }
231
+ /**
232
+ * Fixed value-set column (Lucid `enum`). MySQL renders a native `ENUM(...)`;
233
+ * Postgres and SQLite render `TEXT` plus a `CHECK (col IN (...))` that pins the
234
+ * value set. At least one value is required.
235
+ */
236
+ enum(name, values) {
237
+ this.#addColumn(name, "enum");
238
+ if (this.#currentColumn)
239
+ this.#currentColumn.values = values;
240
+ return this;
88
241
  }
89
242
  // ─── Shortcuts ────────────────────────────────────────────
90
243
  /**
@@ -104,9 +257,8 @@ export class TableBuilder {
104
257
  * t.uuid('id').primary() // no DEFAULT
105
258
  * // and at insert: db.insert({ id: crypto.randomUUID(), ... })
106
259
  *
107
- * See `AUDIT-migration-templates.md` (shipped at the package root) for
108
- * the full audit and the escape-hatch procedure if a future story makes
109
- * the helper dialect-aware.
260
+ * (A dialect-aware escape hatch can be added later if a future story needs
261
+ * per-dialect PK defaults.)
110
262
  */
111
263
  id() {
112
264
  return this.uuid("id").primary().defaultTo(new RawSql("gen_random_uuid()"));
@@ -130,34 +282,46 @@ export class TableBuilder {
130
282
  * t.timestamp('updated_at').notNullable()
131
283
  * // and at insert: db.insert({ created_at: new Date().toISOString(), ... })
132
284
  *
133
- * See `AUDIT-migration-templates.md` (shipped at the package root) for
134
- * the full audit and the escape-hatch procedure if a future story makes
135
- * the helper dialect-aware.
285
+ * (A dialect-aware escape hatch can be added later if a future story needs
286
+ * per-dialect PK defaults.)
136
287
  */
137
- timestamps() {
138
- this.timestamp("created_at").notNullable().defaultTo(new RawSql("NOW()"));
139
- this.timestamp("updated_at").notNullable().defaultTo(new RawSql("NOW()"));
288
+ /**
289
+ * Add `created_at` / `updated_at` columns (Adonis Lucid / Knex
290
+ * `timestamps(useTimestamps, defaultToNow)`).
291
+ *
292
+ * - `useTimestamps` (default `true`): `timestamp` type; `false` → `dateTime`.
293
+ * - `defaultToNow` (default `true`): `NOT NULL DEFAULT CURRENT_TIMESTAMP`;
294
+ * `false` → nullable, no default.
295
+ *
296
+ * The default uses `CURRENT_TIMESTAMP`, which every dialect understands —
297
+ * unlike `NOW()`, which SQLite rejects. So `timestamps()` and the Lucid
298
+ * `timestamps(true, true)` form both port to SQLite/Postgres/MySQL unchanged.
299
+ */
300
+ timestamps(useTimestamps = true, defaultToNow = true) {
301
+ const add = (name) => {
302
+ if (useTimestamps)
303
+ this.timestamp(name);
304
+ else
305
+ this.dateTime(name);
306
+ if (defaultToNow) {
307
+ this.notNullable().defaultTo(new RawSql("CURRENT_TIMESTAMP"));
308
+ }
309
+ };
310
+ add("created_at");
311
+ add("updated_at");
140
312
  return this;
141
313
  }
142
314
  // ─── Column modifiers ─────────────────────────────────────
143
- primary() {
144
- if (this.#currentColumn)
145
- this.#currentColumn.primary = true;
146
- return this;
147
- }
148
315
  notNullable() {
149
316
  if (this.#currentColumn)
150
317
  this.#currentColumn.nullable = false;
318
+ this.#nullabilityTouched = true;
151
319
  return this;
152
320
  }
153
321
  nullable() {
154
322
  if (this.#currentColumn)
155
323
  this.#currentColumn.nullable = true;
156
- return this;
157
- }
158
- unique() {
159
- if (this.#currentColumn)
160
- this.#currentColumn.unique = true;
324
+ this.#nullabilityTouched = true;
161
325
  return this;
162
326
  }
163
327
  /**
@@ -171,9 +335,267 @@ export class TableBuilder {
171
335
  }
172
336
  return this;
173
337
  }
174
- references(table, column = "id") {
338
+ /** MySQL `UNSIGNED` numeric modifier (Lucid `unsigned()`). No-op on pg/sqlite. */
339
+ unsigned() {
175
340
  if (this.#currentColumn)
176
- this.#currentColumn.references = { table, column };
341
+ this.#currentColumn.unsigned = true;
342
+ return this;
343
+ }
344
+ /**
345
+ * Declare the current column a foreign key.
346
+ *
347
+ * - `references('users', 'id')` — atlas form `(table, column='id')`.
348
+ * - `references('users.id')` — Lucid/Knex dotted `'table.column'` shorthand, so
349
+ * a migration copied from Lucid resolves the target the same way. A single
350
+ * argument without a dot is treated as the table name (column defaults to
351
+ * `id`), preserving the atlas one-arg behaviour.
352
+ */
353
+ references(tableOrPath, column) {
354
+ let table = tableOrPath;
355
+ let col = column ?? "id";
356
+ const dot = tableOrPath.indexOf(".");
357
+ // Dotted shorthand only when no explicit column was passed — an explicit
358
+ // second arg always wins, so `references('a.b', 'c')` stays (table 'a.b').
359
+ if (dot !== -1 && column === undefined) {
360
+ table = tableOrPath.slice(0, dot);
361
+ col = tableOrPath.slice(dot + 1);
362
+ }
363
+ if (this.#currentColumn) {
364
+ this.#currentColumn.references = { table, column: col };
365
+ }
366
+ return this;
367
+ }
368
+ /**
369
+ * Referential action for the current column's foreign key `ON DELETE`
370
+ * (Lucid parity). Must follow {@link references}.
371
+ */
372
+ onDelete(action) {
373
+ if (this.#currentColumn?.references) {
374
+ this.#currentColumn.references.onDelete = action;
375
+ }
376
+ return this;
377
+ }
378
+ /** Referential action for the current column's foreign key `ON UPDATE`. Must follow {@link references}. */
379
+ onUpdate(action) {
380
+ if (this.#currentColumn?.references) {
381
+ this.#currentColumn.references.onUpdate = action;
382
+ }
383
+ return this;
384
+ }
385
+ /**
386
+ * Comment the current **column** (Lucid/Knex column `comment()`). Inline on
387
+ * MySQL, a separate `COMMENT ON COLUMN` on Postgres, dropped on SQLite.
388
+ *
389
+ * Deviation, named: Knex's `table.comment()` is the TABLE comment, because
390
+ * its column methods return a separate column builder. Atlas flattens the
391
+ * column modifiers onto the table builder (`.notNullable()`, `.unique()`,
392
+ * `.defaultTo()` all work this way), so `comment()` follows that same rule
393
+ * and the table comment is {@link tableComment}. Resolving it by "is a
394
+ * column pending?" would be exactly the kind of guessing that bites later.
395
+ */
396
+ comment(text) {
397
+ if (this.#currentColumn)
398
+ this.#currentColumn.comment = text;
399
+ return this;
400
+ }
401
+ /** Collate the current **column** (Lucid/Knex column `collate()`). See {@link comment} for why the table form is {@link tableCollate}. */
402
+ collate(collation) {
403
+ if (this.#currentColumn)
404
+ this.#currentColumn.collate = collation;
405
+ return this;
406
+ }
407
+ /**
408
+ * Place an added column first (Lucid/Knex `first()`). MySQL-only —
409
+ * Postgres and SQLite always append, and the Rust compiler raises
410
+ * `E_UNSUPPORTED` rather than dropping the instruction silently.
411
+ */
412
+ first() {
413
+ if (this.#currentColumn)
414
+ this.#currentColumn.position = { at: "first" };
415
+ return this;
416
+ }
417
+ /** Place an added column after `column` (Lucid/Knex `after()`). MySQL-only — see {@link first}. */
418
+ after(column) {
419
+ if (this.#currentColumn) {
420
+ this.#currentColumn.position = { at: "after", column };
421
+ }
422
+ return this;
423
+ }
424
+ // ─── CHECK constraints ────────────────────────────────────
425
+ /** `CHECK (col > 0)` on the current column (Lucid/Knex `checkPositive`). */
426
+ checkPositive(constraintName) {
427
+ return this.#addCheck((column) => ({ check: "positive", column }), constraintName);
428
+ }
429
+ /** `CHECK (col < 0)` on the current column (Lucid/Knex `checkNegative`). */
430
+ checkNegative(constraintName) {
431
+ return this.#addCheck((column) => ({ check: "negative", column }), constraintName);
432
+ }
433
+ /** `CHECK (col IN (…))` on the current column (Lucid/Knex `checkIn`). Values are quoted, never interpolated raw. */
434
+ checkIn(values, constraintName) {
435
+ return this.#addCheck((column) => ({ check: "in", column, values: [...values] }), constraintName);
436
+ }
437
+ /** `CHECK (col NOT IN (…))` on the current column (Lucid/Knex `checkNotIn`). */
438
+ checkNotIn(values, constraintName) {
439
+ return this.#addCheck((column) => ({ check: "notIn", column, values: [...values] }), constraintName);
440
+ }
441
+ /**
442
+ * `CHECK (col BETWEEN lo AND hi)` on the current column (Lucid/Knex
443
+ * `checkBetween`). Accepts one `[min, max]` interval or a list of them —
444
+ * several intervals are OR'd together, as in Knex.
445
+ */
446
+ checkBetween(range, constraintName) {
447
+ // A single [min, max] vs a list of intervals: the first element of a
448
+ // list-of-intervals is itself an array.
449
+ const ranges = Array.isArray(range[0])
450
+ ? range.map((r) => [...r])
451
+ : [[...range]];
452
+ return this.#addCheck((column) => ({ check: "between", column, ranges }), constraintName);
453
+ }
454
+ /** `CHECK (LENGTH(col) <op> n)` on the current column (Lucid/Knex `checkLength`). The operator is allow-listed by the Rust compiler. */
455
+ checkLength(operator, length, constraintName) {
456
+ return this.#addCheck((column) => ({ check: "length", column, operator, length }), constraintName);
457
+ }
458
+ /**
459
+ * `CHECK (col ~ 'pattern')` on the current column (Lucid/Knex `checkRegex`).
460
+ * Postgres spells it `~`; MySQL and SQLite use `REGEXP`.
461
+ *
462
+ * SQLite parses `REGEXP` but ships no implementation — the constraint only
463
+ * works if the connection registers a `regexp` function. Knex behaves the
464
+ * same way, so this is parity rather than a new trap, but it is worth
465
+ * knowing before you rely on it there.
466
+ */
467
+ checkRegex(pattern, constraintName) {
468
+ return this.#addCheck((column) => ({ check: "regex", column, pattern }), constraintName);
469
+ }
470
+ /**
471
+ * A free-form `CHECK (predicate)` (Lucid/Knex `check`). The predicate is
472
+ * emitted verbatim — exactly as trusted as {@link Schema.raw}, so never
473
+ * build it from user input. Prefer the typed `check*` helpers, which are
474
+ * safe by construction.
475
+ */
476
+ check(predicate, constraintName) {
477
+ this.#pushConstraint({
478
+ constraint: "check",
479
+ name: constraintName,
480
+ expr: { check: "raw", predicate },
481
+ });
482
+ return this;
483
+ }
484
+ /** Drop named CHECK constraints (Lucid/Knex `dropChecks`). */
485
+ dropChecks(...constraintNames) {
486
+ this.#assertAlterMode("dropChecks()");
487
+ for (const name of constraintNames) {
488
+ this.#pushStandaloneOp({ op: "dropConstraint", name });
489
+ }
490
+ return this;
491
+ }
492
+ // ─── Table-level constraints ──────────────────────────────
493
+ /**
494
+ * With no argument, mark the current column as the primary key (the
495
+ * existing column modifier). With a column list, declare a composite
496
+ * `PRIMARY KEY (…)` table constraint (Lucid/Knex `primary([...])`).
497
+ */
498
+ primary(columns, constraintName) {
499
+ if (columns === undefined) {
500
+ if (this.#currentColumn)
501
+ this.#currentColumn.primary = true;
502
+ return this;
503
+ }
504
+ this.#pushConstraint({
505
+ constraint: "primary",
506
+ name: constraintName,
507
+ columns: [...columns],
508
+ });
509
+ return this;
510
+ }
511
+ /**
512
+ * With no argument, mark the current column `UNIQUE` (the existing column
513
+ * modifier). With a column list, declare a composite `UNIQUE (…)` table
514
+ * constraint (Lucid/Knex `unique([...])`).
515
+ *
516
+ * Note this is a real constraint, unlike {@link uniqueIndex}, which creates
517
+ * a separate `CREATE UNIQUE INDEX`.
518
+ */
519
+ unique(columns, constraintName) {
520
+ if (columns === undefined) {
521
+ if (this.#currentColumn)
522
+ this.#currentColumn.unique = true;
523
+ return this;
524
+ }
525
+ this.#pushConstraint({
526
+ constraint: "unique",
527
+ name: constraintName ?? this.#constraintName(columns, "unique"),
528
+ columns: [...columns],
529
+ });
530
+ return this;
531
+ }
532
+ /**
533
+ * Declare a composite foreign key (Lucid/Knex
534
+ * `foreign([...]).references([...]).inTable(…)`). Returns a small chainable
535
+ * so the target reads in Knex order; the constraint is recorded up front
536
+ * and filled in as you chain.
537
+ */
538
+ foreign(columns, constraintName) {
539
+ const cols = typeof columns === "string" ? [columns] : [...columns];
540
+ const references = { table: "", columns: [] };
541
+ this.#pushConstraint({
542
+ constraint: "foreign",
543
+ name: constraintName ?? this.#constraintName(cols, "foreign"),
544
+ columns: cols,
545
+ references,
546
+ });
547
+ // The constraint is already recorded; the builder fills `references` in
548
+ // place as the caller chains, so order of arrival doesn't matter.
549
+ return new ForeignKeyBuilder(references);
550
+ }
551
+ // ─── Dropping constraints ─────────────────────────────────
552
+ /** Drop the primary key (Lucid/Knex `dropPrimary`). MySQL drops it by keyword; Postgres by name (default `<table>_pkey`). */
553
+ dropPrimary(constraintName) {
554
+ this.#assertAlterMode("dropPrimary()");
555
+ this.#pushStandaloneOp({ op: "dropPrimary", name: constraintName });
556
+ return this;
557
+ }
558
+ /** Drop a unique constraint by columns (using the default name) or by explicit name (Lucid/Knex `dropUnique`). */
559
+ dropUnique(columns, constraintName) {
560
+ this.#assertAlterMode("dropUnique()");
561
+ this.#pushStandaloneOp({
562
+ op: "dropUnique",
563
+ name: constraintName ?? this.#constraintName(columns, "unique"),
564
+ });
565
+ return this;
566
+ }
567
+ /** Drop a foreign key by columns (using the default name) or by explicit name (Lucid/Knex `dropForeign`). */
568
+ dropForeign(columns, constraintName) {
569
+ this.#assertAlterMode("dropForeign()");
570
+ this.#pushStandaloneOp({
571
+ op: "dropForeign",
572
+ name: constraintName ?? this.#constraintName(columns, "foreign"),
573
+ });
574
+ return this;
575
+ }
576
+ /** Drop `created_at` + `updated_at` (Lucid/Knex `dropTimestamps`). */
577
+ dropTimestamps() {
578
+ return this.dropColumns("created_at", "updated_at");
579
+ }
580
+ // ─── Table options ────────────────────────────────────────
581
+ /** MySQL storage engine (Lucid/Knex `engine`). Ignored on pg/sqlite. */
582
+ engine(name) {
583
+ this.#options.engine = name;
584
+ return this;
585
+ }
586
+ /** MySQL default charset (Lucid/Knex `charset`). Ignored on pg/sqlite. */
587
+ charset(name) {
588
+ this.#options.charset = name;
589
+ return this;
590
+ }
591
+ /** MySQL default collation for the table. Named `tableCollate` because {@link collate} is the column modifier — see {@link comment}. */
592
+ tableCollate(name) {
593
+ this.#options.collate = name;
594
+ return this;
595
+ }
596
+ /** Table comment. Named `tableComment` because {@link comment} is the column modifier — see there for why. */
597
+ tableComment(text) {
598
+ this.#options.comment = text;
177
599
  return this;
178
600
  }
179
601
  // ─── Indexes ──────────────────────────────────────────────
@@ -195,6 +617,78 @@ export class TableBuilder {
195
617
  });
196
618
  return this;
197
619
  }
620
+ // ─── ALTER TABLE operations ───────────────────────────────
621
+ /**
622
+ * Apply the pending column definition as a type change instead of an
623
+ * `ADD COLUMN` (Lucid/Knex `alter()`). Must follow a column-type method.
624
+ *
625
+ * Nullability moves only if `.nullable()` / `.notNullable()` was called
626
+ * before this — a bare `t.string('x').alter()` changes the type and leaves
627
+ * the NOT NULL constraint exactly as it is.
628
+ *
629
+ * SQLite cannot alter a column in place; the Rust compiler rejects it with
630
+ * `E_UNSUPPORTED` rather than emitting a table rebuild behind your back.
631
+ */
632
+ alter() {
633
+ this.#assertAlterMode("alter()");
634
+ const pending = this.#currentOp;
635
+ if (!pending) {
636
+ throw new Error("E_ALTER_MISUSE: alter() must follow a column definition, e.g. table.string('email').alter()");
637
+ }
638
+ if (pending.op === "addColumn") {
639
+ const converted = {
640
+ op: "alterColumn",
641
+ column: pending.column,
642
+ setNullable: this.#nullabilityTouched
643
+ ? pending.column.nullable
644
+ : undefined,
645
+ };
646
+ this.#operations[this.#operations.indexOf(pending)] = converted;
647
+ this.#currentOp = converted;
648
+ }
649
+ return this;
650
+ }
651
+ /** Drop a column (Lucid/Knex `dropColumn`). */
652
+ dropColumn(name) {
653
+ this.#assertAlterMode("dropColumn()");
654
+ this.#pushStandaloneOp({ op: "dropColumn", name });
655
+ return this;
656
+ }
657
+ /** Drop several columns in call order (Lucid/Knex `dropColumns`). */
658
+ dropColumns(...names) {
659
+ for (const name of names)
660
+ this.dropColumn(name);
661
+ return this;
662
+ }
663
+ /** Rename a column (Lucid/Knex `renameColumn`). */
664
+ renameColumn(from, to) {
665
+ this.#assertAlterMode("renameColumn()");
666
+ this.#pushStandaloneOp({ op: "renameColumn", from, to });
667
+ return this;
668
+ }
669
+ /**
670
+ * Make an existing column nullable — `DROP NOT NULL` (Lucid/Knex
671
+ * `setNullable`).
672
+ *
673
+ * **Deviation from Knex, named deliberately.** Knex supports this on every
674
+ * dialect by querying `columnInfo()` at runtime to recover the column's
675
+ * type. Atlas compiles SQL synchronously in Rust with no round-trip, so
676
+ * this is Postgres-only — Postgres is the one dialect whose syntax needs no
677
+ * type. On MySQL use `table.<type>('col').nullable().alter()`, which
678
+ * restates the type; SQLite cannot alter a column in place at all. Both
679
+ * raise `E_UNSUPPORTED` with the alternative spelled out.
680
+ */
681
+ setNullable(name) {
682
+ this.#assertAlterMode("setNullable()");
683
+ this.#pushStandaloneOp({ op: "setNullable", name, nullable: true });
684
+ return this;
685
+ }
686
+ /** Make an existing column `NOT NULL` (Lucid/Knex `dropNullable`). Postgres-only — see {@link setNullable}. */
687
+ dropNullable(name) {
688
+ this.#assertAlterMode("dropNullable()");
689
+ this.#pushStandaloneOp({ op: "setNullable", name, nullable: false });
690
+ return this;
691
+ }
198
692
  // ─── Accessors ────────────────────────────────────────────
199
693
  getColumns() {
200
694
  return [...this.#columns];
@@ -202,32 +696,125 @@ export class TableBuilder {
202
696
  getIndexes() {
203
697
  return [...this.#indexes];
204
698
  }
699
+ /** Ordered ALTER TABLE operations. Empty in `create` mode. */
700
+ getOperations() {
701
+ return [...this.#operations];
702
+ }
205
703
  /** Compile to SQL statements via the Rust compiler. */
206
- toStatements(dialect = getAtlasDialect()) {
207
- const spec = {
208
- kind: "createTable",
209
- table: this.tableName,
210
- columns: this.#columns.map((c) => ({
211
- name: c.name,
212
- kind: TYPE_KIND_MAP[c.type],
213
- length: c.length ?? null,
214
- precision: c.precision ?? null,
215
- scale: c.scale ?? null,
216
- nullable: c.nullable,
217
- primary: c.primary,
218
- autoIncrement: c.autoIncrement ?? false,
219
- unique: c.unique,
220
- default: c.defaultValue ?? null,
221
- references: c.references ?? null,
222
- })),
223
- indexes: this.#indexes.map((i) => ({
224
- name: i.name,
225
- columns: i.columns,
226
- unique: i.unique,
227
- })),
228
- ifNotExists: false,
704
+ toStatements(dialect = getAtlasDialect(), options = {}) {
705
+ const spec = this.mode === "alter"
706
+ ? {
707
+ kind: "alterTable",
708
+ table: this.tableName,
709
+ operations: this.#operations.map((op) => op.op === "addColumn"
710
+ ? { op: op.op, column: this.#serializeColumn(op.column) }
711
+ : op.op === "alterColumn"
712
+ ? {
713
+ op: op.op,
714
+ column: this.#serializeColumn(op.column),
715
+ setNullable: op.setNullable ?? null,
716
+ }
717
+ : op),
718
+ }
719
+ : {
720
+ kind: "createTable",
721
+ table: this.tableName,
722
+ columns: this.#columns.map((c) => this.#serializeColumn(c)),
723
+ indexes: this.#indexes.map((i) => ({
724
+ name: i.name,
725
+ columns: i.columns,
726
+ unique: i.unique,
727
+ })),
728
+ ifNotExists: options.ifNotExists ?? false,
729
+ constraints: this.#constraints,
730
+ options: this.#options,
731
+ };
732
+ const statements = compileStatementNative(spec, dialect).statements;
733
+ // `alterTable` carries no index list — an index added alongside an
734
+ // ALTER compiles to its own CREATE INDEX, appended in declaration order.
735
+ if (this.mode === "alter" && this.#indexes.length > 0) {
736
+ for (const idx of this.#indexes) {
737
+ statements.push(...compileStatementNative({
738
+ kind: "createIndex",
739
+ table: this.tableName,
740
+ name: idx.name,
741
+ columns: idx.columns,
742
+ unique: idx.unique,
743
+ }, dialect).statements);
744
+ }
745
+ }
746
+ return statements;
747
+ }
748
+ /** Flatten a column into the wire shape the Rust `ColumnDef` deserialises. */
749
+ #serializeColumn(c) {
750
+ return {
751
+ name: c.name,
752
+ kind: TYPE_KIND_MAP[c.type],
753
+ length: c.length ?? null,
754
+ precision: c.precision ?? null,
755
+ scale: c.scale ?? null,
756
+ // Flattened into the Rust ColumnTypeSpec — each read for one kind only.
757
+ values: c.values ?? null,
758
+ rawType: c.rawType ?? null,
759
+ nullable: c.nullable,
760
+ primary: c.primary,
761
+ autoIncrement: c.autoIncrement ?? false,
762
+ unique: c.unique,
763
+ unsigned: c.unsigned ?? false,
764
+ default: c.defaultValue ?? null,
765
+ references: c.references ?? null,
766
+ comment: c.comment ?? null,
767
+ collate: c.collate ?? null,
768
+ position: c.position ?? null,
229
769
  };
230
- return compileStatementNative(spec, dialect).statements;
770
+ }
771
+ /**
772
+ * Record a constraint. In `create` mode it renders inside the
773
+ * `CREATE TABLE`; in `alter` mode it becomes an `ADD CONSTRAINT`, kept in
774
+ * call order with the surrounding column operations.
775
+ */
776
+ #pushConstraint(spec) {
777
+ if (this.mode === "alter") {
778
+ this.#operations.push({ op: "addConstraint", constraint: spec });
779
+ }
780
+ else {
781
+ this.#constraints.push(spec);
782
+ }
783
+ }
784
+ /** Build a CHECK against the pending column. */
785
+ #addCheck(build, constraintName) {
786
+ const column = this.#currentColumn?.name;
787
+ if (!column) {
788
+ throw new Error("E_CHECK_MISUSE: a check* helper must follow a column definition, e.g. table.integer('qty').checkPositive()");
789
+ }
790
+ this.#pushConstraint({
791
+ constraint: "check",
792
+ name: constraintName,
793
+ expr: build(column),
794
+ });
795
+ return this;
796
+ }
797
+ /**
798
+ * Default constraint name, following Knex's `<table>_<columns>_<suffix>`
799
+ * convention so `unique([...])` and `dropUnique([...])` agree without the
800
+ * caller naming anything. Distinct from {@link uniqueIndex}, which names a
801
+ * separate INDEX object `idx_…`.
802
+ */
803
+ #constraintName(columns, suffix) {
804
+ const cols = typeof columns === "string" ? [columns] : columns;
805
+ return `${this.tableName}_${cols.join("_")}_${suffix}`;
806
+ }
807
+ #assertAlterMode(method) {
808
+ if (this.mode !== "alter") {
809
+ throw new Error(`E_ALTER_MISUSE: ${method} is only available inside schema.alterTable() — a new table has nothing to alter`);
810
+ }
811
+ }
812
+ /** Record an op that takes no column modifiers, so `.nullable()` etc. can't silently attach to it. */
813
+ #pushStandaloneOp(op) {
814
+ this.#operations.push(op);
815
+ this.#currentColumn = undefined;
816
+ this.#currentOp = undefined;
817
+ this.#nullabilityTouched = false;
231
818
  }
232
819
  #addColumn(name, type) {
233
820
  const col = {
@@ -239,6 +826,23 @@ export class TableBuilder {
239
826
  };
240
827
  this.#columns.push(col);
241
828
  this.#currentColumn = col;
829
+ this.#nullabilityTouched = false;
830
+ if (this.mode === "alter") {
831
+ const op = {
832
+ op: "addColumn",
833
+ column: col,
834
+ };
835
+ this.#operations.push(op);
836
+ this.#currentOp = op;
837
+ }
838
+ return this;
839
+ }
840
+ #addFloat(name, type, precision, scale) {
841
+ this.#addColumn(name, type);
842
+ if (this.#currentColumn) {
843
+ this.#currentColumn.precision = precision;
844
+ this.#currentColumn.scale = scale;
845
+ }
242
846
  return this;
243
847
  }
244
848
  #addIncrements(name, type) {