@mantiq/database 0.0.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 (42) hide show
  1. package/README.md +19 -0
  2. package/package.json +77 -0
  3. package/src/DatabaseManager.ts +115 -0
  4. package/src/DatabaseServiceProvider.ts +39 -0
  5. package/src/contracts/Connection.ts +13 -0
  6. package/src/contracts/Grammar.ts +16 -0
  7. package/src/contracts/MongoConnection.ts +122 -0
  8. package/src/contracts/Paginator.ts +10 -0
  9. package/src/drivers/BaseGrammar.ts +220 -0
  10. package/src/drivers/MSSQLConnection.ts +154 -0
  11. package/src/drivers/MSSQLGrammar.ts +106 -0
  12. package/src/drivers/MongoConnection.ts +298 -0
  13. package/src/drivers/MongoQueryBuilderImpl.ts +77 -0
  14. package/src/drivers/MySQLConnection.ts +120 -0
  15. package/src/drivers/MySQLGrammar.ts +19 -0
  16. package/src/drivers/PostgresConnection.ts +125 -0
  17. package/src/drivers/PostgresGrammar.ts +24 -0
  18. package/src/drivers/SQLiteConnection.ts +125 -0
  19. package/src/drivers/SQLiteGrammar.ts +19 -0
  20. package/src/errors/ConnectionError.ts +10 -0
  21. package/src/errors/ModelNotFoundError.ts +14 -0
  22. package/src/errors/QueryError.ts +11 -0
  23. package/src/events/DatabaseEvents.ts +101 -0
  24. package/src/factories/Factory.ts +170 -0
  25. package/src/factories/Faker.ts +382 -0
  26. package/src/helpers/db.ts +37 -0
  27. package/src/index.ts +100 -0
  28. package/src/migrations/Migration.ts +12 -0
  29. package/src/migrations/MigrationRepository.ts +50 -0
  30. package/src/migrations/Migrator.ts +201 -0
  31. package/src/orm/Collection.ts +236 -0
  32. package/src/orm/Document.ts +202 -0
  33. package/src/orm/Model.ts +775 -0
  34. package/src/orm/ModelQueryBuilder.ts +415 -0
  35. package/src/orm/Scope.ts +39 -0
  36. package/src/orm/eagerLoad.ts +300 -0
  37. package/src/query/Builder.ts +456 -0
  38. package/src/query/Expression.ts +18 -0
  39. package/src/schema/Blueprint.ts +196 -0
  40. package/src/schema/ColumnDefinition.ts +93 -0
  41. package/src/schema/SchemaBuilder.ts +376 -0
  42. package/src/seeders/Seeder.ts +28 -0
