@simplysm/orm-common 13.0.97 → 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.
@@ -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 |