@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.
- package/README.md +19 -0
- package/package.json +77 -0
- package/src/DatabaseManager.ts +115 -0
- package/src/DatabaseServiceProvider.ts +39 -0
- package/src/contracts/Connection.ts +13 -0
- package/src/contracts/Grammar.ts +16 -0
- package/src/contracts/MongoConnection.ts +122 -0
- package/src/contracts/Paginator.ts +10 -0
- package/src/drivers/BaseGrammar.ts +220 -0
- package/src/drivers/MSSQLConnection.ts +154 -0
- package/src/drivers/MSSQLGrammar.ts +106 -0
- package/src/drivers/MongoConnection.ts +298 -0
- package/src/drivers/MongoQueryBuilderImpl.ts +77 -0
- package/src/drivers/MySQLConnection.ts +120 -0
- package/src/drivers/MySQLGrammar.ts +19 -0
- package/src/drivers/PostgresConnection.ts +125 -0
- package/src/drivers/PostgresGrammar.ts +24 -0
- package/src/drivers/SQLiteConnection.ts +125 -0
- package/src/drivers/SQLiteGrammar.ts +19 -0
- package/src/errors/ConnectionError.ts +10 -0
- package/src/errors/ModelNotFoundError.ts +14 -0
- package/src/errors/QueryError.ts +11 -0
- package/src/events/DatabaseEvents.ts +101 -0
- package/src/factories/Factory.ts +170 -0
- package/src/factories/Faker.ts +382 -0
- package/src/helpers/db.ts +37 -0
- package/src/index.ts +100 -0
- package/src/migrations/Migration.ts +12 -0
- package/src/migrations/MigrationRepository.ts +50 -0
- package/src/migrations/Migrator.ts +201 -0
- package/src/orm/Collection.ts +236 -0
- package/src/orm/Document.ts +202 -0
- package/src/orm/Model.ts +775 -0
- package/src/orm/ModelQueryBuilder.ts +415 -0
- package/src/orm/Scope.ts +39 -0
- package/src/orm/eagerLoad.ts +300 -0
- package/src/query/Builder.ts +456 -0
- package/src/query/Expression.ts +18 -0
- package/src/schema/Blueprint.ts +196 -0
- package/src/schema/ColumnDefinition.ts +93 -0
- package/src/schema/SchemaBuilder.ts +376 -0
- 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
|
+
}
|