@@ -0,0 +1,456 @@
1
+ import { Expression } from './Expression.ts'
2
+ import { ModelNotFoundError } from '../errors/ModelNotFoundError.ts'
3
+ import { applyMacros } from '@mantiq/core'
4
+ import type { PaginationResult } from '../contracts/Paginator.ts'
5
+ import type { DatabaseConnection } from '../contracts/Connection.ts'
6
+
7
+ export type Operator = '=' | '!=' | '<>' | '<' | '>' | '<=' | '>=' | 'like' | 'not like' | 'in' | 'not in'
8
+
9
+ export interface WhereClause {
10
+ type: 'basic' | 'in' | 'notIn' | 'null' | 'notNull' | 'between' | 'raw' | 'nested' | 'column'
11
+ boolean: 'and' | 'or'
12
+ column?: string
13
+ operator?: string
14
+ value?: any
15
+ values?: any[]
16
+ range?: [any, any]
17
+ sql?: string
18
+ bindings?: any[]
19
+ nested?: WhereClause[]
20
+ secondColumn?: string
21
+ }
22
+
23
+ export interface JoinClause {
24
+ type: 'inner' | 'left' | 'right'
25
+ table: string
26
+ first: string
27
+ operator: string
28
+ second: string
29
+ }
30
+
31
+ export interface OrderClause {
32
+ column: string | Expression
33
+ direction: 'asc' | 'desc'
34
+ }
35
+
36
+ export interface QueryState {
37
+ table: string
38
+ columns: (string | Expression)[]
39
+ distinct: boolean
40
+ wheres: WhereClause[]
41
+ joins: JoinClause[]
42
+ orders: OrderClause[]
43
+ groups: string[]
44
+ havings: WhereClause[]
45
+ limitValue: number | null
46
+ offsetValue: number | null
47
+ }
48
+
49
+ export class QueryBuilder {
50
+ protected state: QueryState
51
+ protected _connection: DatabaseConnection
52
+
53
+ constructor(connection: DatabaseConnection, table: string) {
54
+ this._connection = connection
55
+ this.state = {
56
+ table,
57
+ columns: ['*'],
58
+ distinct: false,
59
+ wheres: [],
60
+ joins: [],
61
+ orders: [],
62
+ groups: [],
63
+ havings: [],
64
+ limitValue: null,
65
+ offsetValue: null,
66
+ }
67
+ }
68
+
69
+ // ── Selection ─────────────────────────────────────────────────────────────
70
+
71
+ select(...columns: (string | Expression)[]): this {
72
+ this.state.columns = columns.length ? columns : ['*']
73
+ return this
74
+ }
75
+
76
+ selectRaw(expression: string, bindings?: any[]): this {
77
+ this.state.columns = [new Expression(expression, bindings)]
78
+ return this
79
+ }
80
+
81
+ addSelect(...columns: (string | Expression)[]): this {
82
+ if (this.state.columns.length === 1 && this.state.columns[0] === '*') {
83
+ this.state.columns = [...columns]
84
+ } else {
85
+ this.state.columns.push(...columns)
86
+ }
87
+ return this
88
+ }
89
+
90
+ distinct(): this {
91
+ this.state.distinct = true
92
+ return this
93
+ }
94
+
95
+ // ── Where conditions ─────────────────────────────────────────────────────
96
+
97
+ where(column: string | ((q: QueryBuilder) => void), operatorOrValue?: any, value?: any): this {
98
+ if (typeof column === 'function') {
99
+ const nested: WhereClause[] = []
100
+ const sub = new QueryBuilder(this._connection, this.state.table)
101
+ column(sub)
102
+ this.state.wheres.push({ type: 'nested', boolean: 'and', nested: sub.state.wheres })
103
+ return this
104
+ }
105
+
106
+ let operator: string
107
+ let val: any
108
+ if (value === undefined) {
109
+ operator = '='
110
+ val = operatorOrValue
111
+ } else {
112
+ operator = operatorOrValue
113
+ val = value
114
+ }
115
+
116
+ this.state.wheres.push({ type: 'basic', boolean: 'and', column, operator, value: val })
117
+ return this
118
+ }
119
+
120
+ orWhere(column: string | ((q: QueryBuilder) => void), operatorOrValue?: any, value?: any): this {
121
+ if (typeof column === 'function') {
122
+ const sub = new QueryBuilder(this._connection, this.state.table)
123
+ column(sub)
124
+ this.state.wheres.push({ type: 'nested', boolean: 'or', nested: sub.state.wheres })
125
+ return this
126
+ }
127
+ let operator: string
128
+ let val: any
129
+ if (value === undefined) {
130
+ operator = '='
131
+ val = operatorOrValue
132
+ } else {
133
+ operator = operatorOrValue
134
+ val = value
135
+ }
136
+ this.state.wheres.push({ type: 'basic', boolean: 'or', column, operator, value: val })
137
+ return this
138
+ }
139
+
140
+ whereIn(column: string, values: any[]): this {
141
+ this.state.wheres.push({ type: 'in', boolean: 'and', column, values })
142
+ return this
143
+ }
144
+
145
+ whereNotIn(column: string, values: any[]): this {
146
+ this.state.wheres.push({ type: 'notIn', boolean: 'and', column, values })
147
+ return this
148
+ }
149
+
150
+ whereNull(column: string): this {
151
+ this.state.wheres.push({ type: 'null', boolean: 'and', column })
152
+ return this
153
+ }
154
+
155
+ whereNotNull(column: string): this {
156
+ this.state.wheres.push({ type: 'notNull', boolean: 'and', column })
157
+ return this
158
+ }
159
+
160
+ whereBetween(column: string, range: [any, any]): this {
161
+ this.state.wheres.push({ type: 'between', boolean: 'and', column, range })
162
+ return this
163
+ }
164
+
165
+ whereRaw(sql: string, bindings?: any[]): this {
166
+ this.state.wheres.push({ type: 'raw', boolean: 'and', sql, bindings: bindings ?? [] })
167
+ return this
168
+ }
169
+
170
+ /**
171
+ * Add a where clause comparing two columns.
172
+ *
173
+ * @example
174
+ * query.whereColumn('updated_at', '>', 'created_at')
175
+ * query.whereColumn('first_name', 'last_name') // defaults to '='
176
+ */
177
+ whereColumn(first: string, operatorOrSecond: string, second?: string): this {
178
+ let operator: string
179
+ let secondCol: string
180
+ if (second === undefined) {
181
+ operator = '='
182
+ secondCol = operatorOrSecond
183
+ } else {
184
+ operator = operatorOrSecond
185
+ secondCol = second
186
+ }
187
+ this.state.wheres.push({ type: 'column', boolean: 'and', column: first, operator, secondColumn: secondCol })
188
+ return this
189
+ }
190
+
191
+ /**
192
+ * Filter by date part of a datetime column.
193
+ * @example query.whereDate('created_at', '2024-01-15')
194
+ */
195
+ whereDate(column: string, operatorOrValue: string, value?: string): this {
196
+ const [op, val] = value === undefined ? ['=', operatorOrValue] : [operatorOrValue, value]
197
+ return this.whereRaw(`DATE(${column}) ${op} ?`, [val])
198
+ }
199
+
200
+ /**
201
+ * Filter by month of a datetime column.
202
+ * @example query.whereMonth('created_at', '03')
203
+ */
204
+ whereMonth(column: string, operatorOrValue: string | number, value?: string | number): this {
205
+ const [op, val] = value === undefined ? ['=', operatorOrValue] : [operatorOrValue, value]
206
+ return this.whereRaw(`strftime('%m', ${column}) ${op} ?`, [String(val).padStart(2, '0')])
207
+ }
208
+
209
+ /**
210
+ * Filter by year of a datetime column.
211
+ * @example query.whereYear('created_at', 2024)
212
+ */
213
+ whereYear(column: string, operatorOrValue: string | number, value?: string | number): this {
214
+ const [op, val] = value === undefined ? ['=', operatorOrValue] : [operatorOrValue, value]
215
+ return this.whereRaw(`strftime('%Y', ${column}) ${op} ?`, [String(val)])
216
+ }
217
+
218
+ /**
219
+ * Filter by time part of a datetime column.
220
+ * @example query.whereTime('created_at', '>=', '10:00')
221
+ */
222
+ whereTime(column: string, operatorOrValue: string, value?: string): this {
223
+ const [op, val] = value === undefined ? ['=', operatorOrValue] : [operatorOrValue, value]
224
+ return this.whereRaw(`strftime('%H:%M:%S', ${column}) ${op} ?`, [val])
225
+ }
226
+
227
+ // ── Joins ─────────────────────────────────────────────────────────────────
228
+
229
+ join(table: string, first: string, operator: string, second: string): this {
230
+ this.state.joins.push({ type: 'inner', table, first, operator, second })
231
+ return this
232
+ }
233
+
234
+ leftJoin(table: string, first: string, operator: string, second: string): this {
235
+ this.state.joins.push({ type: 'left', table, first, operator, second })
236
+ return this
237
+ }
238
+
239
+ rightJoin(table: string, first: string, operator: string, second: string): this {
240
+ this.state.joins.push({ type: 'right', table, first, operator, second })
241
+ return this
242
+ }
243
+
244
+ // ── Ordering / Grouping ───────────────────────────────────────────────────
245
+
246
+ orderBy(column: string | Expression, direction: 'asc' | 'desc' = 'asc'): this {
247
+ this.state.orders.push({ column, direction })
248
+ return this
249
+ }
250
+
251
+ orderByDesc(column: string): this {
252
+ return this.orderBy(column, 'desc')
253
+ }
254
+
255
+ groupBy(...columns: string[]): this {
256
+ this.state.groups.push(...columns)
257
+ return this
258
+ }
259
+
260
+ having(column: string, operator: string, value: any): this {
261
+ this.state.havings.push({ type: 'basic', boolean: 'and', column, operator, value })
262
+ return this
263
+ }
264
+
265
+ havingRaw(sql: string, bindings?: any[]): this {
266
+ this.state.havings.push({ type: 'raw', boolean: 'and', sql, bindings: bindings ?? [] })
267
+ return this
268
+ }
269
+
270
+ // ── Pagination ────────────────────────────────────────────────────────────
271
+
272
+ limit(value: number): this {
273
+ this.state.limitValue = value
274
+ return this
275
+ }
276
+
277
+ offset(value: number): this {
278
+ this.state.offsetValue = value
279
+ return this
280
+ }
281
+
282
+ take = this.limit
283
+ skip = this.offset
284
+
285
+ // ── Execution ─────────────────────────────────────────────────────────────
286
+
287
+ async get(): Promise<Record<string, any>[]> {
288
+ const { sql, bindings } = this.grammar().compileSelect(this.state)
289
+ return this._connection.select(sql, bindings)
290
+ }
291
+
292
+ async first(): Promise<Record<string, any> | null> {
293
+ const rows = await this.limit(1).get()
294
+ return rows[0] ?? null
295
+ }
296
+
297
+ async firstOrFail(): Promise<Record<string, any>> {
298
+ const row = await this.first()
299
+ if (!row) throw new ModelNotFoundError(this.state.table)
300
+ return row
301
+ }
302
+
303
+ async find(id: number | string): Promise<Record<string, any> | null> {
304
+ return this.where('id', id).first()
305
+ }
306
+
307
+ async value(column: string): Promise<any> {
308
+ const row = await this.select(column).first()
309
+ return row ? row[column] : null
310
+ }
311
+
312
+ async pluck(column: string): Promise<any[]> {
313
+ const rows = await this.select(column).get()
314
+ return rows.map((r) => r[column])
315
+ }
316
+
317
+ async exists(): Promise<boolean> {
318
+ const row = await this.selectRaw('1 as exists_check').limit(1).first()
319
+ return row !== null
320
+ }
321
+
322
+ async doesntExist(): Promise<boolean> {
323
+ return !(await this.exists())
324
+ }
325
+
326
+ /**
327
+ * Get the only record matching the query. Throws if zero or more than one.
328
+ */
329
+ async sole(): Promise<Record<string, any>> {
330
+ const results = await this.limit(2).get()
331
+ if (results.length === 0) throw new ModelNotFoundError(this.state.table)
332
+ if (results.length > 1) throw new Error(`Expected one result for table [${this.state.table}], found multiple.`)
333
+ return results[0]!
334
+ }
335
+
336
+ // ── Aggregates ────────────────────────────────────────────────────────────
337
+
338
+ async count(column = '*'): Promise<number> {
339
+ const row = await this.selectRaw(`COUNT(${column}) as aggregate`).first()
340
+ return Number(row?.['aggregate'] ?? 0)
341
+ }
342
+
343
+ async sum(column: string): Promise<number> {
344
+ const row = await this.selectRaw(`SUM(${column}) as aggregate`).first()
345
+ return Number(row?.['aggregate'] ?? 0)
346
+ }
347
+
348
+ async avg(column: string): Promise<number> {
349
+ const row = await this.selectRaw(`AVG(${column}) as aggregate`).first()
350
+ return Number(row?.['aggregate'] ?? 0)
351
+ }
352
+
353
+ async min(column: string): Promise<any> {
354
+ const row = await this.selectRaw(`MIN(${column}) as aggregate`).first()
355
+ return row?.['aggregate']
356
+ }
357
+
358
+ async max(column: string): Promise<any> {
359
+ const row = await this.selectRaw(`MAX(${column}) as aggregate`).first()
360
+ return row?.['aggregate']
361
+ }
362
+
363
+ // ── Writes ────────────────────────────────────────────────────────────────
364
+
365
+ async insert(data: Record<string, any> | Record<string, any>[]): Promise<void> {
366
+ const rows = Array.isArray(data) ? data : [data]
367
+ for (const row of rows) {
368
+ const { sql, bindings } = this.grammar().compileInsert(this.state.table, row)
369
+ await this._connection.statement(sql, bindings)
370
+ }
371
+ }
372
+
373
+ async insertGetId(data: Record<string, any>): Promise<number> {
374
+ const { sql, bindings } = this.grammar().compileInsertGetId(this.state.table, data)
375
+ const id = await this._connection.insertGetId(sql, bindings)
376
+ return Number(id)
377
+ }
378
+
379
+ async update(data: Record<string, any>): Promise<number> {
380
+ const { sql, bindings } = this.grammar().compileUpdate(this.state.table, this.state, data)
381
+ return this._connection.statement(sql, bindings)
382
+ }
383
+
384
+ async updateOrInsert(
385
+ conditions: Record<string, any>,
386
+ data: Record<string, any>,
387
+ ): Promise<void> {
388
+ const clone = this.clone()
389
+ for (const [k, v] of Object.entries(conditions)) clone.where(k, v)
390
+ const exists = await clone.exists()
391
+ if (exists) {
392
+ await clone.update(data)
393
+ } else {
394
+ await this.insert({ ...conditions, ...data })
395
+ }
396
+ }
397
+
398
+ async delete(): Promise<number> {
399
+ const { sql, bindings } = this.grammar().compileDelete(this.state.table, this.state)
400
+ return this._connection.statement(sql, bindings)
401
+ }
402
+
403
+ async truncate(): Promise<void> {
404
+ const sql = this.grammar().compileTruncate(this.state.table)
405
+ await this._connection.statement(sql, [])
406
+ }
407
+
408
+ // ── Pagination ────────────────────────────────────────────────────────────
409
+
410
+ async paginate(page = 1, perPage = 15): Promise<PaginationResult> {
411
+ const total = await this.clone().count()
412
+ const lastPage = Math.max(1, Math.ceil(total / perPage))
413
+ const currentPage = Math.min(page, lastPage)
414
+ const data = await this.clone().limit(perPage).offset((currentPage - 1) * perPage).get()
415
+ const from = total === 0 ? 0 : (currentPage - 1) * perPage + 1
416
+ const to = Math.min(from + data.length - 1, total)
417
+ return { data, total, perPage, currentPage, lastPage, from, to, hasMore: currentPage < lastPage }
418
+ }
419
+
420
+ // ── Utilities ─────────────────────────────────────────────────────────────
421
+
422
+ toSql(): string {
423
+ return this.grammar().compileSelect(this.state).sql
424
+ }
425
+
426
+ getBindings(): any[] {
427
+ return this.grammar().compileSelect(this.state).bindings
428
+ }
429
+
430
+ clone(): QueryBuilder {
431
+ const copy = new QueryBuilder(this._connection, this.state.table)
432
+ copy.state = {
433
+ ...this.state,
434
+ columns: [...this.state.columns],
435
+ wheres: [...this.state.wheres],
436
+ joins: [...this.state.joins],
437
+ orders: [...this.state.orders],
438
+ groups: [...this.state.groups],
439
+ havings: [...this.state.havings],
440
+ }
441
+ return copy
442
+ }
443
+
444
+ getState(): QueryState {
445
+ return this.state
446
+ }
447
+
448
+ // ── Grammar (driver-specific SQL) ─────────────────────────────────────────
449
+
450
+ protected grammar() {
451
+ return (this._connection as any)._grammar as import('../contracts/Grammar.ts').Grammar
452
+ }
453
+ }
454
+
455
+ // Add macro support — QueryBuilder.macro('name', fn) / instance.__macro('name')
456
+ applyMacros(QueryBuilder)
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Wraps a raw SQL string so the query builder won't escape/quote it.
3
+ * @example db.raw('COUNT(*) as total')
4
+ */
5
+ export class Expression {
6
+ constructor(
7
+ public readonly value: string,
8
+ public readonly bindings: any[] = [],
9
+ ) {}
10
+
11
+ toString(): string {
12
+ return this.value
13
+ }
14
+ }
15
+
16
+ export function raw(expression: string, bindings?: any[]): Expression {
17
+ return new Expression(expression, bindings ?? [])
18
+ }
@@ -0,0 +1,196 @@
1
+ import { ColumnDefinition } from './ColumnDefinition.ts'
2
+
3
+ export interface IndexDefinition {
4
+ type: 'index' | 'unique' | 'primary'
5
+ columns: string[]
6
+ name?: string
7
+ }
8
+
9
+ export interface ForeignKeyDefinition {
10
+ column: string
11
+ references: string
12
+ on: string
13
+ onDelete?: string
14
+ onUpdate?: string
15
+ }
16
+
17
+ export class Blueprint {
18
+ readonly columns: ColumnDefinition[] = []
19
+ readonly indexes: IndexDefinition[] = []
20
+ readonly foreignKeys: ForeignKeyDefinition[] = []
21
+ readonly droppedColumns: string[] = []
22
+ readonly droppedIndexes: string[] = []
23
+
24
+ // ── Common column types ────────────────────────────────────────────────────
25
+
26
+ id(name = 'id'): ColumnDefinition {
27
+ const col = new ColumnDefinition(name, 'bigIncrements')
28
+ this.columns.push(col)
29
+ return col
30
+ }
31
+
32
+ bigIncrements(name: string): ColumnDefinition {
33
+ return this.addColumn(name, 'bigIncrements')
34
+ }
35
+
36
+ increments(name: string): ColumnDefinition {
37
+ return this.addColumn(name, 'increments')
38
+ }
39
+
40
+ string(name: string, length = 255): ColumnDefinition {
41
+ return this.addColumn(name, 'string', length)
42
+ }
43
+
44
+ text(name: string): ColumnDefinition {
45
+ return this.addColumn(name, 'text')
46
+ }
47
+
48
+ longText(name: string): ColumnDefinition {
49
+ return this.addColumn(name, 'longText')
50
+ }
51
+
52
+ mediumText(name: string): ColumnDefinition {
53
+ return this.addColumn(name, 'mediumText')
54
+ }
55
+
56
+ integer(name: string): ColumnDefinition {
57
+ return this.addColumn(name, 'integer')
58
+ }
59
+
60
+ bigInteger(name: string): ColumnDefinition {
61
+ return this.addColumn(name, 'bigInteger')
62
+ }
63
+
64
+ tinyInteger(name: string): ColumnDefinition {
65
+ return this.addColumn(name, 'tinyInteger')
66
+ }
67
+
68
+ smallInteger(name: string): ColumnDefinition {
69
+ return this.addColumn(name, 'smallInteger')
70
+ }
71
+
72
+ unsignedInteger(name: string): ColumnDefinition {
73
+ return this.addColumn(name, 'unsignedInteger')
74
+ }
75
+
76
+ unsignedBigInteger(name: string): ColumnDefinition {
77
+ return this.addColumn(name, 'unsignedBigInteger')
78
+ }
79
+
80
+ float(name: string, precision = 8, scale = 2): ColumnDefinition {
81
+ return this.addColumn(name, 'float', undefined, precision, scale)
82
+ }
83
+
84
+ double(name: string, precision = 15, scale = 8): ColumnDefinition {
85
+ return this.addColumn(name, 'double', undefined, precision, scale)
86
+ }
87
+
88
+ decimal(name: string, precision = 8, scale = 2): ColumnDefinition {
89
+ return this.addColumn(name, 'decimal', undefined, precision, scale)
90
+ }
91
+
92
+ boolean(name: string): ColumnDefinition {
93
+ return this.addColumn(name, 'boolean')
94
+ }
95
+
96
+ date(name: string): ColumnDefinition {
97
+ return this.addColumn(name, 'date')
98
+ }
99
+
100
+ dateTime(name: string): ColumnDefinition {
101
+ return this.addColumn(name, 'dateTime')
102
+ }
103
+
104
+ timestamp(name: string): ColumnDefinition {
105
+ return this.addColumn(name, 'timestamp')
106
+ }
107
+
108
+ timestamps(): void {
109
+ this.timestamp('created_at').nullable()
110
+ this.timestamp('updated_at').nullable()
111
+ }
112
+
113
+ softDeletes(column = 'deleted_at'): ColumnDefinition {
114
+ return this.timestamp(column).nullable()
115
+ }
116
+
117
+ json(name: string): ColumnDefinition {
118
+ return this.addColumn(name, 'json')
119
+ }
120
+
121
+ jsonb(name: string): ColumnDefinition {
122
+ return this.addColumn(name, 'jsonb')
123
+ }
124
+
125
+ uuid(name: string): ColumnDefinition {
126
+ return this.addColumn(name, 'uuid')
127
+ }
128
+
129
+ binary(name: string): ColumnDefinition {
130
+ return this.addColumn(name, 'binary')
131
+ }
132
+
133
+ enum(name: string, values: string[]): ColumnDefinition {
134
+ const col = new ColumnDefinition(name, `enum:${values.join(',')}`)
135
+ this.columns.push(col)
136
+ return col
137
+ }
138
+
139
+ // ── Foreign key shortcuts ─────────────────────────────────────────────────
140
+
141
+ foreignId(name: string): ColumnDefinition {
142
+ return this.unsignedBigInteger(name)
143
+ }
144
+
145
+ foreign(column: string): { references(col: string): { on(table: string): ForeignKeyDefinition } } {
146
+ return {
147
+ references: (col: string) => ({
148
+ on: (table: string): ForeignKeyDefinition => {
149
+ const fk: ForeignKeyDefinition = { column, references: col, on: table }
150
+ this.foreignKeys.push(fk)
151
+ return fk
152
+ },
153
+ }),
154
+ }
155
+ }
156
+
157
+ // ── Indexes ────────────────────────────────────────────────────────────────
158
+
159
+ index(columns: string | string[], name?: string): void {
160
+ this.indexes.push({ type: 'index', columns: Array.isArray(columns) ? columns : [columns], name })
161
+ }
162
+
163
+ unique(columns: string | string[], name?: string): void {
164
+ this.indexes.push({ type: 'unique', columns: Array.isArray(columns) ? columns : [columns], name })
165
+ }
166
+
167
+ primary(columns: string | string[]): void {
168
+ this.indexes.push({ type: 'primary', columns: Array.isArray(columns) ? columns : [columns] })
169
+ }
170
+
171
+ // ── Modify / drop ─────────────────────────────────────────────────────────
172
+
173
+ dropColumn(name: string): void {
174
+ this.droppedColumns.push(name)
175
+ }
176
+
177
+ dropTimestamps(): void {
178
+ this.droppedColumns.push('created_at', 'updated_at')
179
+ }
180
+
181
+ dropSoftDeletes(column = 'deleted_at'): void {
182
+ this.droppedColumns.push(column)
183
+ }
184
+
185
+ dropIndex(name: string): void {
186
+ this.droppedIndexes.push(name)
187
+ }
188
+
189
+ // ── Helpers ───────────────────────────────────────────────────────────────
190
+
191
+ private addColumn(name: string, type: string, length?: number, precision?: number, scale?: number): ColumnDefinition {
192
+ const col = new ColumnDefinition(name, type, length, precision, scale)
193
+ this.columns.push(col)
194
+ return col
195
+ }
196
+ }