@simplysm/orm-common 13.0.96 → 13.0.98
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 +209 -54
- package/docs/core.md +225 -0
- package/docs/expression.md +296 -0
- package/docs/query-builder.md +196 -0
- package/docs/queryable.md +578 -0
- package/docs/schema-builders.md +415 -0
- package/docs/types.md +445 -0
- package/docs/utilities.md +122 -0
- package/package.json +2 -2
- package/docs/ddl.md +0 -300
- package/docs/query.md +0 -644
- package/docs/schema.md +0 -325
|
@@ -0,0 +1,578 @@
|
|
|
1
|
+
# Queryable / Executable
|
|
2
|
+
|
|
3
|
+
Type-safe query builder and stored procedure executor.
|
|
4
|
+
|
|
5
|
+
Source: `src/exec/queryable.ts`, `src/exec/executable.ts`, `src/exec/search-parser.ts`
|
|
6
|
+
|
|
7
|
+
## Queryable
|
|
8
|
+
|
|
9
|
+
Immutable chaining query builder for SELECT, INSERT, UPDATE, DELETE, and UPSERT operations. Each method returns a new `Queryable` instance.
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
class Queryable<TData extends DataRecord, TFrom extends TableBuilder<any, any> | never> {
|
|
13
|
+
constructor(readonly meta: QueryableMeta<TData>);
|
|
14
|
+
// ... chaining and execution methods
|
|
15
|
+
}
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
### Column Selection
|
|
19
|
+
|
|
20
|
+
#### select
|
|
21
|
+
|
|
22
|
+
Specify columns to SELECT. Returns a new column structure.
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
select<R extends Record<string, any>>(
|
|
26
|
+
fn: (columns: QueryableRecord<TData>) => R,
|
|
27
|
+
): Queryable<UnwrapQueryableRecord<R>, never>;
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
db.user().select((u) => ({
|
|
32
|
+
userName: u.name,
|
|
33
|
+
userEmail: u.email,
|
|
34
|
+
}))
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
#### distinct
|
|
38
|
+
|
|
39
|
+
Apply DISTINCT to remove duplicate rows.
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
distinct(): Queryable<TData, never>;
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
#### lock
|
|
46
|
+
|
|
47
|
+
Apply row lock (FOR UPDATE) for exclusive access within a transaction.
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
lock(): Queryable<TData, TFrom>;
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Pagination
|
|
54
|
+
|
|
55
|
+
#### top
|
|
56
|
+
|
|
57
|
+
Select only the top N rows (can be used without ORDER BY).
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
top(count: number): Queryable<TData, TFrom>;
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
#### limit
|
|
64
|
+
|
|
65
|
+
Set LIMIT/OFFSET for pagination. Requires a preceding `orderBy()`.
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
limit(skip: number, take: number): Queryable<TData, TFrom>;
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Sorting
|
|
72
|
+
|
|
73
|
+
#### orderBy
|
|
74
|
+
|
|
75
|
+
Add an ORDER BY clause. Multiple calls apply in order.
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
orderBy(
|
|
79
|
+
fn: (columns: QueryableRecord<TData>) => ExprUnit<ColumnPrimitive>,
|
|
80
|
+
orderBy?: "ASC" | "DESC",
|
|
81
|
+
): Queryable<TData, TFrom>;
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
db.user()
|
|
86
|
+
.orderBy((u) => u.name)
|
|
87
|
+
.orderBy((u) => u.age, "DESC")
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Filtering
|
|
91
|
+
|
|
92
|
+
#### where
|
|
93
|
+
|
|
94
|
+
Add a WHERE condition. Multiple calls are combined with AND.
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
where(
|
|
98
|
+
predicate: (columns: QueryableRecord<TData>) => WhereExprUnit[],
|
|
99
|
+
): Queryable<TData, TFrom>;
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
db.user()
|
|
104
|
+
.where((u) => [expr.eq(u.isActive, true)])
|
|
105
|
+
.where((u) => [expr.gte(u.age, 18)])
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
#### search
|
|
109
|
+
|
|
110
|
+
Perform text search across multiple columns. Supports OR, +must, and -exclude syntax.
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
search(
|
|
114
|
+
fn: (columns: QueryableRecord<TData>) => ExprUnit<string | undefined>[],
|
|
115
|
+
searchText: string,
|
|
116
|
+
): Queryable<TData, TFrom>;
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
db.user()
|
|
121
|
+
.search((u) => [u.name, u.email], "John Doe -withdrawn")
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Grouping
|
|
125
|
+
|
|
126
|
+
#### groupBy
|
|
127
|
+
|
|
128
|
+
Add a GROUP BY clause.
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
groupBy(
|
|
132
|
+
fn: (columns: QueryableRecord<TData>) => ExprUnit<ColumnPrimitive>[],
|
|
133
|
+
): Queryable<TData, never>;
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
#### having
|
|
137
|
+
|
|
138
|
+
Add a HAVING clause (filtering after GROUP BY).
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
having(
|
|
142
|
+
predicate: (columns: QueryableRecord<TData>) => WhereExprUnit[],
|
|
143
|
+
): Queryable<TData, never>;
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
db.order()
|
|
148
|
+
.select((o) => ({
|
|
149
|
+
userId: o.userId,
|
|
150
|
+
totalAmount: expr.sum(o.amount),
|
|
151
|
+
}))
|
|
152
|
+
.groupBy((o) => [o.userId])
|
|
153
|
+
.having((o) => [expr.gte(o.totalAmount, 10000)])
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Joins
|
|
157
|
+
|
|
158
|
+
#### join
|
|
159
|
+
|
|
160
|
+
LEFT OUTER JOIN for 1:N relations (result added as array).
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
join<A extends string, R extends DataRecord>(
|
|
164
|
+
as: A,
|
|
165
|
+
fn: (qr: JoinQueryable, cols: QueryableRecord<TData>) => Queryable<R, any>,
|
|
166
|
+
): Queryable<TData & { [K in A]?: R[] }, TFrom>;
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
db.user()
|
|
171
|
+
.join("posts", (qr, u) =>
|
|
172
|
+
qr.from(Post).where((p) => [expr.eq(p.userId, u.id)])
|
|
173
|
+
)
|
|
174
|
+
// Result: { id, name, posts: [{ id, title }, ...] }
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
#### joinSingle
|
|
178
|
+
|
|
179
|
+
LEFT OUTER JOIN for N:1 or 1:1 relations (result added as single object).
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
joinSingle<A extends string, R extends DataRecord>(
|
|
183
|
+
as: A,
|
|
184
|
+
fn: (qr: JoinQueryable, cols: QueryableRecord<TData>) => Queryable<R, any>,
|
|
185
|
+
): Queryable<TData & { [K in A]?: R }, TFrom>;
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
db.post()
|
|
190
|
+
.joinSingle("user", (qr, p) =>
|
|
191
|
+
qr.from(User).where((u) => [expr.eq(u.id, p.userId)])
|
|
192
|
+
)
|
|
193
|
+
// Result: { id, title, user: { id, name } | undefined }
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
#### include
|
|
197
|
+
|
|
198
|
+
Automatically JOIN related tables based on FK/FKT relations defined in `TableBuilder`.
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
include(fn: (item: PathProxy<TData>) => PathProxy<any>): Queryable<TData, TFrom>;
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
// Single relation
|
|
206
|
+
db.post().include((p) => p.author)
|
|
207
|
+
|
|
208
|
+
// Nested relation
|
|
209
|
+
db.post().include((p) => p.author.company)
|
|
210
|
+
|
|
211
|
+
// Multiple relations
|
|
212
|
+
db.user()
|
|
213
|
+
.include((u) => u.company)
|
|
214
|
+
.include((u) => u.posts)
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### Subqueries
|
|
218
|
+
|
|
219
|
+
#### wrap
|
|
220
|
+
|
|
221
|
+
Wrap the current Queryable as a subquery. Required when using `count()` after `distinct()` or `groupBy()`.
|
|
222
|
+
|
|
223
|
+
```typescript
|
|
224
|
+
wrap(): Queryable<TData, never>;
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
#### union (static)
|
|
228
|
+
|
|
229
|
+
Combine multiple Queryables with UNION (removes duplicates). Requires at least 2 queryables.
|
|
230
|
+
|
|
231
|
+
```typescript
|
|
232
|
+
static union<TData extends DataRecord>(
|
|
233
|
+
...queries: Queryable<TData, any>[]
|
|
234
|
+
): Queryable<TData, never>;
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
```typescript
|
|
238
|
+
const combined = Queryable.union(
|
|
239
|
+
db.user().where((u) => [expr.eq(u.type, "admin")]),
|
|
240
|
+
db.user().where((u) => [expr.eq(u.type, "manager")]),
|
|
241
|
+
);
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### Recursive CTE
|
|
245
|
+
|
|
246
|
+
#### recursive
|
|
247
|
+
|
|
248
|
+
Generate a recursive CTE (Common Table Expression) for hierarchical data.
|
|
249
|
+
|
|
250
|
+
```typescript
|
|
251
|
+
recursive(
|
|
252
|
+
fn: (qr: RecursiveQueryable<TData>) => Queryable<TData, any>,
|
|
253
|
+
): Queryable<TData, never>;
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
```typescript
|
|
257
|
+
db.employee()
|
|
258
|
+
.where((e) => [expr.null(e.managerId)])
|
|
259
|
+
.recursive((cte) =>
|
|
260
|
+
cte.from(Employee)
|
|
261
|
+
.where((e) => [expr.eq(e.managerId, e.self![0].id)])
|
|
262
|
+
)
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### Execution Methods
|
|
266
|
+
|
|
267
|
+
#### execute
|
|
268
|
+
|
|
269
|
+
Execute a SELECT query and return the result array.
|
|
270
|
+
|
|
271
|
+
```typescript
|
|
272
|
+
async execute(): Promise<TData[]>;
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
#### single
|
|
276
|
+
|
|
277
|
+
Return a single result. Throws if more than one result is returned.
|
|
278
|
+
|
|
279
|
+
```typescript
|
|
280
|
+
async single(): Promise<TData | undefined>;
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
#### first
|
|
284
|
+
|
|
285
|
+
Return the first result (only the first even if multiple exist).
|
|
286
|
+
|
|
287
|
+
```typescript
|
|
288
|
+
async first(): Promise<TData | undefined>;
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
#### count
|
|
292
|
+
|
|
293
|
+
Return the number of result rows. Cannot be called directly after `distinct()` or `groupBy()` -- use `wrap()` first.
|
|
294
|
+
|
|
295
|
+
```typescript
|
|
296
|
+
async count(
|
|
297
|
+
fn?: (cols: QueryableRecord<TData>) => ExprUnit<ColumnPrimitive>,
|
|
298
|
+
): Promise<number>;
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
#### exists
|
|
302
|
+
|
|
303
|
+
Check whether any data matching the conditions exists.
|
|
304
|
+
|
|
305
|
+
```typescript
|
|
306
|
+
async exists(): Promise<boolean>;
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
### Mutation Methods
|
|
310
|
+
|
|
311
|
+
#### insert
|
|
312
|
+
|
|
313
|
+
Execute an INSERT query. Automatically splits into chunks of 1000 for MSSQL.
|
|
314
|
+
|
|
315
|
+
```typescript
|
|
316
|
+
async insert(records: TFrom["$inferInsert"][]): Promise<void>;
|
|
317
|
+
async insert<K extends keyof TFrom["$inferColumns"] & string>(
|
|
318
|
+
records: TFrom["$inferInsert"][],
|
|
319
|
+
outputColumns: K[],
|
|
320
|
+
): Promise<Pick<TFrom["$inferColumns"], K>[]>;
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
```typescript
|
|
324
|
+
// Simple insert
|
|
325
|
+
await db.user().insert([{ name: "Alice", createdAt: DateTime.now() }]);
|
|
326
|
+
|
|
327
|
+
// Insert with output
|
|
328
|
+
const [inserted] = await db.user().insert(
|
|
329
|
+
[{ name: "Alice" }],
|
|
330
|
+
["id"],
|
|
331
|
+
);
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
#### insertIfNotExists
|
|
335
|
+
|
|
336
|
+
INSERT only if no data matches the current WHERE condition.
|
|
337
|
+
|
|
338
|
+
```typescript
|
|
339
|
+
async insertIfNotExists(record: TFrom["$inferInsert"]): Promise<void>;
|
|
340
|
+
async insertIfNotExists<K extends keyof TFrom["$inferColumns"] & string>(
|
|
341
|
+
record: TFrom["$inferInsert"],
|
|
342
|
+
outputColumns: K[],
|
|
343
|
+
): Promise<Pick<TFrom["$inferColumns"], K>>;
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
#### insertInto
|
|
347
|
+
|
|
348
|
+
INSERT INTO ... SELECT -- insert the current SELECT results into another table.
|
|
349
|
+
|
|
350
|
+
```typescript
|
|
351
|
+
async insertInto<TTable extends TableBuilder<DataToColumnBuilderRecord<TData>, any>>(
|
|
352
|
+
targetTable: TTable,
|
|
353
|
+
): Promise<void>;
|
|
354
|
+
async insertInto<TTable, TOut extends keyof TTable["$inferColumns"] & string>(
|
|
355
|
+
targetTable: TTable,
|
|
356
|
+
outputColumns: TOut[],
|
|
357
|
+
): Promise<Pick<TData, TOut>[]>;
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
#### update
|
|
361
|
+
|
|
362
|
+
Execute an UPDATE query.
|
|
363
|
+
|
|
364
|
+
```typescript
|
|
365
|
+
async update(
|
|
366
|
+
recordFwd: (cols: QueryableRecord<TData>) => QueryableWriteRecord<TFrom["$inferUpdate"]>,
|
|
367
|
+
): Promise<void>;
|
|
368
|
+
async update<K extends keyof TFrom["$columns"] & string>(
|
|
369
|
+
recordFwd: (cols: QueryableRecord<TData>) => QueryableWriteRecord<TFrom["$inferUpdate"]>,
|
|
370
|
+
outputColumns: K[],
|
|
371
|
+
): Promise<Pick<TFrom["$columns"], K>[]>;
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
```typescript
|
|
375
|
+
await db.user()
|
|
376
|
+
.where((u) => [expr.eq(u.id, 1)])
|
|
377
|
+
.update((u) => ({
|
|
378
|
+
name: expr.val("string", "New Name"),
|
|
379
|
+
}));
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
#### delete
|
|
383
|
+
|
|
384
|
+
Execute a DELETE query.
|
|
385
|
+
|
|
386
|
+
```typescript
|
|
387
|
+
async delete(): Promise<void>;
|
|
388
|
+
async delete<K extends keyof TFrom["$columns"] & string>(
|
|
389
|
+
outputColumns: K[],
|
|
390
|
+
): Promise<Pick<TFrom["$columns"], K>[]>;
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
#### upsert
|
|
394
|
+
|
|
395
|
+
Execute an UPSERT (UPDATE or INSERT) query.
|
|
396
|
+
|
|
397
|
+
```typescript
|
|
398
|
+
async upsert(
|
|
399
|
+
updateFn: (cols: QueryableRecord<TData>) => QueryableWriteRecord<TFrom["$inferUpdate"]>,
|
|
400
|
+
): Promise<void>;
|
|
401
|
+
async upsert<U extends QueryableWriteRecord<TFrom["$inferUpdate"]>>(
|
|
402
|
+
updateFn: (cols: QueryableRecord<TData>) => U,
|
|
403
|
+
insertFn: (updateRecord: U) => QueryableWriteRecord<TFrom["$inferInsert"]>,
|
|
404
|
+
): Promise<void>;
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
```typescript
|
|
408
|
+
// Same data for update and insert
|
|
409
|
+
await db.user()
|
|
410
|
+
.where((u) => [expr.eq(u.email, "test@test.com")])
|
|
411
|
+
.upsert(() => ({
|
|
412
|
+
name: expr.val("string", "Test"),
|
|
413
|
+
email: expr.val("string", "test@test.com"),
|
|
414
|
+
}));
|
|
415
|
+
|
|
416
|
+
// Different data for update vs insert
|
|
417
|
+
await db.user()
|
|
418
|
+
.where((u) => [expr.eq(u.email, "test@test.com")])
|
|
419
|
+
.upsert(
|
|
420
|
+
() => ({ loginCount: expr.val("number", 1) }),
|
|
421
|
+
(update) => ({ ...update, email: expr.val("string", "test@test.com") }),
|
|
422
|
+
);
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
### DDL Helper
|
|
426
|
+
|
|
427
|
+
#### switchFk
|
|
428
|
+
|
|
429
|
+
Enable or disable FK constraints on the table (usable within a transaction).
|
|
430
|
+
|
|
431
|
+
```typescript
|
|
432
|
+
async switchFk(enabled: boolean): Promise<void>;
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
### QueryDef Generators
|
|
436
|
+
|
|
437
|
+
These methods return the raw `QueryDef` JSON AST without executing:
|
|
438
|
+
|
|
439
|
+
- `getSelectQueryDef(): SelectQueryDef`
|
|
440
|
+
- `getInsertQueryDef(records, outputColumns?): InsertQueryDef`
|
|
441
|
+
- `getInsertIfNotExistsQueryDef(record, outputColumns?): InsertIfNotExistsQueryDef`
|
|
442
|
+
- `getInsertIntoQueryDef(targetTable, outputColumns?): InsertIntoQueryDef`
|
|
443
|
+
- `getUpdateQueryDef(recordFwd, outputColumns?): UpdateQueryDef`
|
|
444
|
+
- `getDeleteQueryDef(outputColumns?): DeleteQueryDef`
|
|
445
|
+
- `getUpsertQueryDef(updateRecordFn, insertRecordFn, outputColumns?): UpsertQueryDef`
|
|
446
|
+
- `getResultMeta(outputColumns?): ResultMeta`
|
|
447
|
+
|
|
448
|
+
---
|
|
449
|
+
|
|
450
|
+
## queryable (factory function)
|
|
451
|
+
|
|
452
|
+
Create a Queryable factory function for a table or view. A new alias is assigned on each call.
|
|
453
|
+
|
|
454
|
+
```typescript
|
|
455
|
+
function queryable<TBuilder extends TableBuilder<any, any> | ViewBuilder<any, any, any>>(
|
|
456
|
+
db: DbContextBase,
|
|
457
|
+
tableOrView: TBuilder,
|
|
458
|
+
as?: string,
|
|
459
|
+
): () => Queryable<TBuilder["$inferSelect"], TBuilder extends TableBuilder<any, any> ? TBuilder : never>;
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
---
|
|
463
|
+
|
|
464
|
+
## getMatchedPrimaryKeys
|
|
465
|
+
|
|
466
|
+
Match FK column array with the target table's PK and return PK column name array.
|
|
467
|
+
|
|
468
|
+
```typescript
|
|
469
|
+
function getMatchedPrimaryKeys(
|
|
470
|
+
fkCols: string[],
|
|
471
|
+
targetTable: TableBuilder<any, any>,
|
|
472
|
+
): string[];
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
Throws if FK/PK column count does not match.
|
|
476
|
+
|
|
477
|
+
---
|
|
478
|
+
|
|
479
|
+
## Executable
|
|
480
|
+
|
|
481
|
+
Stored procedure execution wrapper.
|
|
482
|
+
|
|
483
|
+
```typescript
|
|
484
|
+
class Executable<
|
|
485
|
+
TParams extends ColumnBuilderRecord,
|
|
486
|
+
TReturns extends ColumnBuilderRecord,
|
|
487
|
+
> {
|
|
488
|
+
getExecProcQueryDef(params?: InferColumnExprs<TParams>): ExecProcQueryDef;
|
|
489
|
+
async execute(params: InferColumnExprs<TParams>): Promise<InferColumnExprs<TReturns>[][]>;
|
|
490
|
+
}
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
```typescript
|
|
494
|
+
const result = await db.getUserById().execute({ userId: 1n });
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
---
|
|
498
|
+
|
|
499
|
+
## executable (factory function)
|
|
500
|
+
|
|
501
|
+
Create an Executable factory function for a procedure.
|
|
502
|
+
|
|
503
|
+
```typescript
|
|
504
|
+
function executable<
|
|
505
|
+
TParams extends ColumnBuilderRecord,
|
|
506
|
+
TReturns extends ColumnBuilderRecord,
|
|
507
|
+
>(
|
|
508
|
+
db: DbContextBase,
|
|
509
|
+
builder: ProcedureBuilder<TParams, TReturns>,
|
|
510
|
+
): () => Executable<TParams, TReturns>;
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
---
|
|
514
|
+
|
|
515
|
+
## parseSearchQuery
|
|
516
|
+
|
|
517
|
+
Parse a search query string into SQL LIKE patterns.
|
|
518
|
+
|
|
519
|
+
```typescript
|
|
520
|
+
function parseSearchQuery(searchText: string): ParsedSearchQuery;
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
### ParsedSearchQuery
|
|
524
|
+
|
|
525
|
+
```typescript
|
|
526
|
+
interface ParsedSearchQuery {
|
|
527
|
+
/** General search terms (OR condition) - LIKE pattern */
|
|
528
|
+
or: string[];
|
|
529
|
+
/** Required search terms (AND condition, + prefix or quotes) - LIKE pattern */
|
|
530
|
+
must: string[];
|
|
531
|
+
/** Excluded search terms (NOT condition, - prefix) - LIKE pattern */
|
|
532
|
+
not: string[];
|
|
533
|
+
}
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
### Search Syntax
|
|
537
|
+
|
|
538
|
+
| Syntax | Meaning | Example |
|
|
539
|
+
|--------|---------|---------|
|
|
540
|
+
| `term1 term2` | OR (one of them) | `apple banana` |
|
|
541
|
+
| `+term` | Required (AND) | `+apple +banana` |
|
|
542
|
+
| `-term` | Excluded (NOT) | `apple -banana` |
|
|
543
|
+
| `"exact phrase"` | Exact match (required) | `"delicious fruit"` |
|
|
544
|
+
| `*` | Wildcard | `app*` -> `app%` |
|
|
545
|
+
|
|
546
|
+
### Escape Sequences
|
|
547
|
+
|
|
548
|
+
| Input | Meaning |
|
|
549
|
+
|-------|---------|
|
|
550
|
+
| `\\` | Literal `\` |
|
|
551
|
+
| `\*` | Literal `*` |
|
|
552
|
+
| `\%` | Literal `%` |
|
|
553
|
+
| `\"` | Literal `"` |
|
|
554
|
+
| `\+` | Literal `+` |
|
|
555
|
+
| `\-` | Literal `-` |
|
|
556
|
+
|
|
557
|
+
**Example:**
|
|
558
|
+
|
|
559
|
+
```typescript
|
|
560
|
+
parseSearchQuery('apple "delicious fruit" -banana +strawberry')
|
|
561
|
+
// {
|
|
562
|
+
// or: ["%apple%"],
|
|
563
|
+
// must: ["%delicious fruit%", "%strawberry%"],
|
|
564
|
+
// not: ["%banana%"]
|
|
565
|
+
// }
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
---
|
|
569
|
+
|
|
570
|
+
## Type Helpers
|
|
571
|
+
|
|
572
|
+
| Type | Description |
|
|
573
|
+
|------|-------------|
|
|
574
|
+
| `QueryableRecord<TData>` | Maps `TData` fields to `ExprUnit` wrappers (for column references in callbacks) |
|
|
575
|
+
| `QueryableWriteRecord<TData>` | Maps `TData` fields to `ExprInput` (for UPDATE/INSERT value callbacks) |
|
|
576
|
+
| `NullableQueryableRecord<TData>` | Like `QueryableRecord` but all primitives are `| undefined` (LEFT JOIN null propagation) |
|
|
577
|
+
| `UnwrapQueryableRecord<R>` | Reverse-transform from `QueryableRecord` back to `DataRecord` |
|
|
578
|
+
| `PathProxy<TObject>` | Type-safe path proxy for `include()` -- only non-primitive fields are accessible |
|