@zerotal/orm 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/LICENSE +21 -0
- package/README.md +170 -0
- package/package.json +58 -0
- package/src/casts/Cast.ts +200 -0
- package/src/commands/DbSeedCommand.ts +71 -0
- package/src/commands/MakeFactoryCommand.ts +59 -0
- package/src/commands/MakeMigrationCommand.ts +109 -0
- package/src/commands/MakeModelCommand.ts +83 -0
- package/src/commands/MakeSeederCommand.ts +50 -0
- package/src/commands/MigrateCommand.ts +60 -0
- package/src/commands/MigrateFreshCommand.ts +41 -0
- package/src/commands/MigrateGenerateCommand.ts +110 -0
- package/src/commands/MigrateRollbackCommand.ts +43 -0
- package/src/commands/MigrateStatusCommand.ts +49 -0
- package/src/commands/_loadMigrations.ts +34 -0
- package/src/commands/index.ts +30 -0
- package/src/config.ts +182 -0
- package/src/conventions.ts +67 -0
- package/src/db/DB.ts +486 -0
- package/src/db/NPlusOneDetector.ts +176 -0
- package/src/db/QueryBuilder.ts +2458 -0
- package/src/db/ReadWriteRouter.ts +96 -0
- package/src/db/TransactionContext.ts +13 -0
- package/src/db/dialects/MysqlDialect.ts +57 -0
- package/src/db/dialects/PostgresDialect.ts +55 -0
- package/src/db/dialects/SqliteDialect.ts +54 -0
- package/src/db/dialects/index.ts +25 -0
- package/src/db/dialects/types.ts +67 -0
- package/src/db/resolver.ts +30 -0
- package/src/db/sql-types.ts +12 -0
- package/src/db/types.ts +296 -0
- package/src/errors/MassAssignmentError.ts +25 -0
- package/src/errors/MigrationError.ts +18 -0
- package/src/errors/ModelNotFoundError.ts +21 -0
- package/src/errors/NPlusOneError.ts +6 -0
- package/src/errors/RelationNotLoadedError.ts +19 -0
- package/src/errors/StateError.ts +18 -0
- package/src/errors/TransactionError.ts +13 -0
- package/src/errors/UnsupportedDialectError.ts +18 -0
- package/src/errors/index.ts +7 -0
- package/src/events.ts +112 -0
- package/src/global.d.ts +17 -0
- package/src/implicitBinding.ts +73 -0
- package/src/index.ts +255 -0
- package/src/model/BaseModel.ts +2499 -0
- package/src/model/ModelQueryBuilder.ts +1808 -0
- package/src/model/Observer.ts +73 -0
- package/src/model/OrmContext.ts +71 -0
- package/src/model/ReactiveProxy.ts +53 -0
- package/src/model/SoftDeletes.ts +108 -0
- package/src/model/State.ts +290 -0
- package/src/model/decorators/_metadata.ts +211 -0
- package/src/model/decorators/_registerRelation.ts +20 -0
- package/src/model/decorators/belongsTo.ts +38 -0
- package/src/model/decorators/column.ts +278 -0
- package/src/model/decorators/hasMany.ts +34 -0
- package/src/model/decorators/hasManyThrough.ts +50 -0
- package/src/model/decorators/hasOne.ts +34 -0
- package/src/model/decorators/hasOneThrough.ts +40 -0
- package/src/model/decorators/manyToMany.ts +55 -0
- package/src/model/decorators/morphMany.ts +38 -0
- package/src/model/decorators/morphOne.ts +38 -0
- package/src/model/decorators/morphTo.ts +51 -0
- package/src/model/decorators/morphToMany.ts +49 -0
- package/src/model/decorators/morphedByMany.ts +46 -0
- package/src/model/decorators/table.ts +124 -0
- package/src/model/hooks/HookRegistry.ts +110 -0
- package/src/model/mixins.ts +536 -0
- package/src/model/payload.ts +114 -0
- package/src/model/relations/RelationRegistry.ts +184 -0
- package/src/observability.ts +210 -0
- package/src/provider/DatabaseProvider.ts +266 -0
- package/src/schema/Blueprint.ts +900 -0
- package/src/schema/ColumnDefinition.ts +517 -0
- package/src/schema/Migration.ts +34 -0
- package/src/schema/MigrationCodegen.ts +108 -0
- package/src/schema/MigrationRunner.ts +351 -0
- package/src/schema/ModelInspector.ts +133 -0
- package/src/schema/Schema.ts +140 -0
- package/src/schema/SchemaDiffer.ts +137 -0
- package/src/schema/SchemaInspector.ts +164 -0
- package/src/schema/__test_migrations__/001_create_test_table.ts +15 -0
- package/src/schema/autoMigrate.ts +154 -0
- package/src/schema/index.ts +28 -0
- package/src/seeding/Seeder.ts +46 -0
- package/src/support/identifiers.ts +62 -0
|
@@ -0,0 +1,900 @@
|
|
|
1
|
+
import { ColumnBuilder, ForeignIdColumnBuilder, ForeignKeyBuilder } from "./ColumnDefinition.ts";
|
|
2
|
+
|
|
3
|
+
// ── Storage interface ─────────────────────────────────────────────────────────
|
|
4
|
+
// Blueprint stores columns as `IColumnSQL` so the phantom `Locked` type is
|
|
5
|
+
// erased at the array boundary. The SQL compilation loop only ever calls
|
|
6
|
+
// `toColumnSQL()`, `name`, and `wantsIndex`.
|
|
7
|
+
|
|
8
|
+
interface IColumnSQL {
|
|
9
|
+
readonly name: string;
|
|
10
|
+
readonly wantsIndex: boolean;
|
|
11
|
+
readonly isAlter: boolean;
|
|
12
|
+
toColumnSQL(dialect?: "sqlite" | "mysql" | "postgres"): string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface IndexEntry {
|
|
16
|
+
columns: string[];
|
|
17
|
+
name: string | undefined;
|
|
18
|
+
unique: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface RenameEntry {
|
|
22
|
+
from: string;
|
|
23
|
+
to: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface DropIndexEntry {
|
|
27
|
+
nameOrColumns: string | string[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ── Blueprint ─────────────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The fluent table builder passed as `table` to the callbacks of
|
|
34
|
+
* {@link Schema.create}, {@link Schema.createIfNotExists} and {@link Schema.table}.
|
|
35
|
+
*
|
|
36
|
+
* Each method call records an intent (a column, an index, a foreign key, a drop,
|
|
37
|
+
* a rename) on the blueprint; the accumulated intent is compiled to SQL only when
|
|
38
|
+
* the surrounding `Schema` helper calls {@link Blueprint.toCreateSQL} (for
|
|
39
|
+
* `create`) or {@link Blueprint.toAlterSQL} (for `table`). Column-type methods
|
|
40
|
+
* return a {@link ColumnBuilder} (or {@link ForeignIdColumnBuilder}) so per-column
|
|
41
|
+
* modifiers can be chained; table-level methods return `this` so index and
|
|
42
|
+
* constraint calls can be chained.
|
|
43
|
+
*
|
|
44
|
+
* @remarks
|
|
45
|
+
* This ORM targets SQLite (via `Bun.sql`) as its primary dialect, so the concrete
|
|
46
|
+
* storage type of every column collapses to one of SQLite's storage classes:
|
|
47
|
+
* `INTEGER`, `REAL`, `TEXT` or `BLOB`. The many distinct column-type methods
|
|
48
|
+
* (`bigInteger`, `mediumText`, `char`, `decimal`, …) exist for a familiar,
|
|
49
|
+
* expressive schema API and multi-database portability, but on SQLite several of
|
|
50
|
+
* them compile to
|
|
51
|
+
* the same underlying type and length/precision arguments are accepted yet
|
|
52
|
+
* ignored. Notes on the affected methods call this out. Dialect-specific
|
|
53
|
+
* behaviour (MySQL `MODIFY COLUMN`, PostgreSQL per-attribute `ALTER COLUMN`,
|
|
54
|
+
* fulltext/spatial indexes) is only exercised on the ALTER path via
|
|
55
|
+
* {@link Blueprint.toAlterSQL}.
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* ```ts
|
|
59
|
+
* await Schema.create('posts', (table) => {
|
|
60
|
+
* table.id();
|
|
61
|
+
* table.string('title');
|
|
62
|
+
* table.text('body').nullable();
|
|
63
|
+
* table.enum('status', ['draft', 'published']).default('draft');
|
|
64
|
+
* table.foreignId('author_id').constrained('users').cascadeOnDelete();
|
|
65
|
+
* table.timestamps();
|
|
66
|
+
*
|
|
67
|
+
* table.unique('title');
|
|
68
|
+
* table.index(['status', 'author_id']);
|
|
69
|
+
* });
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
export class Blueprint {
|
|
73
|
+
private _cols: IColumnSQL[] = [];
|
|
74
|
+
private _fks: ForeignKeyBuilder[] = [];
|
|
75
|
+
private _indexes: IndexEntry[] = [];
|
|
76
|
+
private _drops: string[] = [];
|
|
77
|
+
private _renames: RenameEntry[] = [];
|
|
78
|
+
private _dropIndexes: DropIndexEntry[] = [];
|
|
79
|
+
private _dropForeigns: Array<string | string[]> = [];
|
|
80
|
+
private _fulltexts: IndexEntry[] = [];
|
|
81
|
+
private _tablePK: string[] | null = null;
|
|
82
|
+
|
|
83
|
+
// ── Integer columns ───────────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Auto-incrementing integer primary key.
|
|
87
|
+
* Shorthand: `table.id()` is identical to `table.increments('id')`.
|
|
88
|
+
*
|
|
89
|
+
* @param name - Column name, defaults to `"id"`.
|
|
90
|
+
* @returns The column builder for the new `INTEGER PRIMARY KEY AUTOINCREMENT` column.
|
|
91
|
+
* @category Column types
|
|
92
|
+
*/
|
|
93
|
+
id(name = "id"): ColumnBuilder {
|
|
94
|
+
return this.increments(name);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Auto-incrementing `INTEGER PRIMARY KEY AUTOINCREMENT` column.
|
|
99
|
+
*
|
|
100
|
+
* @param name - Column name, defaults to `"id"`.
|
|
101
|
+
* @category Column types
|
|
102
|
+
*/
|
|
103
|
+
increments(name = "id"): ColumnBuilder {
|
|
104
|
+
return this._add(new ColumnBuilder(name, "INTEGER", true, true));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Auto-incrementing big-integer primary key. On SQLite this is identical to
|
|
109
|
+
* {@link Blueprint.increments} (both use the `INTEGER` storage class).
|
|
110
|
+
*
|
|
111
|
+
* @param name - Column name, defaults to `"id"`.
|
|
112
|
+
* @category Column types
|
|
113
|
+
*/
|
|
114
|
+
bigIncrements(name = "id"): ColumnBuilder {
|
|
115
|
+
return this._add(new ColumnBuilder(name, "INTEGER", true, true));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Signed `INTEGER` column.
|
|
120
|
+
* @category Column types
|
|
121
|
+
*/
|
|
122
|
+
integer(name: string): ColumnBuilder {
|
|
123
|
+
return this._add(new ColumnBuilder(name, "INTEGER"));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Big-integer column. Stored as `INTEGER` on SQLite (no distinct `BIGINT` type).
|
|
128
|
+
* @category Column types
|
|
129
|
+
*/
|
|
130
|
+
bigInteger(name: string): ColumnBuilder {
|
|
131
|
+
return this._add(new ColumnBuilder(name, "INTEGER"));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Tiny-integer column. Stored as `INTEGER` on SQLite.
|
|
136
|
+
* @category Column types
|
|
137
|
+
*/
|
|
138
|
+
tinyInteger(name: string): ColumnBuilder {
|
|
139
|
+
return this._add(new ColumnBuilder(name, "INTEGER"));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Small-integer column. Stored as `INTEGER` on SQLite.
|
|
144
|
+
* @category Column types
|
|
145
|
+
*/
|
|
146
|
+
smallInteger(name: string): ColumnBuilder {
|
|
147
|
+
return this._add(new ColumnBuilder(name, "INTEGER"));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Medium-integer column. Stored as `INTEGER` on SQLite.
|
|
152
|
+
* @category Column types
|
|
153
|
+
*/
|
|
154
|
+
mediumInteger(name: string): ColumnBuilder {
|
|
155
|
+
return this._add(new ColumnBuilder(name, "INTEGER"));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Unsigned integer column. The unsigned flag is tracked for multi-DB
|
|
160
|
+
* compatibility only — SQLite has no `UNSIGNED` type, so no extra SQL is emitted.
|
|
161
|
+
* @category Column types
|
|
162
|
+
*/
|
|
163
|
+
unsignedInteger(name: string): ColumnBuilder<"unsigned"> {
|
|
164
|
+
return this._add(new ColumnBuilder(name, "INTEGER")).unsigned();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Unsigned big-integer column — the conventional type for foreign-key columns.
|
|
169
|
+
* Stored as `INTEGER` on SQLite.
|
|
170
|
+
* @category Column types
|
|
171
|
+
*/
|
|
172
|
+
unsignedBigInteger(name: string): ColumnBuilder<"unsigned"> {
|
|
173
|
+
return this._add(new ColumnBuilder(name, "INTEGER")).unsigned();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Unsigned small-integer column. Stored as `INTEGER` on SQLite.
|
|
178
|
+
* @category Column types
|
|
179
|
+
*/
|
|
180
|
+
unsignedSmallInteger(name: string): ColumnBuilder<"unsigned"> {
|
|
181
|
+
return this._add(new ColumnBuilder(name, "INTEGER")).unsigned();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Unsigned tiny-integer column. Stored as `INTEGER` on SQLite.
|
|
186
|
+
* @category Column types
|
|
187
|
+
*/
|
|
188
|
+
unsignedTinyInteger(name: string): ColumnBuilder<"unsigned"> {
|
|
189
|
+
return this._add(new ColumnBuilder(name, "INTEGER")).unsigned();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Unsigned medium-integer column. Stored as `INTEGER` on SQLite.
|
|
194
|
+
* @category Column types
|
|
195
|
+
*/
|
|
196
|
+
unsignedMediumInteger(name: string): ColumnBuilder<"unsigned"> {
|
|
197
|
+
return this._add(new ColumnBuilder(name, "INTEGER")).unsigned();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ── String / text columns ─────────────────────────────────────────────────
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Variable-length string column (`VARCHAR`-style), stored as `TEXT`.
|
|
204
|
+
* @param name - Column name.
|
|
205
|
+
* @param _length - Max length; accepted for multi-DB compatibility but ignored on SQLite.
|
|
206
|
+
* @category Column types
|
|
207
|
+
*/
|
|
208
|
+
string(name: string, _length = 255): ColumnBuilder {
|
|
209
|
+
return this._add(new ColumnBuilder(name, "TEXT"));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Fixed-length `CHAR` column. Stored as `TEXT` on SQLite; `_length` is ignored.
|
|
214
|
+
* @category Column types
|
|
215
|
+
*/
|
|
216
|
+
char(name: string, _length = 255): ColumnBuilder {
|
|
217
|
+
return this._add(new ColumnBuilder(name, "TEXT"));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* `TEXT` column for arbitrary-length strings.
|
|
222
|
+
* @category Column types
|
|
223
|
+
*/
|
|
224
|
+
text(name: string): ColumnBuilder {
|
|
225
|
+
return this._add(new ColumnBuilder(name, "TEXT"));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Tiny-text column. Stored as `TEXT` on SQLite.
|
|
230
|
+
* @category Column types
|
|
231
|
+
*/
|
|
232
|
+
tinyText(name: string): ColumnBuilder {
|
|
233
|
+
return this._add(new ColumnBuilder(name, "TEXT"));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Medium-text column. Stored as `TEXT` on SQLite.
|
|
238
|
+
* @category Column types
|
|
239
|
+
*/
|
|
240
|
+
mediumText(name: string): ColumnBuilder {
|
|
241
|
+
return this._add(new ColumnBuilder(name, "TEXT"));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Long-text column. Stored as `TEXT` on SQLite.
|
|
246
|
+
* @category Column types
|
|
247
|
+
*/
|
|
248
|
+
longText(name: string): ColumnBuilder {
|
|
249
|
+
return this._add(new ColumnBuilder(name, "TEXT"));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ── UUID / ULID ───────────────────────────────────────────────────────────
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* UUID column — stored as `TEXT` (36 chars).
|
|
256
|
+
*
|
|
257
|
+
* @example
|
|
258
|
+
* ```ts
|
|
259
|
+
* table.uuid('id').primary();
|
|
260
|
+
* table.uuid('id').primary().defaultTo(sql`gen_random_uuid()`);
|
|
261
|
+
* ```
|
|
262
|
+
* @category Column types
|
|
263
|
+
*/
|
|
264
|
+
uuid(name: string): ColumnBuilder {
|
|
265
|
+
return this._add(new ColumnBuilder(name, "TEXT"));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* ULID column — stored as `TEXT` (26 chars).
|
|
270
|
+
* ULIDs are lexicographically sortable and URL-safe.
|
|
271
|
+
* @category Column types
|
|
272
|
+
*/
|
|
273
|
+
ulid(name: string): ColumnBuilder {
|
|
274
|
+
return this._add(new ColumnBuilder(name, "TEXT"));
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// ── Numeric columns ───────────────────────────────────────────────────────
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Single-precision floating-point column, stored as `REAL`.
|
|
281
|
+
* @category Column types
|
|
282
|
+
*/
|
|
283
|
+
float(name: string): ColumnBuilder {
|
|
284
|
+
return this._add(new ColumnBuilder(name, "REAL"));
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Double-precision floating-point column, stored as `REAL`.
|
|
289
|
+
* `_precision`/`_scale` are accepted for multi-DB compatibility but ignored on SQLite.
|
|
290
|
+
* @category Column types
|
|
291
|
+
*/
|
|
292
|
+
double(name: string, _precision?: number, _scale?: number): ColumnBuilder {
|
|
293
|
+
return this._add(new ColumnBuilder(name, "REAL"));
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Fixed-point decimal column. Stored as `REAL` on SQLite (no exact `DECIMAL`
|
|
298
|
+
* type); `_precision`/`_scale` are accepted but ignored.
|
|
299
|
+
* @category Column types
|
|
300
|
+
*/
|
|
301
|
+
decimal(name: string, _precision = 8, _scale = 2): ColumnBuilder {
|
|
302
|
+
return this._add(new ColumnBuilder(name, "REAL"));
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Unsigned fixed-point decimal column. Stored as `REAL` on SQLite; the unsigned
|
|
307
|
+
* flag is tracked for compatibility only.
|
|
308
|
+
* @category Column types
|
|
309
|
+
*/
|
|
310
|
+
unsignedDecimal(name: string, _precision = 8, _scale = 2): ColumnBuilder<"unsigned"> {
|
|
311
|
+
return this._add(new ColumnBuilder(name, "REAL")).unsigned();
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// ── Boolean ───────────────────────────────────────────────────────────────
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Boolean column. Stored as `INTEGER` (0 / 1); JS booleans passed to
|
|
318
|
+
* {@link ColumnBuilder.default} serialise to `1` / `0`.
|
|
319
|
+
* @category Column types
|
|
320
|
+
*/
|
|
321
|
+
boolean(name: string): ColumnBuilder {
|
|
322
|
+
return this._add(new ColumnBuilder(name, "INTEGER"));
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// ── Date / time columns ───────────────────────────────────────────────────
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Date-and-time column, stored as `TEXT` (ISO-8601).
|
|
329
|
+
* @category Column types
|
|
330
|
+
*/
|
|
331
|
+
dateTime(name: string): ColumnBuilder {
|
|
332
|
+
return this._add(new ColumnBuilder(name, "TEXT"));
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Timestamp column. Alias of {@link Blueprint.dateTime} — stored as `TEXT` (ISO-8601).
|
|
337
|
+
* @category Column types
|
|
338
|
+
*/
|
|
339
|
+
timestamp(name: string): ColumnBuilder {
|
|
340
|
+
return this.dateTime(name);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Date-only column, stored as `TEXT` (`YYYY-MM-DD`).
|
|
345
|
+
* @category Column types
|
|
346
|
+
*/
|
|
347
|
+
date(name: string): ColumnBuilder {
|
|
348
|
+
return this._add(new ColumnBuilder(name, "TEXT"));
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Time-of-day column, stored as `TEXT` (`HH:MM:SS`).
|
|
353
|
+
* @category Column types
|
|
354
|
+
*/
|
|
355
|
+
time(name: string): ColumnBuilder {
|
|
356
|
+
return this._add(new ColumnBuilder(name, "TEXT"));
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* 4-digit year column, stored as `INTEGER`.
|
|
361
|
+
* @category Column types
|
|
362
|
+
*/
|
|
363
|
+
year(name: string): ColumnBuilder {
|
|
364
|
+
return this._add(new ColumnBuilder(name, "INTEGER"));
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// ── Binary ────────────────────────────────────────────────────────────────
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Binary column, stored as `BLOB`.
|
|
371
|
+
* @category Column types
|
|
372
|
+
*/
|
|
373
|
+
binary(name: string): ColumnBuilder {
|
|
374
|
+
return this._add(new ColumnBuilder(name, "BLOB"));
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// ── JSON ──────────────────────────────────────────────────────────────────
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* JSON column, stored as `TEXT` (serialised JSON).
|
|
381
|
+
* @category Column types
|
|
382
|
+
*/
|
|
383
|
+
json(name: string): ColumnBuilder {
|
|
384
|
+
return this._add(new ColumnBuilder(name, "TEXT"));
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// ── Enum / set ────────────────────────────────────────────────────────────
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Enum column — stored as `TEXT` with a `CHECK` constraint enforcing the
|
|
391
|
+
* allowed values. Single quotes in values are escaped.
|
|
392
|
+
*
|
|
393
|
+
* @param name - Column name.
|
|
394
|
+
* @param values - The permitted string values.
|
|
395
|
+
* @example
|
|
396
|
+
* ```ts
|
|
397
|
+
* table.enum('status', ['active', 'inactive', 'suspended']);
|
|
398
|
+
* ```
|
|
399
|
+
* @category Column types
|
|
400
|
+
*/
|
|
401
|
+
enum(name: string, values: string[]): ColumnBuilder<"check"> {
|
|
402
|
+
const quoted = values.map((v) => `'${v.replace(/'/g, "''")}'`).join(", ");
|
|
403
|
+
return this._add(new ColumnBuilder(name, "TEXT")).check(`${name} IN (${quoted})`);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* MySQL `SET` column — stored as `TEXT` on SQLite. Unlike {@link Blueprint.enum},
|
|
408
|
+
* no `CHECK` constraint is emitted, so `_values` is not enforced on SQLite.
|
|
409
|
+
* @category Column types
|
|
410
|
+
*/
|
|
411
|
+
|
|
412
|
+
set(name: string, _values: string[]): ColumnBuilder {
|
|
413
|
+
return this._add(new ColumnBuilder(name, "TEXT"));
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// ── Network / IP ─────────────────────────────────────────────────────────
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* IPv4 / IPv6 address column — stored as `TEXT` (up to 45 chars for IPv6).
|
|
420
|
+
* @category Column types
|
|
421
|
+
*/
|
|
422
|
+
ipAddress(name: string): ColumnBuilder {
|
|
423
|
+
return this._add(new ColumnBuilder(name, "TEXT"));
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* MAC address column — stored as `TEXT` (17 chars, `xx:xx:xx:xx:xx:xx`).
|
|
428
|
+
* @category Column types
|
|
429
|
+
*/
|
|
430
|
+
macAddress(name: string): ColumnBuilder {
|
|
431
|
+
return this._add(new ColumnBuilder(name, "TEXT"));
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// ── Foreign keys (fluent) ─────────────────────────────────────────────────
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Unsigned big-integer foreign-key column. Chain `.constrained()` on the
|
|
438
|
+
* returned {@link ForeignIdColumnBuilder} to add the `FOREIGN KEY` constraint
|
|
439
|
+
* automatically, inferring the referenced table from the column name.
|
|
440
|
+
*
|
|
441
|
+
* ```ts
|
|
442
|
+
* table.foreignId('user_id').constrained().nullOnDelete();
|
|
443
|
+
* table.foreignId('post_id').constrained('blog_posts');
|
|
444
|
+
* ```
|
|
445
|
+
*
|
|
446
|
+
* @category Foreign keys
|
|
447
|
+
*/
|
|
448
|
+
foreignId(name: string): ForeignIdColumnBuilder {
|
|
449
|
+
const col = new ForeignIdColumnBuilder(name, "INTEGER", (c) => this.foreign(c));
|
|
450
|
+
col.unsigned();
|
|
451
|
+
this._cols.push(col as unknown as IColumnSQL);
|
|
452
|
+
return col;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* UUID (`TEXT`) foreign-key column. Chain `.constrained()` on the returned
|
|
457
|
+
* {@link ForeignIdColumnBuilder} to add the FK constraint:
|
|
458
|
+
*
|
|
459
|
+
* ```ts
|
|
460
|
+
* table.foreignUuid('user_id').constrained();
|
|
461
|
+
* ```
|
|
462
|
+
*
|
|
463
|
+
* @category Foreign keys
|
|
464
|
+
*/
|
|
465
|
+
foreignUuid(name: string): ForeignIdColumnBuilder {
|
|
466
|
+
const col = new ForeignIdColumnBuilder(name, "TEXT", (c) => this.foreign(c));
|
|
467
|
+
this._cols.push(col as unknown as IColumnSQL);
|
|
468
|
+
return col;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// ── Shorthands ────────────────────────────────────────────────────────────
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Add nullable `created_at` and `updated_at` (`TEXT`) columns.
|
|
475
|
+
* @category Table modifiers
|
|
476
|
+
*/
|
|
477
|
+
timestamps(): void {
|
|
478
|
+
this.dateTime("created_at").nullable();
|
|
479
|
+
this.dateTime("updated_at").nullable();
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Alias for {@link Blueprint.timestamps} — both columns are always nullable.
|
|
484
|
+
* @category Table modifiers
|
|
485
|
+
*/
|
|
486
|
+
nullableTimestamps(): void {
|
|
487
|
+
this.timestamps();
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Add a nullable `deleted_at` (`TEXT`) column for soft-delete support.
|
|
492
|
+
* @param column - Column name, defaults to `"deleted_at"`.
|
|
493
|
+
* @category Table modifiers
|
|
494
|
+
*/
|
|
495
|
+
softDeletes(column = "deleted_at"): void {
|
|
496
|
+
this.dateTime(column).nullable();
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Add `remember_token` — a nullable 100-char string used by session-based
|
|
501
|
+
* "remember me" functionality.
|
|
502
|
+
* @category Table modifiers
|
|
503
|
+
*/
|
|
504
|
+
rememberToken(): ColumnBuilder<"nullability"> {
|
|
505
|
+
return this.string("remember_token", 100).nullable();
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Add `{name}_id` (unsigned big-integer) and `{name}_type` (string) columns
|
|
510
|
+
* plus a composite index — the standard polymorphic-relation pattern.
|
|
511
|
+
*
|
|
512
|
+
* @param name - Relation base name (e.g. `"taggable"`).
|
|
513
|
+
* @param indexName - Optional explicit name for the composite index.
|
|
514
|
+
* @example
|
|
515
|
+
* ```ts
|
|
516
|
+
* table.morphs('taggable');
|
|
517
|
+
* // adds: taggable_id INTEGER, taggable_type TEXT, index on both
|
|
518
|
+
* ```
|
|
519
|
+
* @category Table modifiers
|
|
520
|
+
*/
|
|
521
|
+
morphs(name: string, indexName?: string): void {
|
|
522
|
+
this.unsignedBigInteger(`${name}_id`);
|
|
523
|
+
this.string(`${name}_type`);
|
|
524
|
+
this.index([`${name}_id`, `${name}_type`], indexName);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Same as {@link Blueprint.morphs} but both columns are nullable.
|
|
529
|
+
* @category Table modifiers
|
|
530
|
+
*/
|
|
531
|
+
nullableMorphs(name: string, indexName?: string): void {
|
|
532
|
+
this.unsignedBigInteger(`${name}_id`).nullable();
|
|
533
|
+
this.string(`${name}_type`).nullable();
|
|
534
|
+
this.index([`${name}_id`, `${name}_type`], indexName);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* UUID-based polymorphic relation columns: `{name}_id` (`TEXT` UUID) and
|
|
539
|
+
* `{name}_type` (string) plus a composite index.
|
|
540
|
+
* @category Table modifiers
|
|
541
|
+
*/
|
|
542
|
+
uuidMorphs(name: string, indexName?: string): void {
|
|
543
|
+
this.uuid(`${name}_id`);
|
|
544
|
+
this.string(`${name}_type`);
|
|
545
|
+
this.index([`${name}_id`, `${name}_type`], indexName);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// ── Foreign keys (explicit) ───────────────────────────────────────────────
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Begin an explicit foreign-key constraint on an existing column, returning a
|
|
552
|
+
* {@link ForeignKeyBuilder} to configure the referenced table/column and
|
|
553
|
+
* referential actions.
|
|
554
|
+
*
|
|
555
|
+
* @example
|
|
556
|
+
* ```ts
|
|
557
|
+
* table.foreign('user_id').references('id').on('users').onDelete('CASCADE');
|
|
558
|
+
* ```
|
|
559
|
+
* @category Foreign keys
|
|
560
|
+
*/
|
|
561
|
+
foreign(column: string): ForeignKeyBuilder {
|
|
562
|
+
const fk = new ForeignKeyBuilder(column);
|
|
563
|
+
this._fks.push(fk);
|
|
564
|
+
return fk;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// ── Table-level constraints ───────────────────────────────────────────────
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* Define a table-level (optionally composite) primary key. Only applied on
|
|
571
|
+
* `CREATE TABLE` and only when no column already declares itself the primary key.
|
|
572
|
+
*
|
|
573
|
+
* @param columns - One column name or several for a composite key.
|
|
574
|
+
* @param _name - Constraint name; accepted for compatibility but unused on SQLite.
|
|
575
|
+
* @example
|
|
576
|
+
* ```ts
|
|
577
|
+
* table.primary(['user_id', 'role_id']);
|
|
578
|
+
* ```
|
|
579
|
+
* @category Indexes
|
|
580
|
+
*/
|
|
581
|
+
primary(columns: string | string[], _name?: string): this {
|
|
582
|
+
this._tablePK = _arr(columns);
|
|
583
|
+
return this;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Add a non-unique index across one or more columns. When `name` is omitted the
|
|
588
|
+
* index name is derived as `{table}_{cols}_index`.
|
|
589
|
+
* @category Indexes
|
|
590
|
+
*/
|
|
591
|
+
index(columns: string | string[], name?: string): this {
|
|
592
|
+
this._indexes.push({ columns: _arr(columns), unique: false, name });
|
|
593
|
+
return this;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* Add a unique index across one or more columns. When `name` is omitted the
|
|
598
|
+
* index name is derived as `{table}_{cols}_unique`.
|
|
599
|
+
* @category Indexes
|
|
600
|
+
*/
|
|
601
|
+
unique(columns: string | string[], name?: string): this {
|
|
602
|
+
this._indexes.push({ columns: _arr(columns), unique: true, name });
|
|
603
|
+
return this;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* Full-text index. On MySQL/Postgres this is intended to emit a `FULLTEXT`/GIN
|
|
608
|
+
* index; on SQLite a plain `CREATE INDEX` is emitted (use FTS5 virtual tables
|
|
609
|
+
* for true full-text search there).
|
|
610
|
+
* @category Indexes
|
|
611
|
+
*/
|
|
612
|
+
fulltext(columns: string | string[], name?: string): this {
|
|
613
|
+
this._fulltexts.push({ columns: _arr(columns), unique: false, name });
|
|
614
|
+
return this;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Spatial (GIS) index. Falls back to a plain `CREATE INDEX` on SQLite.
|
|
619
|
+
* @category Indexes
|
|
620
|
+
*/
|
|
621
|
+
spatialIndex(columns: string | string[], name?: string): this {
|
|
622
|
+
this._fulltexts.push({ columns: _arr(columns), unique: false, name });
|
|
623
|
+
return this;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// ── ALTER TABLE helpers ───────────────────────────────────────────────────
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Drop one or more columns (`ALTER TABLE … DROP COLUMN`). Only meaningful on the
|
|
630
|
+
* ALTER path, i.e. inside a {@link Schema.table} callback.
|
|
631
|
+
* @category Dropping
|
|
632
|
+
*/
|
|
633
|
+
dropColumn(...names: string[]): this {
|
|
634
|
+
this._drops.push(...names);
|
|
635
|
+
return this;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Rename a column (`ALTER TABLE … RENAME COLUMN from TO to`).
|
|
640
|
+
* @category Table modifiers
|
|
641
|
+
*/
|
|
642
|
+
renameColumn(from: string, to: string): this {
|
|
643
|
+
this._renames.push({ from, to });
|
|
644
|
+
return this;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* Drop an index by name, or by deriving `{table}_{cols}_index` from a column list.
|
|
649
|
+
* Emits `DROP INDEX IF EXISTS`; a no-op when the name cannot be determined.
|
|
650
|
+
* @category Dropping
|
|
651
|
+
*/
|
|
652
|
+
dropIndex(nameOrColumns: string | string[]): this {
|
|
653
|
+
this._dropIndexes.push({ nameOrColumns });
|
|
654
|
+
return this;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Drop a unique index. On SQLite unique constraints are implemented as indexes,
|
|
659
|
+
* so this is identical to {@link Blueprint.dropIndex}.
|
|
660
|
+
* @category Dropping
|
|
661
|
+
*/
|
|
662
|
+
dropUnique(nameOrColumns: string | string[]): this {
|
|
663
|
+
return this.dropIndex(nameOrColumns);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Drop a foreign-key constraint by name (or derived `{table}_{cols}_foreign`).
|
|
668
|
+
* Emits `ALTER TABLE … DROP FOREIGN KEY` (MySQL) / `DROP CONSTRAINT` (Postgres).
|
|
669
|
+
* No SQL is emitted on SQLite, which cannot drop FK constraints without a full
|
|
670
|
+
* table rebuild — use a rebuild migration there.
|
|
671
|
+
* @category Dropping
|
|
672
|
+
*/
|
|
673
|
+
dropForeign(nameOrColumns: string | string[]): this {
|
|
674
|
+
this._dropForeigns.push(nameOrColumns);
|
|
675
|
+
return this;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* Drop the table primary key.
|
|
680
|
+
*
|
|
681
|
+
* @remarks
|
|
682
|
+
* Currently a no-op for every dialect: it records no intent and emits no SQL.
|
|
683
|
+
* Dropping a primary key on SQLite requires a full table rebuild.
|
|
684
|
+
* @category Dropping
|
|
685
|
+
*/
|
|
686
|
+
dropPrimary(): this {
|
|
687
|
+
return this;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// ── SQL generation ────────────────────────────────────────────────────────
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* Compile the accumulated column/constraint/index intent into the statements
|
|
694
|
+
* that create the table. Called by {@link Schema.create}.
|
|
695
|
+
*
|
|
696
|
+
* @param table - Target table name.
|
|
697
|
+
* @returns `[ "CREATE TABLE …", ...("CREATE INDEX …")* ]` — the create statement
|
|
698
|
+
* first, followed by one statement per index.
|
|
699
|
+
* @category Table modifiers
|
|
700
|
+
*/
|
|
701
|
+
toCreateSQL(table: string, dialect: "sqlite" | "mysql" | "postgres" = "sqlite"): string[] {
|
|
702
|
+
const inlineParts: string[] = [];
|
|
703
|
+
|
|
704
|
+
for (const col of this._cols) inlineParts.push(col.toColumnSQL(dialect));
|
|
705
|
+
|
|
706
|
+
// Table-level composite primary key (only when no column-level PK exists)
|
|
707
|
+
if (this._tablePK) {
|
|
708
|
+
inlineParts.push(`PRIMARY KEY (${this._tablePK.join(", ")})`);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
for (const fk of this._fks) inlineParts.push(fk.toConstraintSQL());
|
|
712
|
+
|
|
713
|
+
return [
|
|
714
|
+
`CREATE TABLE ${table} (\n ${inlineParts.join(",\n ")}\n)`,
|
|
715
|
+
...this._indexStatements(table),
|
|
716
|
+
];
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/**
|
|
720
|
+
* Compile the accumulated intent into `ALTER TABLE` / `CREATE INDEX IF NOT
|
|
721
|
+
* EXISTS` / `DROP INDEX` statements. Called by {@link Schema.table}.
|
|
722
|
+
*
|
|
723
|
+
* @param table - Target table name.
|
|
724
|
+
* @param dialect - Governs column-modification and drop-foreign SQL; defaults to `"sqlite"`.
|
|
725
|
+
* @returns The ordered list of DDL statements to execute.
|
|
726
|
+
* @category Table modifiers
|
|
727
|
+
*/
|
|
728
|
+
toAlterSQL(table: string, dialect: "sqlite" | "mysql" | "postgres" = "sqlite"): string[] {
|
|
729
|
+
const statements: string[] = [];
|
|
730
|
+
|
|
731
|
+
for (const col of this._cols) {
|
|
732
|
+
if (!col.isAlter) {
|
|
733
|
+
// New column — all dialects support ADD COLUMN.
|
|
734
|
+
statements.push(`ALTER TABLE ${table} ADD COLUMN ${col.toColumnSQL(dialect)}`);
|
|
735
|
+
} else {
|
|
736
|
+
// Column modification — dialect-specific.
|
|
737
|
+
statements.push(..._alterColumnSQL(table, col, dialect));
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
for (const name of this._drops) statements.push(`ALTER TABLE ${table} DROP COLUMN ${name}`);
|
|
741
|
+
for (const r of this._renames)
|
|
742
|
+
statements.push(`ALTER TABLE ${table} RENAME COLUMN ${r.from} TO ${r.to}`);
|
|
743
|
+
for (const di of this._dropIndexes) {
|
|
744
|
+
const idxName = _resolveIndexName(table, di.nameOrColumns);
|
|
745
|
+
if (idxName) statements.push(`DROP INDEX IF EXISTS ${idxName}`);
|
|
746
|
+
}
|
|
747
|
+
for (const df of this._dropForeigns) {
|
|
748
|
+
const fkName = _resolveForeignName(table, df);
|
|
749
|
+
if (dialect === "mysql") statements.push(`ALTER TABLE ${table} DROP FOREIGN KEY ${fkName}`);
|
|
750
|
+
else if (dialect === "postgres")
|
|
751
|
+
statements.push(`ALTER TABLE ${table} DROP CONSTRAINT ${fkName}`);
|
|
752
|
+
// SQLite: cannot drop a FK constraint in place — no statement emitted.
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
statements.push(...this._indexStatements(table, true));
|
|
756
|
+
return statements;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// ── Private ───────────────────────────────────────────────────────────────
|
|
760
|
+
|
|
761
|
+
private _add<L extends string>(col: ColumnBuilder<L>): ColumnBuilder<L> {
|
|
762
|
+
this._cols.push(col as unknown as IColumnSQL);
|
|
763
|
+
return col;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
private _indexStatements(table: string, ifNotExists = false): string[] {
|
|
767
|
+
const stmts: string[] = [];
|
|
768
|
+
const guard = ifNotExists ? "IF NOT EXISTS " : "";
|
|
769
|
+
|
|
770
|
+
for (const idx of this._indexes) {
|
|
771
|
+
const name =
|
|
772
|
+
idx.name ?? `${table}_${idx.columns.join("_")}_${idx.unique ? "unique" : "index"}`;
|
|
773
|
+
const unique = idx.unique ? "UNIQUE " : "";
|
|
774
|
+
stmts.push(`CREATE ${unique}INDEX ${guard}${name} ON ${table} (${idx.columns.join(", ")})`);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
for (const ft of this._fulltexts) {
|
|
778
|
+
const name = ft.name ?? `${table}_${ft.columns.join("_")}_fulltext`;
|
|
779
|
+
stmts.push(`CREATE INDEX ${guard}${name} ON ${table} (${ft.columns.join(", ")})`);
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
for (const col of this._cols) {
|
|
783
|
+
if (col.wantsIndex) {
|
|
784
|
+
stmts.push(`CREATE INDEX ${guard}${table}_${col.name}_index ON ${table} (${col.name})`);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
return stmts;
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
793
|
+
|
|
794
|
+
function _arr(v: string | string[]): string[] {
|
|
795
|
+
return Array.isArray(v) ? v : [v];
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
function _resolveForeignName(table: string, nameOrColumns: string | string[]): string {
|
|
799
|
+
if (typeof nameOrColumns === "string") return nameOrColumns;
|
|
800
|
+
return `${table}_${nameOrColumns.join("_")}_foreign`;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
function _resolveIndexName(table: string, nameOrColumns: string | string[]): string | null {
|
|
804
|
+
if (typeof nameOrColumns === "string" && !nameOrColumns.includes(",")) {
|
|
805
|
+
return nameOrColumns; // already a name
|
|
806
|
+
}
|
|
807
|
+
const cols = _arr(nameOrColumns);
|
|
808
|
+
return `${table}_${cols.join("_")}_index`;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// ── Dialect-aware ALTER COLUMN ────────────────────────────────────────────────
|
|
812
|
+
|
|
813
|
+
interface IAlterCol {
|
|
814
|
+
readonly name: string;
|
|
815
|
+
toColumnSQL(dialect?: "sqlite" | "mysql" | "postgres"): string;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/**
|
|
819
|
+
* Emit the correct SQL to modify an existing column, per dialect.
|
|
820
|
+
*
|
|
821
|
+
* MySQL / MariaDB:
|
|
822
|
+
* ALTER TABLE t MODIFY COLUMN col TEXT NOT NULL DEFAULT 'x'
|
|
823
|
+
*
|
|
824
|
+
* PostgreSQL:
|
|
825
|
+
* ALTER TABLE t ALTER COLUMN col TYPE TEXT,
|
|
826
|
+
* ALTER COLUMN col SET/DROP NOT NULL,
|
|
827
|
+
* ALTER COLUMN col SET/DROP DEFAULT …
|
|
828
|
+
* (PostgreSQL requires one sub-command per attribute change.)
|
|
829
|
+
*
|
|
830
|
+
* SQLite:
|
|
831
|
+
* SQLite does not support modifying column definitions without a full table
|
|
832
|
+
* rebuild. A console warning is emitted and the statement is skipped.
|
|
833
|
+
* Use a manual table-rebuild migration when you need structural changes on
|
|
834
|
+
* SQLite.
|
|
835
|
+
*/
|
|
836
|
+
function _alterColumnSQL(
|
|
837
|
+
table: string,
|
|
838
|
+
col: IAlterCol,
|
|
839
|
+
dialect: "sqlite" | "mysql" | "postgres",
|
|
840
|
+
): string[] {
|
|
841
|
+
if (dialect === "mysql") {
|
|
842
|
+
return [`ALTER TABLE ${table} MODIFY COLUMN ${col.toColumnSQL("mysql")}`];
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
if (dialect === "postgres") {
|
|
846
|
+
// Parse the full column SQL to extract type + constraints.
|
|
847
|
+
// col.toColumnSQL() returns e.g. "name TEXT NOT NULL DEFAULT 'x'"
|
|
848
|
+
return _postgresAlterStatements(table, col);
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
// SQLite — warn and skip.
|
|
852
|
+
console.warn(
|
|
853
|
+
`[Zerotal ORM] Warning: .alter() on column "${col.name}" in table "${table}" ` +
|
|
854
|
+
`is not supported by SQLite and was skipped.\n` +
|
|
855
|
+
`SQLite requires a full table rebuild to change a column's type or constraints.\n` +
|
|
856
|
+
`Consider creating a new column, migrating data, and dropping the old one instead.`,
|
|
857
|
+
);
|
|
858
|
+
return [];
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
/**
|
|
862
|
+
* Break a full column definition into individual PostgreSQL ALTER COLUMN
|
|
863
|
+
* sub-commands so each attribute can be changed independently.
|
|
864
|
+
*
|
|
865
|
+
* Example: "email TEXT NOT NULL DEFAULT 'x'" →
|
|
866
|
+
* ALTER TABLE users ALTER COLUMN email TYPE TEXT
|
|
867
|
+
* ALTER TABLE users ALTER COLUMN email SET NOT NULL
|
|
868
|
+
* ALTER TABLE users ALTER COLUMN email SET DEFAULT 'x'
|
|
869
|
+
*/
|
|
870
|
+
function _postgresAlterStatements(table: string, col: IAlterCol): string[] {
|
|
871
|
+
const sql = col.toColumnSQL(); // e.g. "email TEXT NOT NULL DEFAULT 'val' UNIQUE"
|
|
872
|
+
const name = col.name;
|
|
873
|
+
const stmts: string[] = [];
|
|
874
|
+
|
|
875
|
+
// Extract the SQL type (first token after the column name).
|
|
876
|
+
const afterName = sql.slice(name.length).trim();
|
|
877
|
+
const typeMatch = afterName.match(/^([A-Z]+(?:\([^)]*\))?)/i);
|
|
878
|
+
if (typeMatch) {
|
|
879
|
+
stmts.push(`ALTER TABLE ${table} ALTER COLUMN ${name} TYPE ${typeMatch[1]}`);
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// NOT NULL / nullable.
|
|
883
|
+
if (/NOT NULL/i.test(afterName)) {
|
|
884
|
+
stmts.push(`ALTER TABLE ${table} ALTER COLUMN ${name} SET NOT NULL`);
|
|
885
|
+
} else {
|
|
886
|
+
stmts.push(`ALTER TABLE ${table} ALTER COLUMN ${name} DROP NOT NULL`);
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// DEFAULT.
|
|
890
|
+
const defMatch = afterName.match(
|
|
891
|
+
/DEFAULT\s+(\S+(?:\s+\S+)*?)(?:\s+(?:NOT NULL|NULL|UNIQUE|CHECK|GENERATED)|$)/i,
|
|
892
|
+
);
|
|
893
|
+
if (defMatch) {
|
|
894
|
+
stmts.push(`ALTER TABLE ${table} ALTER COLUMN ${name} SET DEFAULT ${defMatch[1]}`);
|
|
895
|
+
} else {
|
|
896
|
+
stmts.push(`ALTER TABLE ${table} ALTER COLUMN ${name} DROP DEFAULT`);
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
return stmts;
|
|
900
|
+
}
|