@simplysm/orm-common 13.0.81 → 13.0.83

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,413 @@
1
+ # Expression Builder
2
+
3
+ The `expr` object provides a dialect-independent SQL expression AST builder. Expressions are compiled to SQL by the QueryBuilder for each target DBMS (MySQL, MSSQL, PostgreSQL).
4
+
5
+ ## API Reference
6
+
7
+ ### Value Creation
8
+
9
+ #### `expr.val(dataType, value)`
10
+
11
+ Wrap a literal value as an `ExprUnit`.
12
+
13
+ ```typescript
14
+ expr.val("string", "active")
15
+ expr.val("number", 100)
16
+ expr.val("DateOnly", DateOnly.today())
17
+ ```
18
+
19
+ #### `expr.col(dataType, ...path)`
20
+
21
+ Create a column reference (typically used internally; proxy objects in Queryable callbacks handle this automatically).
22
+
23
+ ```typescript
24
+ expr.col("string", "T1", "name")
25
+ ```
26
+
27
+ #### `expr.raw(dataType)\`template\``
28
+
29
+ Raw SQL expression (escape hatch). Interpolated values are automatically parameterized.
30
+
31
+ ```typescript
32
+ db.user().select((u) => ({
33
+ data: expr.raw("string")`JSON_EXTRACT(${u.metadata}, '$.email')`,
34
+ }))
35
+ ```
36
+
37
+ ---
38
+
39
+ ### Comparison Operators (WHERE)
40
+
41
+ All comparison operators return `WhereExprUnit` for use in `.where()` callbacks.
42
+
43
+ | Method | SQL | Description |
44
+ |--------|-----|-------------|
45
+ | `expr.eq(source, target)` | `=` (NULL-safe) | Equality comparison |
46
+ | `expr.gt(source, target)` | `>` | Greater than |
47
+ | `expr.lt(source, target)` | `<` | Less than |
48
+ | `expr.gte(source, target)` | `>=` | Greater than or equal |
49
+ | `expr.lte(source, target)` | `<=` | Less than or equal |
50
+ | `expr.between(source, from?, to?)` | `BETWEEN` | Range comparison (undefined = unbounded) |
51
+
52
+ ```typescript
53
+ db.user().where((u) => [
54
+ expr.eq(u.status, "active"),
55
+ expr.gte(u.age, 18),
56
+ expr.between(u.score, 60, 100),
57
+ ])
58
+ ```
59
+
60
+ ---
61
+
62
+ ### NULL Check
63
+
64
+ #### `expr.null(source)`
65
+
66
+ Check if value is NULL (`IS NULL`).
67
+
68
+ ```typescript
69
+ db.user().where((u) => [expr.null(u.deletedAt)])
70
+ ```
71
+
72
+ ---
73
+
74
+ ### String Search (WHERE)
75
+
76
+ #### `expr.like(source, pattern)`
77
+
78
+ LIKE pattern matching. `%` matches any characters, `_` matches single character.
79
+
80
+ ```typescript
81
+ db.user().where((u) => [expr.like(u.name, "John%")])
82
+ ```
83
+
84
+ #### `expr.regexp(source, pattern)`
85
+
86
+ Regular expression matching. Syntax varies by DBMS.
87
+
88
+ ```typescript
89
+ db.user().where((u) => [expr.regexp(u.email, "^[a-z]+@")])
90
+ ```
91
+
92
+ ---
93
+
94
+ ### IN / EXISTS (WHERE)
95
+
96
+ #### `expr.in(source, values)`
97
+
98
+ Check if value is in a list.
99
+
100
+ ```typescript
101
+ db.user().where((u) => [expr.in(u.status, ["active", "pending"])])
102
+ ```
103
+
104
+ #### `expr.inQuery(source, query)`
105
+
106
+ Check if value is in a subquery result (subquery must SELECT a single column).
107
+
108
+ ```typescript
109
+ db.user().where((u) => [
110
+ expr.inQuery(
111
+ u.id,
112
+ db.order().where((o) => [expr.gt(o.amount, 1000)]).select((o) => ({ userId: o.userId })),
113
+ ),
114
+ ])
115
+ ```
116
+
117
+ #### `expr.exists(query)`
118
+
119
+ Check if a subquery returns any rows.
120
+
121
+ ```typescript
122
+ db.user().where((u) => [
123
+ expr.exists(db.order().where((o) => [expr.eq(o.userId, u.id)])),
124
+ ])
125
+ ```
126
+
127
+ ---
128
+
129
+ ### Logical Operators (WHERE)
130
+
131
+ #### `expr.not(condition)`
132
+
133
+ Negate a condition.
134
+
135
+ ```typescript
136
+ expr.not(expr.eq(u.status, "deleted"))
137
+ ```
138
+
139
+ #### `expr.and(conditions)`
140
+
141
+ Combine conditions with AND (note: `.where()` arrays are implicitly AND-ed).
142
+
143
+ ```typescript
144
+ expr.and([expr.eq(u.status, "active"), expr.gte(u.age, 18)])
145
+ ```
146
+
147
+ #### `expr.or(conditions)`
148
+
149
+ Combine conditions with OR.
150
+
151
+ ```typescript
152
+ expr.or([expr.eq(u.status, "active"), expr.eq(u.status, "pending")])
153
+ ```
154
+
155
+ ---
156
+
157
+ ### String Functions (SELECT)
158
+
159
+ | Method | SQL | Description |
160
+ |--------|-----|-------------|
161
+ | `expr.concat(...args)` | `CONCAT` | Concatenate strings (NULL treated as empty) |
162
+ | `expr.left(source, length)` | `LEFT` | Extract left N characters |
163
+ | `expr.right(source, length)` | `RIGHT` | Extract right N characters |
164
+ | `expr.trim(source)` | `TRIM` | Remove leading/trailing whitespace |
165
+ | `expr.padStart(source, length, fill)` | `LPAD` | Left-pad string |
166
+ | `expr.replace(source, from, to)` | `REPLACE` | Replace occurrences |
167
+ | `expr.upper(source)` | `UPPER` | Convert to uppercase |
168
+ | `expr.lower(source)` | `LOWER` | Convert to lowercase |
169
+ | `expr.length(source)` | `CHAR_LENGTH` | Character count |
170
+ | `expr.byteLength(source)` | `OCTET_LENGTH` | Byte count |
171
+ | `expr.substring(source, start, length?)` | `SUBSTRING` | Extract substring (1-based index) |
172
+ | `expr.indexOf(source, search)` | `LOCATE/CHARINDEX` | Find position (1-based, 0 if not found) |
173
+
174
+ ```typescript
175
+ db.user().select((u) => ({
176
+ fullName: expr.concat(u.firstName, " ", u.lastName),
177
+ initial: expr.left(u.name, 1),
178
+ phone: expr.replace(u.phone, "-", ""),
179
+ }))
180
+ ```
181
+
182
+ ---
183
+
184
+ ### Numeric Functions (SELECT)
185
+
186
+ | Method | SQL | Description |
187
+ |--------|-----|-------------|
188
+ | `expr.abs(source)` | `ABS` | Absolute value |
189
+ | `expr.round(source, digits)` | `ROUND` | Round to N decimal places |
190
+ | `expr.ceil(source)` | `CEILING` | Round up |
191
+ | `expr.floor(source)` | `FLOOR` | Round down |
192
+
193
+ ```typescript
194
+ db.product().select((p) => ({
195
+ price: expr.round(p.price, 2),
196
+ absDiscount: expr.abs(p.discount),
197
+ }))
198
+ ```
199
+
200
+ ---
201
+
202
+ ### Date Functions (SELECT)
203
+
204
+ #### Extraction
205
+
206
+ | Method | SQL | Returns |
207
+ |--------|-----|---------|
208
+ | `expr.year(source)` | `YEAR` | Year (4-digit) |
209
+ | `expr.month(source)` | `MONTH` | Month (1-12) |
210
+ | `expr.day(source)` | `DAY` | Day (1-31) |
211
+ | `expr.hour(source)` | `HOUR` | Hour (0-23) |
212
+ | `expr.minute(source)` | `MINUTE` | Minute (0-59) |
213
+ | `expr.second(source)` | `SECOND` | Second (0-59) |
214
+ | `expr.isoWeek(source)` | `WEEK` | ISO week number (1-53) |
215
+ | `expr.isoWeekStartDate(source)` | -- | Monday of the week |
216
+ | `expr.isoYearMonth(source)` | -- | First day of the month |
217
+
218
+ #### Arithmetic
219
+
220
+ | Method | SQL | Description |
221
+ |--------|-----|-------------|
222
+ | `expr.dateDiff(unit, from, to)` | `DATEDIFF` | Difference between dates |
223
+ | `expr.dateAdd(unit, source, value)` | `DATEADD` | Add interval to date |
224
+ | `expr.formatDate(source, format)` | `DATE_FORMAT` | Format date as string |
225
+
226
+ **DateUnit values:** `"year"` | `"month"` | `"day"` | `"hour"` | `"minute"` | `"second"`
227
+
228
+ ```typescript
229
+ db.user().select((u) => ({
230
+ age: expr.dateDiff("year", u.birthDate, expr.val("DateOnly", DateOnly.today())),
231
+ expiresAt: expr.dateAdd("month", u.startDate, 12),
232
+ birthYear: expr.year(u.birthDate),
233
+ }))
234
+ ```
235
+
236
+ ---
237
+
238
+ ### Conditional Functions (SELECT)
239
+
240
+ #### `expr.coalesce(...args)`
241
+
242
+ Return first non-null value (`COALESCE`).
243
+
244
+ ```typescript
245
+ expr.coalesce(u.nickname, u.name, "Guest")
246
+ ```
247
+
248
+ #### `expr.nullIf(source, value)`
249
+
250
+ Return NULL if source equals value (`NULLIF`).
251
+
252
+ ```typescript
253
+ expr.nullIf(u.bio, "") // empty string -> NULL
254
+ ```
255
+
256
+ #### `expr.is(condition)`
257
+
258
+ Convert WHERE expression to boolean value for SELECT.
259
+
260
+ ```typescript
261
+ db.user().select((u) => ({
262
+ isActive: expr.is(expr.eq(u.status, "active")),
263
+ }))
264
+ ```
265
+
266
+ #### `expr.switch<T>()`
267
+
268
+ CASE WHEN expression builder (chaining API).
269
+
270
+ ```typescript
271
+ db.user().select((u) => ({
272
+ grade: expr.switch<string>()
273
+ .case(expr.gte(u.score, 90), "A")
274
+ .case(expr.gte(u.score, 80), "B")
275
+ .default("C"),
276
+ }))
277
+ ```
278
+
279
+ #### `expr.if(condition, then, else_)`
280
+
281
+ Simple IF/IIF expression.
282
+
283
+ ```typescript
284
+ expr.if(expr.gte(u.age, 18), "adult", "minor")
285
+ ```
286
+
287
+ ---
288
+
289
+ ### Aggregate Functions (SELECT)
290
+
291
+ All aggregate functions ignore NULL values. Return NULL only when all values are NULL or no rows.
292
+
293
+ | Method | SQL | Description |
294
+ |--------|-----|-------------|
295
+ | `expr.count(arg?, distinct?)` | `COUNT` | Row count (no arg = count all) |
296
+ | `expr.sum(arg)` | `SUM` | Sum of numeric column |
297
+ | `expr.avg(arg)` | `AVG` | Average of numeric column |
298
+ | `expr.max(arg)` | `MAX` | Maximum value |
299
+ | `expr.min(arg)` | `MIN` | Minimum value |
300
+
301
+ ```typescript
302
+ db.order().select((o) => ({
303
+ userId: o.userId,
304
+ totalAmount: expr.sum(o.amount),
305
+ orderCount: expr.count(),
306
+ uniqueProducts: expr.count(o.productId, true),
307
+ lastOrder: expr.max(o.createdAt),
308
+ })).groupBy((o) => [o.userId])
309
+ ```
310
+
311
+ ---
312
+
313
+ ### Other Functions
314
+
315
+ | Method | SQL | Description |
316
+ |--------|-----|-------------|
317
+ | `expr.greatest(...args)` | `GREATEST` | Maximum of multiple values |
318
+ | `expr.least(...args)` | `LEAST` | Minimum of multiple values |
319
+ | `expr.rowNum()` | `ROW_NUMBER` | Simple row number |
320
+ | `expr.random()` | `RAND/RANDOM` | Random number |
321
+ | `expr.cast(source, targetType)` | `CAST` | Type conversion |
322
+
323
+ ```typescript
324
+ expr.cast(u.id, { type: "varchar", length: 10 })
325
+ expr.greatest(u.score1, u.score2, u.score3)
326
+ ```
327
+
328
+ ---
329
+
330
+ ### Window Functions
331
+
332
+ #### `expr.window(fn, spec?)`
333
+
334
+ Apply a window function with OVER clause.
335
+
336
+ **Window function types:**
337
+
338
+ | Function | Description |
339
+ |----------|-------------|
340
+ | `{ type: "rowNumber" }` | ROW_NUMBER() |
341
+ | `{ type: "rank" }` | RANK() |
342
+ | `{ type: "denseRank" }` | DENSE_RANK() |
343
+ | `{ type: "ntile", n }` | NTILE(n) |
344
+ | `{ type: "lag", column, offset?, default? }` | LAG() |
345
+ | `{ type: "lead", column, offset?, default? }` | LEAD() |
346
+ | `{ type: "firstValue", column }` | FIRST_VALUE() |
347
+ | `{ type: "lastValue", column }` | LAST_VALUE() |
348
+ | `{ type: "sum", column }` | Window SUM |
349
+ | `{ type: "avg", column }` | Window AVG |
350
+ | `{ type: "count", column? }` | Window COUNT |
351
+ | `{ type: "min", column }` | Window MIN |
352
+ | `{ type: "max", column }` | Window MAX |
353
+
354
+ **WinSpec:**
355
+
356
+ ```typescript
357
+ {
358
+ partitionBy?: ExprInput[],
359
+ orderBy?: [ExprInput, ("ASC" | "DESC")?][],
360
+ }
361
+ ```
362
+
363
+ ```typescript
364
+ db.order().select((o) => ({
365
+ id: o.id,
366
+ rowNum: expr.window(
367
+ { type: "rowNumber" },
368
+ { partitionBy: [o.userId], orderBy: [[o.createdAt, "DESC"]] },
369
+ ),
370
+ runningTotal: expr.window(
371
+ { type: "sum", column: o.amount },
372
+ { partitionBy: [o.userId], orderBy: [[o.createdAt]] },
373
+ ),
374
+ }))
375
+ ```
376
+
377
+ ---
378
+
379
+ ### Subquery
380
+
381
+ #### `expr.subquery(query, fn)`
382
+
383
+ Scalar subquery for use in SELECT expressions.
384
+
385
+ ```typescript
386
+ db.user().select((u) => ({
387
+ name: u.name,
388
+ postCount: expr.subquery(
389
+ db.post().where((p) => [expr.eq(p.authorId, u.id)]),
390
+ (q) => expr.count(q.id),
391
+ ),
392
+ }))
393
+ ```
394
+
395
+ ---
396
+
397
+ ### Helper Types
398
+
399
+ #### `ExprUnit<TPrimitive>`
400
+
401
+ Type-safe expression wrapper that tracks the return type via TypeScript generics. Access `.n` to assert non-nullable.
402
+
403
+ #### `WhereExprUnit`
404
+
405
+ Expression wrapper for WHERE clause conditions.
406
+
407
+ #### `ExprInput<TPrimitive>`
408
+
409
+ Union type accepting either `ExprUnit<T>` or a literal value of type `T`.
410
+
411
+ ```typescript
412
+ type ExprInput<T> = ExprUnit<T> | T;
413
+ ```
@@ -0,0 +1,198 @@
1
+ # Query Builder
2
+
3
+ Converts `QueryDef` AST objects into dialect-specific SQL strings for MySQL, MSSQL, and PostgreSQL.
4
+
5
+ ## API Reference
6
+
7
+ ### `createQueryBuilder(dialect)`
8
+
9
+ Factory function that returns a dialect-specific `QueryBuilderBase` instance.
10
+
11
+ ```typescript
12
+ function createQueryBuilder(dialect: Dialect): QueryBuilderBase
13
+ ```
14
+
15
+ **Parameters:**
16
+ - `dialect` -- `"mysql"` | `"mssql"` | `"postgresql"`
17
+
18
+ **Returns:** `MysqlQueryBuilder` | `MssqlQueryBuilder` | `PostgresqlQueryBuilder`
19
+
20
+ ---
21
+
22
+ ### `QueryBuilderBase`
23
+
24
+ Abstract base class for SQL rendering. Contains shared logic and dispatches to dialect-specific implementations.
25
+
26
+ #### `build(def)`
27
+
28
+ Convert any `QueryDef` to SQL.
29
+
30
+ ```typescript
31
+ build(def: QueryDef): QueryBuildResult
32
+ ```
33
+
34
+ **QueryBuildResult:**
35
+
36
+ ```typescript
37
+ interface QueryBuildResult {
38
+ sql: string;
39
+ resultSetIndex?: number; // Which result set to read (for multi-statement queries)
40
+ resultSetStride?: number; // Read every Nth result set
41
+ }
42
+ ```
43
+
44
+ ---
45
+
46
+ ### `ExprRendererBase`
47
+
48
+ Abstract base class for rendering `Expr` AST nodes to SQL strings.
49
+
50
+ #### Key Methods
51
+
52
+ | Method | Description |
53
+ |--------|-------------|
54
+ | `render(expr)` | Render any Expr or WhereExpr to SQL string |
55
+ | `renderWhere(exprs)` | Render array of WhereExpr joined with AND |
56
+ | `wrap(name)` | Wrap identifier (MySQL: `` `name` ``, MSSQL: `[name]`, PostgreSQL: `"name"`) |
57
+ | `escapeString(value)` | Escape string for SQL literal |
58
+ | `escapeValue(value)` | Escape any value to appropriate SQL literal |
59
+
60
+ ---
61
+
62
+ ### Dialect Implementations
63
+
64
+ #### `MysqlQueryBuilder` / `MysqlExprRenderer`
65
+
66
+ MySQL 8.0.14+ specific SQL generation.
67
+
68
+ - Identifier quoting: `` `name` ``
69
+ - NULL-safe equality: `<=>`
70
+ - LATERAL JOIN support
71
+ - `LIMIT offset, count` syntax
72
+
73
+ #### `MssqlQueryBuilder` / `MssqlExprRenderer`
74
+
75
+ Microsoft SQL Server 2012+ specific SQL generation.
76
+
77
+ - Identifier quoting: `[name]`
78
+ - Three-part naming: `[database].[schema].[name]`
79
+ - `CROSS APPLY` / `OUTER APPLY` instead of LATERAL
80
+ - `OFFSET ... FETCH NEXT` for pagination
81
+ - `IDENTITY_INSERT` handling
82
+
83
+ #### `PostgresqlQueryBuilder` / `PostgresqlExprRenderer`
84
+
85
+ PostgreSQL 9.0+ specific SQL generation.
86
+
87
+ - Identifier quoting: `"name"`
88
+ - `LATERAL` JOIN support
89
+ - `LIMIT ... OFFSET` syntax
90
+ - `RETURN QUERY` for procedures
91
+
92
+ ---
93
+
94
+ ### Supported QueryDef Types
95
+
96
+ The query builder handles all `QueryDef` union types:
97
+
98
+ #### DML
99
+
100
+ | Type | Description |
101
+ |------|-------------|
102
+ | `select` | SELECT query |
103
+ | `insert` | INSERT query |
104
+ | `insertIfNotExists` | Conditional INSERT |
105
+ | `insertInto` | INSERT INTO ... SELECT |
106
+ | `update` | UPDATE query |
107
+ | `delete` | DELETE query |
108
+ | `upsert` | INSERT or UPDATE (MERGE) |
109
+
110
+ #### DDL -- Table
111
+
112
+ | Type | Description |
113
+ |------|-------------|
114
+ | `createTable` | CREATE TABLE |
115
+ | `dropTable` | DROP TABLE |
116
+ | `renameTable` | RENAME TABLE |
117
+ | `truncate` | TRUNCATE TABLE |
118
+
119
+ #### DDL -- Column
120
+
121
+ | Type | Description |
122
+ |------|-------------|
123
+ | `addColumn` | ALTER TABLE ADD COLUMN |
124
+ | `dropColumn` | ALTER TABLE DROP COLUMN |
125
+ | `modifyColumn` | ALTER TABLE MODIFY COLUMN |
126
+ | `renameColumn` | ALTER TABLE RENAME COLUMN |
127
+
128
+ #### DDL -- Constraints
129
+
130
+ | Type | Description |
131
+ |------|-------------|
132
+ | `addPrimaryKey` | ADD PRIMARY KEY |
133
+ | `dropPrimaryKey` | DROP PRIMARY KEY |
134
+ | `addForeignKey` | ADD FOREIGN KEY |
135
+ | `dropForeignKey` | DROP FOREIGN KEY |
136
+ | `addIndex` | CREATE INDEX |
137
+ | `dropIndex` | DROP INDEX |
138
+
139
+ #### DDL -- View/Procedure
140
+
141
+ | Type | Description |
142
+ |------|-------------|
143
+ | `createView` | CREATE VIEW |
144
+ | `dropView` | DROP VIEW |
145
+ | `createProc` | CREATE PROCEDURE |
146
+ | `dropProc` | DROP PROCEDURE |
147
+ | `execProc` | EXECUTE PROCEDURE |
148
+
149
+ #### Utility
150
+
151
+ | Type | Description |
152
+ |------|-------------|
153
+ | `clearSchema` | Drop all objects in schema |
154
+ | `schemaExists` | Check schema existence |
155
+ | `switchFk` | Enable/disable FK constraints |
156
+
157
+ ---
158
+
159
+ ## Usage Examples
160
+
161
+ ### Build SQL from QueryDef
162
+
163
+ ```typescript
164
+ import { createQueryBuilder } from "@simplysm/orm-common";
165
+
166
+ const builder = createQueryBuilder("mysql");
167
+ const result = builder.build({
168
+ type: "select",
169
+ from: { database: "mydb", name: "User" },
170
+ as: "T1",
171
+ select: {
172
+ id: { type: "column", path: ["T1", "id"] },
173
+ name: { type: "column", path: ["T1", "name"] },
174
+ },
175
+ where: [{
176
+ type: "eq",
177
+ source: { type: "column", path: ["T1", "status"] },
178
+ target: { type: "value", value: "active" },
179
+ }],
180
+ });
181
+
182
+ console.log(result.sql);
183
+ // SELECT `T1`.`id` AS `id`, `T1`.`name` AS `name`
184
+ // FROM `mydb`.`User` AS `T1`
185
+ // WHERE `T1`.`status` <=> 'active'
186
+ ```
187
+
188
+ ### Multi-dialect Testing
189
+
190
+ ```typescript
191
+ import { createQueryBuilder, dialects } from "@simplysm/orm-common";
192
+
193
+ for (const dialect of dialects) {
194
+ const builder = createQueryBuilder(dialect);
195
+ const result = builder.build(queryDef);
196
+ console.log(`[${dialect}] ${result.sql}`);
197
+ }
198
+ ```